From 98a7ac9bd604dde55f55f1cd199d144bce3c8665 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 5 Sep 2024 15:22:09 +0300 Subject: [PATCH 01/94] Topic get info changes Signed-off-by: ibankov --- .../services/consensus_create_topic.proto | 16 +++++++ .../services/consensus_topic_info.proto | 16 +++++++ .../services/consensus_update_topic.proto | 16 +++++++ .../services/custom_fees.proto | 12 +++++ .../services/state/consensus/topic.proto | 13 +++++ .../ConsensusGetTopicInfoHandler.java | 3 ++ .../handlers/ConsensusDeleteTopicTest.java | 3 ++ .../handlers/ConsensusGetTopicInfoTest.java | 3 ++ .../impl/test/handlers/ConsensusTestBase.java | 22 ++++++++- .../test/handlers/ConsensusTestUtils.java | 5 +- .../queries/consensus/HapiGetTopicInfo.java | 48 +++++++++++++++++-- .../transactions/token/CustomFeeSpecs.java | 12 +++++ .../transactions/token/CustomFeeTests.java | 18 +++++++ 13 files changed, 181 insertions(+), 6 deletions(-) diff --git a/hapi/hedera-protobufs/services/consensus_create_topic.proto b/hapi/hedera-protobufs/services/consensus_create_topic.proto index e036c24c71b4..1da6cef7532c 100644 --- a/hapi/hedera-protobufs/services/consensus_create_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_create_topic.proto @@ -27,6 +27,7 @@ option java_package = "com.hederahashgraph.api.proto.java"; option java_multiple_files = true; import "basic_types.proto"; +import "custom_fees.proto"; import "duration.proto"; /** @@ -70,4 +71,19 @@ message ConsensusCreateTopicTransactionBody { * If specified, there must be an adminKey and the autoRenewAccount must sign this transaction. */ AccountID autoRenewAccount = 7; + + /** + * Access control for update/delete of custom fees. Null if there is no key. + */ + Key fee_schedule_key = 8; + + /** + * If the transaction contains a signer from this list, no custom fees are applied. + */ + repeated Key free_messages_key_list = 9; + + /** + * The custom fee to be assessed during a message submission to this topic + */ + repeated ConsensusCustomFee custom_fees = 10; } diff --git a/hapi/hedera-protobufs/services/consensus_topic_info.proto b/hapi/hedera-protobufs/services/consensus_topic_info.proto index 749dd4954f29..04e977c1268b 100644 --- a/hapi/hedera-protobufs/services/consensus_topic_info.proto +++ b/hapi/hedera-protobufs/services/consensus_topic_info.proto @@ -27,6 +27,7 @@ option java_package = "com.hederahashgraph.api.proto.java"; option java_multiple_files = true; import "basic_types.proto"; +import "custom_fees.proto"; import "duration.proto"; import "timestamp.proto"; @@ -87,4 +88,19 @@ message ConsensusTopicInfo { * The ledger ID the response was returned from; please see HIP-198 for the network-specific IDs. */ bytes ledger_id = 9; + + /** + * Access control for update/delete of custom fees. Null if there is no key. + */ + Key fee_schedule_key = 10; + + /** + * If the transaction contains a signer from this list, no custom fees are applied. + */ + repeated Key free_messages_key_list = 11; + + /* + * The custom fee to be assessed during a message submission to this topic + */ + repeated ConsensusCustomFee custom_fees = 12; } diff --git a/hapi/hedera-protobufs/services/consensus_update_topic.proto b/hapi/hedera-protobufs/services/consensus_update_topic.proto index 9ca6a55aee28..1a2ceb1fdcd9 100644 --- a/hapi/hedera-protobufs/services/consensus_update_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_update_topic.proto @@ -28,6 +28,7 @@ option java_multiple_files = true; import "google/protobuf/wrappers.proto"; import "basic_types.proto"; +import "custom_fees.proto"; import "duration.proto"; import "timestamp.proto"; @@ -87,4 +88,19 @@ message ConsensusUpdateTopicTransactionBody { * If unspecified, no change. */ AccountID autoRenewAccount = 9; + + /** + * Access control for update/delete of custom fees. Null if there is no key. + */ + Key fee_schedule_key = 10; + + /** + * If the transaction contains a signer from this list, no custom fees are applied. + */ + repeated Key free_messages_key_list = 11; + + /* + * The custom fee to be assessed during a message submission to this topic + */ + repeated ConsensusCustomFee custom_fees = 12; } diff --git a/hapi/hedera-protobufs/services/custom_fees.proto b/hapi/hedera-protobufs/services/custom_fees.proto index b01359d97327..466638367568 100644 --- a/hapi/hedera-protobufs/services/custom_fees.proto +++ b/hapi/hedera-protobufs/services/custom_fees.proto @@ -173,3 +173,15 @@ message AssessedCustomFee { */ repeated AccountID effective_payer_account_id = 4; } + +message ConsensusCustomFee { + /** + * Fixed fee to be charged + */ + FixedFee fixed_fee = 1; + + /** + * The account to receive the custom fee + */ + AccountID fee_collector_account_id = 2; +} diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 09e62cfaeeee..d99af7363dec 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -23,6 +23,7 @@ package proto; */ import "basic_types.proto"; +import "custom_fees.proto"; option java_package = "com.hederahashgraph.api.proto.java"; // <<>> This comment is special code for setting PBJ Compiler java package @@ -98,4 +99,16 @@ message Topic { * If present, enforces access control for message submission to the topic. */ Key submit_key = 10; + /** + * If present, enforces access control for update/delete of this topic custom fees. + */ + Key fee_schedule_key = 11; + /** + * If the transaction contains a signer from this list, no custom fees are applied. + */ + repeated Key free_messages_key_list = 12; + /* + * The custom fee to be assessed during a message submission to this topic + */ + repeated ConsensusCustomFee custom_fees = 13; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java index 0465b707dc75..af2e2cae4fa1 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java @@ -163,6 +163,9 @@ private Optional infoForTopic( if (!isEmpty(meta.submitKey())) info.submitKey(meta.submitKey()); info.autoRenewPeriod(Duration.newBuilder().seconds(meta.autoRenewPeriod())); if (meta.hasAutoRenewAccountId()) info.autoRenewAccount(meta.autoRenewAccountId()); + if (meta.hasFeeScheduleKey()) info.feeScheduleKey(meta.feeScheduleKey()); + if (!meta.freeMessagesKeyList().isEmpty()) info.freeMessagesKeyList(meta.freeMessagesKeyList()); + if (!meta.customFees().isEmpty()) info.customFees(meta.customFees()); info.ledgerId(config.id()); return Optional.of(info.build()); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java index 205c9dc94405..aeb1abbf9843 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java @@ -201,6 +201,9 @@ void adminKeyDoesntExist() { Bytes.wrap(runningHash), memo, null, + null, + null, + null, null); writableTopicState = writableTopicStateWithOneKey(); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java index 2c72206d8880..329e2fb1678c 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java @@ -228,6 +228,9 @@ private ConsensusTopicInfo getExpectedInfo() { .autoRenewAccount(topic.autoRenewAccountId()) .autoRenewPeriod(WELL_KNOWN_AUTO_RENEW_PERIOD) .ledgerId(new BytesConverter().convert("0x03")) + .feeScheduleKey(feeScheduleKey) + .freeMessagesKeyList(key, anotherKey) + .customFees(customFees) .build(); } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java index 6adf10993836..9d1c23afa51d 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java @@ -27,6 +27,8 @@ 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.FixedFee; import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.ReadableTopicStoreImpl; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; @@ -41,6 +43,7 @@ import com.swirlds.state.test.fixtures.MapWritableKVState; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.Instant; +import java.util.List; import java.util.function.Function; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; @@ -52,6 +55,7 @@ public class ConsensusTestBase { private static final String A_NAME = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; private static final String B_NAME = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; private static final String C_NAME = "cccccccccccccccccccccccccccccccc"; + private static final String SCHEDULE_KEY = "scheduleKey"; private static final Function KEY_BUILDER = value -> Key.newBuilder().ed25519(Bytes.wrap(value.getBytes())); public static final Key A_THRESHOLD_KEY = Key.newBuilder() @@ -82,8 +86,12 @@ public class ConsensusTestBase { KEY_BUILDER.apply(B_NAME).build(), A_COMPLEX_KEY))) .build(); + public static final Key SHEDULE_KEY = Key.newBuilder() + .keyList(KeyList.newBuilder().keys(KEY_BUILDER.apply(SCHEDULE_KEY).build())) + .build(); protected final Key key = A_COMPLEX_KEY; protected final Key anotherKey = B_COMPLEX_KEY; + protected final Key feeScheduleKey = SHEDULE_KEY; protected final AccountID payerId = AccountID.newBuilder().accountNum(3).build(); public static final AccountID anotherPayer = AccountID.newBuilder().accountNum(13257).build(); @@ -102,6 +110,10 @@ 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 customFees = List.of(ConsensusCustomFee.newBuilder() + .fixedFee(FixedFee.newBuilder().amount(1).build()) + .feeCollectorAccountId(anotherPayer) + .build()); protected Topic topic; @@ -209,7 +221,10 @@ protected void givenValidTopic( Bytes.wrap(runningHash), memo, withAdminKey ? key : null, - withSubmitKey ? key : null); + withSubmitKey ? key : null, + feeScheduleKey, + List.of(key, anotherKey), + customFees); topicNoKeys = new Topic( topicId, sequenceNumber, @@ -220,7 +235,10 @@ protected void givenValidTopic( Bytes.wrap(runningHash), memo, null, - null); + null, + feeScheduleKey, + List.of(key, anotherKey), + customFees); } protected Topic createTopic() { diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java index a0b017a1c002..ea69bd5640f9 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java @@ -75,6 +75,9 @@ static Topic newTopic(Key admin, Key submit) { null, "memo", admin, - submit); + submit, + null, + null, + null); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java index 0535709a16f7..ed30bf3d29df 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java @@ -22,11 +22,13 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.common.base.MoreObjects; 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.HederaFunctionality; @@ -35,8 +37,11 @@ import com.hederahashgraph.api.proto.java.Transaction; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import java.util.OptionalLong; +import java.util.function.BiConsumer; import java.util.function.LongConsumer; import java.util.function.LongSupplier; import org.apache.logging.log4j.LogManager; @@ -56,9 +61,13 @@ public class HapiGetTopicInfo extends HapiQueryOp { private OptionalLong autoRenewPeriod = OptionalLong.empty(); private boolean hasNoAdminKey = false; private boolean hasNoSubmitKey = false; + private boolean hasNoFeeScheduleKey = false; private Optional adminKey = Optional.empty(); private Optional submitKey = Optional.empty(); private Optional autoRenewAccount = Optional.empty(); + private Optional feeScheduleKey = Optional.empty(); + private final List expectedFreeMessagesKeyList = new ArrayList<>(); + private final List>> expectedFees = new ArrayList<>(); private boolean saveRunningHash = false; private Optional seqNoInfoObserver = Optional.empty(); @@ -129,6 +138,16 @@ public HapiGetTopicInfo hasSubmitKey(String exp) { return this; } + public HapiGetTopicInfo hasFeeScheduleKey(final String exp) { + feeScheduleKey = Optional.of(exp); + return this; + } + + public HapiGetTopicInfo hasNoFeeScheduleKey() { + hasNoFeeScheduleKey = true; + return this; + } + public HapiGetTopicInfo hasAutoRenewAccount(String exp) { autoRenewAccount = Optional.of(exp); return this; @@ -144,6 +163,16 @@ public HapiGetTopicInfo savingSeqNoTo(LongConsumer consumer) { return this; } + public HapiGetTopicInfo hasFreeMessagesKeys(List freeMessagesKeyAssertion) { + expectedFreeMessagesKeyList.addAll(freeMessagesKeyAssertion); + return this; + } + + public HapiGetTopicInfo hasCustom(BiConsumer> feeAssertion) { + expectedFees.add(feeAssertion); + return this; + } + @Override public HederaFunctionality type() { return HederaFunctionality.ConsensusGetTopicInfo; @@ -171,9 +200,7 @@ protected void assertExpectationsGiven(HapiSpec spec) { expiryObserver.accept(info.getExpirationTime().getSeconds()); } topicMemo.ifPresent(exp -> assertEquals(exp, info.getMemo(), "Bad memo!")); - if (seqNoFn.isPresent()) { - seqNo = OptionalLong.of(seqNoFn.get().getAsLong()); - } + seqNoFn.ifPresent(longSupplier -> seqNo = OptionalLong.of(longSupplier.getAsLong())); seqNo.ifPresent(exp -> assertEquals(exp, info.getSequenceNumber(), "Bad sequence number!")); seqNoInfoObserver.ifPresent(obs -> obs.accept(info.getSequenceNumber())); runningHashEntry.ifPresent( @@ -185,6 +212,8 @@ protected void assertExpectationsGiven(HapiSpec spec) { exp -> assertEquals(exp, info.getAutoRenewPeriod().getSeconds(), "Bad auto-renew period!")); adminKey.ifPresent(exp -> assertEquals(spec.registry().getKey(exp), info.getAdminKey(), "Bad admin key!")); submitKey.ifPresent(exp -> assertEquals(spec.registry().getKey(exp), info.getSubmitKey(), "Bad submit key!")); + feeScheduleKey.ifPresent( + exp -> assertEquals(spec.registry().getKey(exp), info.getFeeScheduleKey(), "Bad fee schedule key!")); autoRenewAccount.ifPresent( exp -> assertEquals(asId(exp, spec), info.getAutoRenewAccount(), "Bad auto-renew account!")); if (hasNoAdminKey) { @@ -194,6 +223,19 @@ protected void assertExpectationsGiven(HapiSpec spec) { if (hasNoSubmitKey) { assertFalse(info.hasSubmitKey(), "Should have no submit key!"); } + if (hasNoFeeScheduleKey) { + assertFalse(info.hasFeeScheduleKey(), "Should have no fee schedule key!"); + } + final var actualFees = info.getCustomFeesList(); + for (var expectedFee : expectedFees) { + expectedFee.accept(spec, actualFees); + } + var actualFreeMessagesKeys = info.getFreeMessagesKeyListList(); + for (var expectedKey : expectedFreeMessagesKeyList) { + assertTrue( + actualFreeMessagesKeys.contains(spec.registry().getKey(expectedKey)), + "Doesn't contain free messages key!"); + } expectedLedgerId.ifPresent(id -> Assertions.assertEquals(id, info.getLedgerId())); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java index c5ca48428604..ceba5d2af2be 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java @@ -22,6 +22,7 @@ import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.transactions.TxnUtils; +import com.hederahashgraph.api.proto.java.ConsensusCustomFee; import com.hederahashgraph.api.proto.java.CustomFee; import com.hederahashgraph.api.proto.java.FixedFee; import com.hederahashgraph.api.proto.java.Fraction; @@ -126,6 +127,10 @@ static CustomFee builtFixedHbar(long amount, String collector, boolean allCollec return baseFixedBuilder(amount, collector, allCollectorsExempt, spec).build(); } + static ConsensusCustomFee builtFixedTopicHbar(long amount, String collector, HapiSpec spec) { + return baseFixedTopicBuilder(amount, collector, spec).build(); + } + static FixedFee builtFixedHbarSansCollector(long amount) { return FixedFee.newBuilder().setAmount(amount).build(); } @@ -213,4 +218,11 @@ static CustomFee.Builder baseFixedBuilder( .setFixedFee(fixedBuilder) .setFeeCollectorAccountId(collectorId); } + + static ConsensusCustomFee.Builder baseFixedTopicBuilder(long amount, String collector, HapiSpec spec) { + final var collectorId = + isIdLiteral(collector) ? asAccount(collector) : spec.registry().getAccountID(collector); + final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); + return ConsensusCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collectorId); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java index 78db32c4bee5..41da013e3cc5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java @@ -24,6 +24,7 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFeeInheritingRoyaltyCollector; import com.hedera.services.bdd.spec.HapiSpec; +import com.hederahashgraph.api.proto.java.ConsensusCustomFee; import com.hederahashgraph.api.proto.java.CustomFee; import java.util.List; import java.util.OptionalLong; @@ -91,6 +92,13 @@ public static BiConsumer> royaltyFeeWithFallbackInToke }; } + public static BiConsumer> fixedTopicHbarFee(long amount, String collector) { + return (spec, actual) -> { + final var expected = CustomFeeSpecs.builtFixedTopicHbar(amount, collector, spec); + failUnlessConsensusFeePresent("fixed ℏ", actual, expected); + }; + } + private static void failUnlessPresent(String detail, List actual, CustomFee expected) { for (var customFee : actual) { if (expected.equals(customFee)) { @@ -99,4 +107,14 @@ private static void failUnlessPresent(String detail, List actual, Cus } Assertions.fail("Expected a " + detail + " fee " + expected + ", but only had: " + actual); } + + private static void failUnlessConsensusFeePresent( + String detail, List actual, ConsensusCustomFee expected) { + for (var customFee : actual) { + if (expected.equals(customFee)) { + return; + } + } + Assertions.fail("Expected a " + detail + " fee " + expected + ", but only had: " + actual); + } } From a1e12570b31b5f08c6ce8e698135e5a036d883f6 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 5 Sep 2024 16:53:18 +0300 Subject: [PATCH 02/94] add temporary fix for topic update unit test Signed-off-by: ibankov --- .../consensus/impl/handlers/ConsensusUpdateTopicHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java index d3c56633ff99..a93ceed65dbd 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java @@ -161,6 +161,10 @@ public void handle(@NonNull final HandleContext handleContext) { builder.sequenceNumber(topic.sequenceNumber()); builder.runningHash(topic.runningHash()); builder.deleted(topic.deleted()); + // TODO: added so unit tests pass (fix this when implementing the actual logic) + builder.feeScheduleKey(topic.feeScheduleKey()); + builder.freeMessagesKeyList(topic.freeMessagesKeyList()); + builder.customFees(topic.customFees()); // And then resolve mutable attributes, and put the new topic back resolveMutableBuilderAttributes(handleContext, op, builder, topic); topicStore.put(builder.build()); From 36d7f4bb68edcefa72ff0019a7c2e223f7c969da Mon Sep 17 00:00:00 2001 From: Kim Rader Date: Sun, 8 Sep 2024 11:58:43 -0700 Subject: [PATCH 03/94] Update documentation on protobuf changes Signed-off-by: Kim Rader --- .../services/consensus_create_topic.proto | 28 +++++++++++--- .../services/consensus_topic_info.proto | 30 ++++++++++++--- .../services/consensus_update_topic.proto | 21 ++++++++--- .../services/custom_fees.proto | 24 ++++++++++-- .../services/state/consensus/topic.proto | 37 ++++++++++++++++--- 5 files changed, 114 insertions(+), 26 deletions(-) diff --git a/hapi/hedera-protobufs/services/consensus_create_topic.proto b/hapi/hedera-protobufs/services/consensus_create_topic.proto index 1da6cef7532c..6a6aa6bc780c 100644 --- a/hapi/hedera-protobufs/services/consensus_create_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_create_topic.proto @@ -73,17 +73,35 @@ message ConsensusCreateTopicTransactionBody { AccountID autoRenewAccount = 7; /** - * Access control for update/delete of custom fees. Null if there is no key. + * Access control for update/delete of custom fees. + *

+ * If unset, custom fees CANNOT be set for this topic.
+ * If not set when the topic is created, this field CANNOT be set via update. + * If set when the topic is created, this field CAN be changed via update. */ Key fee_schedule_key = 8; /** - * If the transaction contains a signer from this list, no custom fees are applied. - */ + * A set of keys that are allowed to submit messages to the topic without + * paying the topic's custom fees. + *

+ * If a submit transaction is signed by _any_ key included in this set, + * custom fees SHALL NOT be charged for that transaction. + *

+ * If free_messages_key_list is unset, the following keys are exempt + * from custom fees: adminKey, submitKey, fee_schedule_key. + */ repeated Key free_messages_key_list = 9; /** - * The custom fee to be assessed during a message submission to this topic - */ + * A set of custom fee definitions.
+ * These are fees to be assessed for each submit to this topic. + *

+ * Each fee defined in this set SHALL be evaluated for + * each message submitted to this topic, and the resultant + * total assessed fees SHALL be charged.
+ * Custom fees defined here SHALL be in addition to the base + * network and node fees. + */ repeated ConsensusCustomFee custom_fees = 10; } diff --git a/hapi/hedera-protobufs/services/consensus_topic_info.proto b/hapi/hedera-protobufs/services/consensus_topic_info.proto index 04e977c1268b..9a893d143de3 100644 --- a/hapi/hedera-protobufs/services/consensus_topic_info.proto +++ b/hapi/hedera-protobufs/services/consensus_topic_info.proto @@ -90,17 +90,35 @@ message ConsensusTopicInfo { bytes ledger_id = 9; /** - * Access control for update/delete of custom fees. Null if there is no key. - */ + * Access control for update/delete of custom fees. + *

+ * If unset, custom fees CANNOT be set for this topic.
+ * If not set when the topic is created, this field CANNOT be set via update. + * If set when the topic is created, this field CAN be changed via update. + */ Key fee_schedule_key = 10; /** - * If the transaction contains a signer from this list, no custom fees are applied. - */ + * A set of keys that are allowed to submit messages to the topic without + * paying the topic's custom fees. + *

+ * If a submit transaction is signed by _any_ key included in this set, + * custom fees SHALL NOT be charged for that transaction. + *

+ * If free_messages_key_list is unset, the following keys are exempt + * from custom fees: adminKey, submitKey, fee_schedule_key. + */ repeated Key free_messages_key_list = 11; - /* - * The custom fee to be assessed during a message submission to this topic + /** + * A set of custom fee definitions.
+ * These are fees to be assessed for each submit to this topic. + *

+ * Each fee defined in this set SHALL be evaluated for + * each message submitted to this topic, and the resultant + * total assessed fees SHALL be charged.
+ * Custom fees defined here SHALL be in addition to the base + * network and node fees. */ repeated ConsensusCustomFee custom_fees = 12; } diff --git a/hapi/hedera-protobufs/services/consensus_update_topic.proto b/hapi/hedera-protobufs/services/consensus_update_topic.proto index 1a2ceb1fdcd9..d3a9ca778cad 100644 --- a/hapi/hedera-protobufs/services/consensus_update_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_update_topic.proto @@ -90,17 +90,28 @@ message ConsensusUpdateTopicTransactionBody { AccountID autoRenewAccount = 9; /** - * Access control for update/delete of custom fees. Null if there is no key. + * Access control for update/delete of custom fees. + *

+ * If unset in state, this field MUST NOT be set.
+ * If unset in this transaction and set in state, the current value SHALL + * be removed.
+ * If set in this transaction, the existing key MUST sign this transaction. */ Key fee_schedule_key = 10; /** - * If the transaction contains a signer from this list, no custom fees are applied. - */ + * A set of keys that are allowed to submit messages to the topic without + * paying the topic's custom fees. + *

+ * If this list is empty, the current set of keys is unchanged. + */ repeated Key free_messages_key_list = 11; - /* - * The custom fee to be assessed during a message submission to this topic + /** + * A set of custom fee definitions.
+ * These are fees to be assessed for each submit to this topic. + *

+ * If this list is empty, the current set of fees is unchanged. */ repeated ConsensusCustomFee custom_fees = 12; } diff --git a/hapi/hedera-protobufs/services/custom_fees.proto b/hapi/hedera-protobufs/services/custom_fees.proto index 466638367568..a2efee502317 100644 --- a/hapi/hedera-protobufs/services/custom_fees.proto +++ b/hapi/hedera-protobufs/services/custom_fees.proto @@ -174,14 +174,30 @@ message AssessedCustomFee { repeated AccountID effective_payer_account_id = 4; } +/** + * A custom fee definition for a consensus topic. + *

+ * This fee definition is specific to an Hedera Consensus Service (HCS) topic + * and SHOULD NOT be used in any other context.
+ * All fields for this message are REQUIRED.
+ * Only "fixed" fee definitions are supported because there is no basis for + * a fractional fee on a consensus submit transaction. + */ message ConsensusCustomFee { /** - * Fixed fee to be charged - */ + * A fixed custom fee. + *

+ * The amount of HBAR or other token described by this `FixedFee` SHALL + * be charged to the transction payer for each message submitted to a + * topic that assigns this consensus custom fee. + */ FixedFee fixed_fee = 1; /** - * The account to receive the custom fee - */ + * A collection account identifier. + *

+ * All amounts collected for this consensus custom fee SHALL be transferred + * to the account identified by this field. + */ AccountID fee_collector_account_id = 2; } diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index d99af7363dec..4414c8b47031 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -49,6 +49,9 @@ option java_multiple_files = true; * 5. A memo string whose UTF-8 encoding is at most 100 bytes. * 6. (Optional) An admin key whose signature must be active for the topic's metadata to be updated. * 7. (Optional) A submit key whose signature must be active for the topic to receive a message. + * 8. (Optional) A fee schedule key whose signature must be active for the topic's custom fees to be updated. + * 9. (Optional) A list of keys that can submit messages without paying custom fees. + * 10. (Optional) A list of custom fees to be assessed for each message submitted to the topic. */ message Topic { /** @@ -99,16 +102,38 @@ message Topic { * If present, enforces access control for message submission to the topic. */ Key submit_key = 10; + /** - * If present, enforces access control for update/delete of this topic custom fees. - */ + * Access control for update/delete of custom fees. + *

+ * If this field is unset, the current custom fees CANNOT be changed.
+ * If this field is set, that `Key` MUST sign any transaction to update + * the custom fee schedule for this topic. + */ Key fee_schedule_key = 11; + /** - * If the transaction contains a signer from this list, no custom fees are applied. - */ + * A list of "privileged payer" keys. + *

+ * If a submit transaction is signed by _any_ key from this list, + * custom fees SHALL NOT be charged for that transaction.
+ * If free_messages_key_list is unset, it SHALL _implicitly_ contain + * the key `admin_key`, the key `submit_key`, and the key + * `fee_schedule_key`, if any of those keys are set. + */ repeated Key free_messages_key_list = 12; - /* - * The custom fee to be assessed during a message submission to this topic + + /** + * A set of custom fee definitions.
+ * These are fees to be assessed for each submit to this topic. + *

+ * If this list is empty, the only fees charged for a submit to this + * topic SHALL be the network and node fees.
+ * If this list is not empty, each fee defined in this set SHALL + * be evaluated for each message submitted to this topic, and the + * resultant total assessed fees SHALL be charged.
+ * 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; } From dca8c7445003ff49fe9b8b1aae680ff9c25661fd Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 9 Sep 2024 17:45:02 +0300 Subject: [PATCH 04/94] Add custom fees to ConsensusCreateTopicHandler Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 4 + .../hedera/node/config/data/TopicsConfig.java | 5 +- .../handlers/ConsensusCreateTopicHandler.java | 118 ++++++++++++---- .../ConsensusCustomFeesValidator.java | 131 ++++++++++++++++++ .../src/main/java/module-info.java | 3 + .../handlers/ConsensusCreateTopicTest.java | 3 +- .../src/main/java/module-info.java | 3 +- .../consensus/HapiTopicCreate.java | 43 ++++++ .../transactions/token/CustomFeeSpecs.java | 30 ++++ .../bdd/suites/hip991/TopicCustomFeeTest.java | 110 +++++++++++++++ 10 files changed, 420 insertions(+), 30 deletions(-) create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 1327de4fee91..fe3da22d3a41 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1593,4 +1593,8 @@ enum ResponseCodeEnum { * airdrop and whether the sender can fulfill the offer. */ INVALID_TOKEN_IN_PENDING_AIRDROP = 369; + + MAX_ENTRIES_FOR_FMKL_EXCEEDED = 370; + FMKL_CONTAINS_DUPLICATED_KEYS = 371; + INVALID_KEY_IN_FMKL = 372; } diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java index e778c57d7966..6379e5c70535 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java @@ -21,4 +21,7 @@ import com.swirlds.config.api.ConfigProperty; @ConfigData("topics") -public record TopicsConfig(@ConfigProperty(defaultValue = "1000000") @NetworkProperty long maxNumber) {} +public record TopicsConfig( + @ConfigProperty(defaultValue = "1000000") @NetworkProperty long maxNumber, + @ConfigProperty(defaultValue = "10") @NetworkProperty int maxCustoFeeEntriesForTopics, + @ConfigProperty(defaultValue = "10") @NetworkProperty int maxEntriesForFreeMessagesKeyList) {} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index 85cfeba7bc62..e298447ef977 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -19,25 +19,38 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTORENEW_ACCOUNT_NOT_ALLOWED; import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTORENEW_DURATION_NOT_IN_RANGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.BAD_ENCODING; +import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEES_LIST_TOO_LONG; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_EXPIRATION_TIME; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FMKL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FMKL_EXCEEDED; import static com.hedera.node.app.hapi.utils.fee.ConsensusServiceFeeBuilder.getConsensusCreateTopicFee; import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.RUNNING_HASH_BYTE_ARRAY_SIZE; import static com.hedera.node.app.spi.validation.AttributeValidator.isImmutableKey; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.Duration; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.SubType; import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.consensus.ConsensusCreateTopicTransactionBody; import com.hedera.hapi.node.state.consensus.Topic; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.hapi.utils.CommonPbjConverters; import com.hedera.node.app.hapi.utils.fee.SigValueObj; +import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.records.ConsensusCreateTopicStreamBuilder; +import com.hedera.node.app.service.consensus.impl.validators.ConsensusCustomFeesValidator; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.ReadableTokenRelationStore; +import com.hedera.node.app.service.token.ReadableTokenStore; +// import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.validation.ExpiryMeta; @@ -58,14 +71,23 @@ */ @Singleton public class ConsensusCreateTopicHandler implements TransactionHandler { + private final ConsensusCustomFeesValidator customFeesValidator; + + /** + * Default constructor for injection. + * @param customFeesValidator custom fees validator + */ @Inject - public ConsensusCreateTopicHandler() { - // Exists for injection + public ConsensusCreateTopicHandler(@NonNull final ConsensusCustomFeesValidator customFeesValidator) { + requireNonNull(customFeesValidator); + this.customFeesValidator = customFeesValidator; } @Override public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { - // nothing to do + final var op = txn.consensusCreateTopicOrThrow(); + final var uniqueKeysCount = op.freeMessagesKeyList().stream().distinct().count(); + validateTruePreCheck(uniqueKeysCount == op.freeMessagesKeyList().size(), INVALID_TRANSACTION_BODY); } @Override @@ -98,33 +120,10 @@ public void handle(@NonNull final HandleContext handleContext) { requireNonNull(handleContext, "The argument 'context' must not be null"); final var op = handleContext.body().consensusCreateTopicOrThrow(); - - final var configuration = handleContext.configuration(); - final var topicConfig = configuration.getConfigData(TopicsConfig.class); final var topicStore = handleContext.storeFactory().writableStore(WritableTopicStore.class); final var builder = new Topic.Builder(); - - /* Validate admin and submit keys and set them. Empty key list is allowed and is used for immutable entities */ - if (op.hasAdminKey() && !isImmutableKey(op.adminKey())) { - handleContext.attributeValidator().validateKey(op.adminKey()); - builder.adminKey(op.adminKey()); - } - - // submitKey() is not checked in preCheck() - if (op.hasSubmitKey()) { - handleContext.attributeValidator().validateKey(op.submitKey()); - builder.submitKey(op.submitKey()); - } - - /* Validate if the current topic can be created */ - if (topicStore.sizeOfState() >= topicConfig.maxNumber()) { - throw new HandleException(MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); - } - - /* Validate the topic memo */ - handleContext.attributeValidator().validateMemo(op.memo()); - builder.memo(op.memo()); + validateSemantics(op, handleContext, builder); final var impliedExpiry = handleContext.consensusNow().getEpochSecond() + op.autoRenewPeriodOrElse(Duration.DEFAULT).seconds(); @@ -174,6 +173,71 @@ public void handle(@NonNull final HandleContext handleContext) { } } + private void validateSemantics( + ConsensusCreateTopicTransactionBody op, HandleContext handleContext, Topic.Builder builder) { + + final var configuration = handleContext.configuration(); + final var topicConfig = configuration.getConfigData(TopicsConfig.class); + final var topicStore = handleContext.storeFactory().readableStore(ReadableTopicStore.class); + final var accountStore = handleContext.storeFactory().readableStore(ReadableAccountStore.class); + final var tokenStore = handleContext.storeFactory().readableStore(ReadableTokenStore.class); + final var tokenRelStore = handleContext.storeFactory().readableStore(ReadableTokenRelationStore.class); + + // validate max size of lists in the transaction body + if (!op.freeMessagesKeyList().isEmpty()) { + validateTrue( + op.freeMessagesKeyList().size() <= topicConfig.maxEntriesForFreeMessagesKeyList(), + MAX_ENTRIES_FOR_FMKL_EXCEEDED); + } + + if (!op.customFees().isEmpty()) { + validateTrue( + op.customFees().size() <= topicConfig.maxCustoFeeEntriesForTopics(), CUSTOM_FEES_LIST_TOO_LONG); + } + + /* Validate admin and submit keys and set them. Empty key list is allowed and is used for immutable entities */ + if (op.hasAdminKey() && !isImmutableKey(op.adminKey())) { + handleContext.attributeValidator().validateKey(op.adminKey()); + builder.adminKey(op.adminKey()); + } + + // submitKey() is not checked in preCheck() + if (op.hasSubmitKey()) { + handleContext.attributeValidator().validateKey(op.submitKey()); + builder.submitKey(op.submitKey()); + } + + // validate keys + if (op.hasFeeScheduleKey()) { + handleContext.attributeValidator().validateKey(op.feeScheduleKey(), INVALID_CUSTOM_FEE_SCHEDULE_KEY); + builder.feeScheduleKey(op.feeScheduleKey()); + } + + // validate keys + if (!op.freeMessagesKeyList().isEmpty()) { + op.freeMessagesKeyList() + .forEach(key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FMKL)); + builder.freeMessagesKeyList(op.freeMessagesKeyList()); + } + + // validate custom fees + if (!op.customFees().isEmpty()) { + // todo check if token is frozen to fee collector in token create handler + customFeesValidator.validateForCreation( + accountStore, tokenRelStore, tokenStore, op.customFees(), handleContext.expiryValidator()); + builder.customFees(op.customFees()); + } + + /* Validate if the current topic can be created */ + if (topicStore.sizeOfState() >= topicConfig.maxNumber()) { + throw new HandleException(MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); + } + + /* Validate the topic memo */ + handleContext.attributeValidator().validateMemo(op.memo()); + builder.memo(op.memo()); + } + @NonNull @Override public Fees calculateFees(@NonNull final FeeContext feeContext) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java new file mode 100644 index 000000000000..469e828661bf --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.validators; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON; +import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEE_NOT_FULLY_SPECIFIED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CUSTOM_FEE_COLLECTOR; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID_IN_CUSTOM_FEES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR; +import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenType; +import com.hedera.hapi.node.transaction.ConsensusCustomFee; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.ReadableTokenRelationStore; +import com.hedera.node.app.service.token.ReadableTokenStore; +import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; +import com.hedera.node.app.spi.validation.ExpiryValidator; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; +import javax.inject.Inject; + +public class ConsensusCustomFeesValidator { + /** + * Constructs a {@link ConsensusCustomFeesValidator} instance. + */ + @Inject + public ConsensusCustomFeesValidator() { + // Needed for Dagger injection + } + + /** + * Validates custom fees for {@code TokenCreate} operation.This returns list of custom + * fees that need to be auto associated with the collector account. This is required + * for fixed fees with denominating token id set to sentinel value of 0.0.0. + * NOTE: This logic is subject to change in future PR for TokenCreate + * + * @param accountStore The account store. + * @param tokenRelationStore The token relation store. + * @param tokenStore The token store. + * @param customFees The custom fees to validate. + * @param expiryValidator The expiry validator to use (for fee collector accounts) + * @return The set of custom fees that need to auto associate collector accounts + */ + public void validateForCreation( + @NonNull final ReadableAccountStore accountStore, + @NonNull final ReadableTokenRelationStore tokenRelationStore, + @NonNull final ReadableTokenStore tokenStore, + @NonNull final List customFees, + @NonNull final ExpiryValidator expiryValidator) { + requireNonNull(accountStore); + requireNonNull(tokenRelationStore); + requireNonNull(tokenStore); + requireNonNull(customFees); + requireNonNull(expiryValidator); + + final List fees = new ArrayList<>(); + for (final var fee : customFees) { + // Validate the fee collector account is in a usable state + TokenHandlerHelper.getIfUsableForAliasedId( + fee.feeCollectorAccountIdOrElse(AccountID.DEFAULT), + accountStore, + expiryValidator, + INVALID_CUSTOM_FEE_COLLECTOR); + + final var isSpecified = fee.hasFixedFee(); + validateTrue(isSpecified, CUSTOM_FEE_NOT_FULLY_SPECIFIED); + validateFixedFeeForCreation(fee, tokenRelationStore, tokenStore); + } + } + + private void validateFixedFeeForCreation( + @NonNull final ConsensusCustomFee fee, + @NonNull final ReadableTokenRelationStore tokenRelationStore, + @NonNull final ReadableTokenStore tokenStore) { + final var fixedFee = fee.fixedFeeOrThrow(); + validateTrue(fixedFee.amount() > 0, CUSTOM_FEE_MUST_BE_POSITIVE); + if (fixedFee.hasDenominatingTokenId()) { + validateExplicitTokenDenomination( + fee.feeCollectorAccountId(), fixedFee.denominatingTokenId(), tokenRelationStore, tokenStore); + } + } + + /** + * Validate explicitly set token denomination for custom fees. + * @param feeCollectorNum The fee collector account number. + * @param tokenNum The token number used for token denomination. + * @param tokenRelationStore The token relation store. + * @param tokenStore The token store. + */ + private void validateExplicitTokenDenomination( + @NonNull final AccountID feeCollectorNum, + @NonNull final TokenID tokenNum, + @NonNull final ReadableTokenRelationStore tokenRelationStore, + @NonNull final ReadableTokenStore tokenStore) { + final var denomToken = tokenStore.get(tokenNum); + validateTrue(denomToken != null, INVALID_TOKEN_ID_IN_CUSTOM_FEES); + validateFalse(denomToken.paused(), INVALID_TOKEN_ID_IN_CUSTOM_FEES); + validateTrue(isFungibleCommon(denomToken.tokenType()), CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON); + validateTrue(tokenRelationStore.get(feeCollectorNum, tokenNum) != null, TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR); + } + + /** + * Validates that the given token type is fungible common. + * @param tokenType The token type to validate. + * @return {@code true} if the token type is fungible common, otherwise {@code false} + */ + private boolean isFungibleCommon(@NonNull final TokenType tokenType) { + return tokenType.equals(TokenType.FUNGIBLE_COMMON); + } +} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java index 60c4db706a28..bda3380fca53 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java @@ -2,6 +2,8 @@ module com.hedera.node.app.service.consensus.impl { requires transitive com.hedera.node.app.service.consensus; + requires transitive com.hedera.node.app.service.token.impl; + requires transitive com.hedera.node.app.service.token; requires transitive com.hedera.node.app.spi; requires transitive com.hedera.node.hapi; requires transitive com.swirlds.config.api; @@ -24,4 +26,5 @@ exports com.hedera.node.app.service.consensus.impl.handlers; exports com.hedera.node.app.service.consensus.impl.records; exports com.hedera.node.app.service.consensus.impl.schemas; + exports com.hedera.node.app.service.consensus.impl.validators; } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java index c4bc746a4f4f..409f639c8bed 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java @@ -46,6 +46,7 @@ import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.records.ConsensusCreateTopicStreamBuilder; +import com.hedera.node.app.service.consensus.impl.validators.ConsensusCustomFeesValidator; import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; import com.hedera.node.app.spi.ids.EntityNumGenerator; @@ -117,7 +118,7 @@ private TransactionBody newCreateTxn(Key adminKey, Key submitKey, boolean hasAut @BeforeEach void setUp() { - subject = new ConsensusCreateTopicHandler(); + subject = new ConsensusCreateTopicHandler(new ConsensusCustomFeesValidator()); config = HederaTestConfigBuilder.create() .withValue("topics.maxNumber", 10L) .getOrCreateConfig(); diff --git a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java index a48b6b7bfa5b..d4c4b479bcf2 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java @@ -36,7 +36,8 @@ exports com.hedera.node.app.service.token.impl.validators; exports com.hedera.node.app.service.token.impl.util to com.hedera.node.app, - com.hedera.node.app.service.token.impl.test; + com.hedera.node.app.service.token.impl.test, + com.hedera.node.app.service.consensus.impl; exports com.hedera.node.app.service.token.impl.handlers.staking to com.hedera.node.app, com.hedera.node.app.service.token.impl.test; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java index dd977e39ba4b..8c936a106ee9 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java @@ -22,6 +22,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnUtils.netOf; import static com.hederahashgraph.api.proto.java.HederaFunctionality.ConsensusCreateTopic; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static java.util.stream.Collectors.toList; import com.google.common.base.MoreObjects; import com.google.protobuf.ByteString; @@ -31,6 +32,7 @@ import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.transactions.HapiTxnOp; import com.hederahashgraph.api.proto.java.ConsensusCreateTopicTransactionBody; +import com.hederahashgraph.api.proto.java.ConsensusCustomFee; import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.Transaction; @@ -41,6 +43,7 @@ import java.util.OptionalLong; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Stream; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -58,6 +61,12 @@ public class HapiTopicCreate extends HapiTxnOp { private Optional autoRenewAccountId = Optional.empty(); private Optional adminKeyShape = Optional.empty(); private Optional submitKeyShape = Optional.empty(); + private Optional feeScheduleKey = Optional.empty(); + private Optional feeScheduleKeyName = Optional.empty(); + private Optional feeScheduleKeyShape = Optional.empty(); + private final List> feeScheduleSuppliers = new ArrayList<>(); + private Optional>> freeMesssagesKeyNamesList = Optional.empty(); + private Optional> freeMesssageKeyList = Optional.empty(); /** For some test we need the capability to build transaction has no autoRenewPeiord */ private boolean clearAutoRenewPeriod = false; @@ -86,6 +95,23 @@ public HapiTopicCreate submitKeyName(final String s) { return this; } + public HapiTopicCreate feeScheduleKeyName(final String s) { + feeScheduleKeyName = Optional.of(s); + return this; + } + + public HapiTopicCreate freeMessagesKeys(String... keys) { + freeMesssagesKeyNamesList = Optional.of(Stream.of(keys) + .>map(k -> spec -> spec.registry().getKey(k)) + .collect(toList())); + return self(); + } + + public HapiTopicCreate withConsensusCustomFee(final Function supplier) { + feeScheduleSuppliers.add(supplier); + return this; + } + public HapiTopicCreate adminKeyShape(final KeyShape shape) { adminKeyShape = Optional.of(shape); return this; @@ -143,6 +169,14 @@ protected Consumer opBodyDef(final HapiSpec spec) throw submitKey.ifPresent(b::setSubmitKey); autoRenewAccountId.ifPresent(id -> b.setAutoRenewAccount(asId(id, spec))); autoRenewPeriod.ifPresent(secs -> b.setAutoRenewPeriod(asDuration(secs))); + feeScheduleKey.ifPresent(b::setFeeScheduleKey); + freeMesssageKeyList.ifPresent(keys -> keys.forEach(b::addFreeMessagesKeyList)); + if (!feeScheduleSuppliers.isEmpty()) { + for (final var supplier : feeScheduleSuppliers) { + b.addCustomFees(supplier.apply(spec)); + } + } + // todo add custom fee if (clearAutoRenewPeriod) { b.clearAutoRenewPeriod(); } @@ -158,6 +192,15 @@ private void genKeysFor(final HapiSpec spec) { if (submitKeyName.isPresent() || submitKeyShape.isPresent()) { submitKey = Optional.of(netOf(spec, submitKeyName, submitKeyShape)); } + + if (feeScheduleKeyName.isPresent() || feeScheduleKeyShape.isPresent()) { + feeScheduleKey = Optional.of(netOf(spec, feeScheduleKeyName, feeScheduleKeyShape)); + } + + freeMesssagesKeyNamesList.ifPresent(functions -> freeMesssageKeyList = Optional.of(functions.stream() + .map(f -> f.apply(spec)) + .filter(k -> k != null && k != Key.getDefaultInstance()) + .collect(toList()))); } @Override diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java index ceba5d2af2be..f538180060d2 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java @@ -225,4 +225,34 @@ static ConsensusCustomFee.Builder baseFixedTopicBuilder(long amount, String coll final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); return ConsensusCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collectorId); } + + // consensus custom fee suppliers + public static Function fixedConsensusHbarFee(long amount, String collector) { + return spec -> builtConsensusFixedHbar(amount, collector, spec); + } + + public static Function fixedConsensusHtsFee( + long amount, String denom, String collector) { + return spec -> builtConsensusFixedHts(amount, denom, collector, spec); + } + + // builders + static ConsensusCustomFee builtConsensusFixedHbar(long amount, String collector, HapiSpec spec) { + return baseFixedTopicBuilder(amount, collector, spec).build(); + } + + static ConsensusCustomFee builtConsensusFixedHts(long amount, String denom, String collector, HapiSpec spec) { + final var builder = baseFixedTopicBuilder(amount, collector, spec); + final var denomId = + isIdLiteral(denom) ? asToken(denom) : spec.registry().getTokenID(denom); + builder.getFixedFeeBuilder().setDenominatingTokenId(denomId); + return builder.build(); + } + + // static ConsensusCustomFee.Builder baseFixedTopicBuilder(long amount, String collector, HapiSpec spec) { + // final var collectorId = + // isIdLiteral(collector) ? asAccount(collector) : spec.registry().getAccountID(collector); + // final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); + // return ConsensusCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collectorId); + // } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java new file mode 100644 index 000000000000..10cf5c8d3f8c --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.suites.hip991; + +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.fixedTopicHbarFee; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; + +import com.hedera.services.bdd.junit.HapiTest; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; + +public class TopicCustomFeeTest { + + @HapiTest + @DisplayName("Create topic with all keys") + final Stream createTopicWithAllKeys() { + final var adminKey = "adminKey"; + final var submitKey = "submitKey"; + final var feeScheduleKey = "feeScheduleKey"; + return hapiTest( + newKeyNamed(adminKey), + newKeyNamed(submitKey), + newKeyNamed(feeScheduleKey), + newKeyNamed("firstFMK"), + newKeyNamed("secondFMK"), + newKeyNamed("thirdFMK"), + newKeyNamed("fourthFMK"), + cryptoCreate("collector"), + createTopic("testTopic") + .adminKeyName(adminKey) + .submitKeyName(submitKey) + .feeScheduleKeyName(feeScheduleKey) + .freeMessagesKeys("firstFMK", "secondFMK", "thirdFMK"), + getTopicInfo("testTopic") + .hasAdminKey(adminKey) + .hasSubmitKey(submitKey) + .hasFeeScheduleKey(feeScheduleKey) + .hasFreeMessagesKeys(List.of("firstFMK", "secondFMK", "thirdFMK"))); + } + + @HapiTest + @DisplayName("Create topic with submitKey and feeScheduleKey") + final Stream createTopicWithSubmitKeyAndFeeScheduleKey() { + final var submitKey = "submitKey"; + final var feeScheduleKey = "feeScheduleKey"; + return hapiTest( + newKeyNamed(submitKey), + newKeyNamed(feeScheduleKey), + createTopic("testTopic").submitKeyName(submitKey).feeScheduleKeyName(feeScheduleKey), + getTopicInfo("testTopic").hasSubmitKey(submitKey).hasFeeScheduleKey(feeScheduleKey)); + } + + @HapiTest + @DisplayName("Create topic with only feeScheduleKey") + final Stream createTopicWithOnlyFeeScheduleKey() { + final var feeScheduleKey = "feeScheduleKey"; + return hapiTest( + newKeyNamed(feeScheduleKey), + createTopic("testTopic").feeScheduleKeyName(feeScheduleKey), + getTopicInfo("testTopic").hasFeeScheduleKey(feeScheduleKey)); + } + + @HapiTest + @DisplayName("Create topic with 1 Hbar fixed fee") + final Stream createTopicWithOneHbarFixedFee() { + final var adminKey = "adminKey"; + final var submitKey = "submitKey"; + final var feeScheduleKey = "feeScheduleKey"; + final var collector = "collector"; + return hapiTest( + newKeyNamed(adminKey), + newKeyNamed(submitKey), + newKeyNamed(feeScheduleKey), + cryptoCreate(collector), + createTopic("testTopic") + .adminKeyName(adminKey) + .submitKeyName(submitKey) + .feeScheduleKeyName(feeScheduleKey) + .withConsensusCustomFee(fixedConsensusHbarFee(1, collector)), + // todo check if we need to sign with feeScheduleKey on create? + getTopicInfo("testTopic") + .hasAdminKey(adminKey) + .hasSubmitKey(submitKey) + .hasFeeScheduleKey(feeScheduleKey) + .hasCustom(fixedTopicHbarFee(1, collector))); + } + + // todo add test get info of deleted or expired-? +} From 773cbcd8e793d881ba929e897a1dac907df1d25f Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Sep 2024 12:23:52 +0300 Subject: [PATCH 05/94] Refactor Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 11 ++++++ .../handlers/ConsensusCreateTopicHandler.java | 34 +++++++------------ .../ConsensusCustomFeesValidator.java | 14 +++----- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index fe3da22d3a41..d8c5a0cfe22d 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1594,7 +1594,18 @@ enum ResponseCodeEnum { */ INVALID_TOKEN_IN_PENDING_AIRDROP = 369; + /** + * The number of entries in the free messages key list exceed the maximum. + */ MAX_ENTRIES_FOR_FMKL_EXCEEDED = 370; + + /** + * There is a duplicate key in the free messages key list. + */ FMKL_CONTAINS_DUPLICATED_KEYS = 371; + + /** + * There is an invalid key in the free messages key list. + */ INVALID_KEY_IN_FMKL = 372; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index e298447ef977..bf53b51f719c 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -20,11 +20,11 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTORENEW_DURATION_NOT_IN_RANGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.BAD_ENCODING; import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEES_LIST_TOO_LONG; +import static com.hedera.hapi.node.base.ResponseCodeEnum.FMKL_CONTAINS_DUPLICATED_KEYS; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_EXPIRATION_TIME; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FMKL; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FMKL_EXCEEDED; import static com.hedera.node.app.hapi.utils.fee.ConsensusServiceFeeBuilder.getConsensusCreateTopicFee; @@ -50,7 +50,6 @@ import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.service.token.ReadableTokenRelationStore; import com.hedera.node.app.service.token.ReadableTokenStore; -// import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.validation.ExpiryMeta; @@ -87,7 +86,7 @@ public ConsensusCreateTopicHandler(@NonNull final ConsensusCustomFeesValidator c public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { final var op = txn.consensusCreateTopicOrThrow(); final var uniqueKeysCount = op.freeMessagesKeyList().stream().distinct().count(); - validateTruePreCheck(uniqueKeysCount == op.freeMessagesKeyList().size(), INVALID_TRANSACTION_BODY); + validateTruePreCheck(uniqueKeysCount == op.freeMessagesKeyList().size(), FMKL_CONTAINS_DUPLICATED_KEYS); } @Override @@ -183,19 +182,7 @@ private void validateSemantics( final var tokenStore = handleContext.storeFactory().readableStore(ReadableTokenStore.class); final var tokenRelStore = handleContext.storeFactory().readableStore(ReadableTokenRelationStore.class); - // validate max size of lists in the transaction body - if (!op.freeMessagesKeyList().isEmpty()) { - validateTrue( - op.freeMessagesKeyList().size() <= topicConfig.maxEntriesForFreeMessagesKeyList(), - MAX_ENTRIES_FOR_FMKL_EXCEEDED); - } - - if (!op.customFees().isEmpty()) { - validateTrue( - op.customFees().size() <= topicConfig.maxCustoFeeEntriesForTopics(), CUSTOM_FEES_LIST_TOO_LONG); - } - - /* Validate admin and submit keys and set them. Empty key list is allowed and is used for immutable entities */ + // Validate admin and submit keys and set them. Empty key list is allowed and is used for immutable entities if (op.hasAdminKey() && !isImmutableKey(op.adminKey())) { handleContext.attributeValidator().validateKey(op.adminKey()); builder.adminKey(op.adminKey()); @@ -207,14 +194,17 @@ private void validateSemantics( builder.submitKey(op.submitKey()); } - // validate keys + // validate hasFeeScheduleKey() if (op.hasFeeScheduleKey()) { handleContext.attributeValidator().validateKey(op.feeScheduleKey(), INVALID_CUSTOM_FEE_SCHEDULE_KEY); builder.feeScheduleKey(op.feeScheduleKey()); } - // validate keys + // validate size of the list and the keys if (!op.freeMessagesKeyList().isEmpty()) { + validateTrue( + op.freeMessagesKeyList().size() <= topicConfig.maxEntriesForFreeMessagesKeyList(), + MAX_ENTRIES_FOR_FMKL_EXCEEDED); op.freeMessagesKeyList() .forEach(key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FMKL)); builder.freeMessagesKeyList(op.freeMessagesKeyList()); @@ -222,16 +212,16 @@ private void validateSemantics( // validate custom fees if (!op.customFees().isEmpty()) { - // todo check if token is frozen to fee collector in token create handler + validateTrue( + op.customFees().size() <= topicConfig.maxCustoFeeEntriesForTopics(), CUSTOM_FEES_LIST_TOO_LONG); customFeesValidator.validateForCreation( accountStore, tokenRelStore, tokenStore, op.customFees(), handleContext.expiryValidator()); builder.customFees(op.customFees()); } /* Validate if the current topic can be created */ - if (topicStore.sizeOfState() >= topicConfig.maxNumber()) { - throw new HandleException(MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); - } + validateTrue( + topicStore.sizeOfState() < topicConfig.maxNumber(), MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); /* Validate the topic memo */ handleContext.attributeValidator().validateMemo(op.memo()); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java index 469e828661bf..8837ad820739 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java @@ -22,6 +22,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CUSTOM_FEE_COLLECTOR; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID_IN_CUSTOM_FEES; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR; +import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsableForAliasedId; import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -33,10 +34,8 @@ import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.service.token.ReadableTokenRelationStore; import com.hedera.node.app.service.token.ReadableTokenStore; -import com.hedera.node.app.service.token.impl.util.TokenHandlerHelper; import com.hedera.node.app.spi.validation.ExpiryValidator; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.ArrayList; import java.util.List; import javax.inject.Inject; @@ -50,17 +49,13 @@ public ConsensusCustomFeesValidator() { } /** - * Validates custom fees for {@code TokenCreate} operation.This returns list of custom - * fees that need to be auto associated with the collector account. This is required - * for fixed fees with denominating token id set to sentinel value of 0.0.0. - * NOTE: This logic is subject to change in future PR for TokenCreate + * Validates custom fees for {@code ConsensusCreateTopic} operation. * * @param accountStore The account store. * @param tokenRelationStore The token relation store. * @param tokenStore The token store. * @param customFees The custom fees to validate. * @param expiryValidator The expiry validator to use (for fee collector accounts) - * @return The set of custom fees that need to auto associate collector accounts */ public void validateForCreation( @NonNull final ReadableAccountStore accountStore, @@ -74,10 +69,9 @@ public void validateForCreation( requireNonNull(customFees); requireNonNull(expiryValidator); - final List fees = new ArrayList<>(); for (final var fee : customFees) { // Validate the fee collector account is in a usable state - TokenHandlerHelper.getIfUsableForAliasedId( + getIfUsableForAliasedId( fee.feeCollectorAccountIdOrElse(AccountID.DEFAULT), accountStore, expiryValidator, @@ -103,6 +97,7 @@ private void validateFixedFeeForCreation( /** * Validate explicitly set token denomination for custom fees. + * * @param feeCollectorNum The fee collector account number. * @param tokenNum The token number used for token denomination. * @param tokenRelationStore The token relation store. @@ -122,6 +117,7 @@ private void validateExplicitTokenDenomination( /** * Validates that the given token type is fungible common. + * * @param tokenType The token type to validate. * @return {@code true} if the token type is fungible common, otherwise {@code false} */ From e67f50ba668a668ccdc2006b0ea9ebe49e217c33 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Sep 2024 12:54:26 +0300 Subject: [PATCH 06/94] consensus custom fee builders and asserts for hapi tests Signed-off-by: Zhivko Kelchev --- .../consensus/HapiTopicCreate.java | 1 - .../transactions/token/CustomFeeSpecs.java | 17 ++------- .../transactions/token/CustomFeeTests.java | 13 +++++-- .../bdd/suites/hip991/TopicCustomFeeTest.java | 35 +++++++++++++++++-- 4 files changed, 46 insertions(+), 20 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java index 8c936a106ee9..76dd711d3e0a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java @@ -176,7 +176,6 @@ protected Consumer opBodyDef(final HapiSpec spec) throw b.addCustomFees(supplier.apply(spec)); } } - // todo add custom fee if (clearAutoRenewPeriod) { b.clearAutoRenewPeriod(); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java index f538180060d2..dad2ca3005fd 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java @@ -127,10 +127,6 @@ static CustomFee builtFixedHbar(long amount, String collector, boolean allCollec return baseFixedBuilder(amount, collector, allCollectorsExempt, spec).build(); } - static ConsensusCustomFee builtFixedTopicHbar(long amount, String collector, HapiSpec spec) { - return baseFixedTopicBuilder(amount, collector, spec).build(); - } - static FixedFee builtFixedHbarSansCollector(long amount) { return FixedFee.newBuilder().setAmount(amount).build(); } @@ -219,7 +215,7 @@ static CustomFee.Builder baseFixedBuilder( .setFeeCollectorAccountId(collectorId); } - static ConsensusCustomFee.Builder baseFixedTopicBuilder(long amount, String collector, HapiSpec spec) { + static ConsensusCustomFee.Builder baseConsensusFixedBuilder(long amount, String collector, HapiSpec spec) { final var collectorId = isIdLiteral(collector) ? asAccount(collector) : spec.registry().getAccountID(collector); final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); @@ -238,21 +234,14 @@ public static Function fixedConsensusHtsFee( // builders static ConsensusCustomFee builtConsensusFixedHbar(long amount, String collector, HapiSpec spec) { - return baseFixedTopicBuilder(amount, collector, spec).build(); + return baseConsensusFixedBuilder(amount, collector, spec).build(); } static ConsensusCustomFee builtConsensusFixedHts(long amount, String denom, String collector, HapiSpec spec) { - final var builder = baseFixedTopicBuilder(amount, collector, spec); + final var builder = baseConsensusFixedBuilder(amount, collector, spec); final var denomId = isIdLiteral(denom) ? asToken(denom) : spec.registry().getTokenID(denom); builder.getFixedFeeBuilder().setDenominatingTokenId(denomId); return builder.build(); } - - // static ConsensusCustomFee.Builder baseFixedTopicBuilder(long amount, String collector, HapiSpec spec) { - // final var collectorId = - // isIdLiteral(collector) ? asAccount(collector) : spec.registry().getAccountID(collector); - // final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); - // return ConsensusCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collectorId); - // } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java index 41da013e3cc5..8a352c50345b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java @@ -92,13 +92,22 @@ public static BiConsumer> royaltyFeeWithFallbackInToke }; } - public static BiConsumer> fixedTopicHbarFee(long amount, String collector) { + public static BiConsumer> expectedConsensusFixedHbarFee( + long amount, String collector) { return (spec, actual) -> { - final var expected = CustomFeeSpecs.builtFixedTopicHbar(amount, collector, spec); + final var expected = CustomFeeSpecs.builtConsensusFixedHbar(amount, collector, spec); failUnlessConsensusFeePresent("fixed ℏ", actual, expected); }; } + public static BiConsumer> expectedConsensusFixedHTSFee( + long amount, String token, String collector) { + return (spec, actual) -> { + final var expected = CustomFeeSpecs.builtConsensusFixedHts(amount, token, collector, spec); + failUnlessConsensusFeePresent("fixed hts", actual, expected); + }; + } + private static void failUnlessPresent(String detail, List actual, CustomFee expected) { for (var customFee : actual) { if (expected.equals(customFee)) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 10cf5c8d3f8c..f7ec968bc0a5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -20,11 +20,16 @@ import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.fixedTopicHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import com.hedera.services.bdd.junit.HapiTest; +import com.hederahashgraph.api.proto.java.TokenType; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.DisplayName; @@ -103,8 +108,32 @@ final Stream createTopicWithOneHbarFixedFee() { .hasAdminKey(adminKey) .hasSubmitKey(submitKey) .hasFeeScheduleKey(feeScheduleKey) - .hasCustom(fixedTopicHbarFee(1, collector))); + .hasCustom(expectedConsensusFixedHbarFee(1, collector))); } - // todo add test get info of deleted or expired-? + @HapiTest + @DisplayName("Create topic with 1 HTS fixed fee") + final Stream createTopicWithOneHTSFixedFee() { + final var adminKey = "adminKey"; + final var submitKey = "submitKey"; + final var feeScheduleKey = "feeScheduleKey"; + final var collector = "collector"; + return hapiTest( + newKeyNamed(adminKey), + newKeyNamed(submitKey), + newKeyNamed(feeScheduleKey), + cryptoCreate(collector), + tokenCreate("testToken").tokenType(TokenType.FUNGIBLE_COMMON).initialSupply(500), + tokenAssociate(collector, "testToken"), + createTopic("testTopic") + .adminKeyName(adminKey) + .submitKeyName(submitKey) + .feeScheduleKeyName(feeScheduleKey) + .withConsensusCustomFee(fixedConsensusHtsFee(1, "testToken", collector)), + getTopicInfo("testTopic") + .hasAdminKey(adminKey) + .hasSubmitKey(submitKey) + .hasFeeScheduleKey(feeScheduleKey) + .hasCustom(expectedConsensusFixedHTSFee(1, "testToken", collector))); + } } From b9c05bdb41e1c65510e2e627e386e741dae20602 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Sep 2024 14:00:28 +0300 Subject: [PATCH 07/94] Topic create hapi tests Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 50 +++++ .../bdd/suites/hip991/TopicCustomFeeTest.java | 189 +++++++++--------- 2 files changed, 143 insertions(+), 96 deletions(-) create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java new file mode 100644 index 000000000000..26d6125cf50d --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.suites.hip991; + +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; + +import com.hedera.services.bdd.spec.SpecOperation; +import java.util.ArrayList; + +public class TopicCustomFeeBase { + protected static final String TOPIC = "topic"; + protected static final String ADMIN_KEY = "adminKey"; + protected static final String SUBMIT_KEY = "submitKey"; + protected static final String FEE_SCHEDULE_KEY = "feeScheduleKey"; + protected static final String FREE_MSG_KEY_PREFIX = "freeMessageKey_"; + + protected static SpecOperation[] setupBaseKeys() { + return new SpecOperation[] {newKeyNamed(ADMIN_KEY), newKeyNamed(SUBMIT_KEY), newKeyNamed(FEE_SCHEDULE_KEY)}; + } + + protected static SpecOperation[] newNamedKeysForFMKL(int count) { + final var list = new ArrayList(); + for (int i = 0; i < count; i++) { + list.add(newKeyNamed(FREE_MSG_KEY_PREFIX + i)); + } + return list.toArray(new SpecOperation[0]); + } + + protected static String[] freeMsgKeyNames(int count) { + final var list = new ArrayList(); + for (int i = 0; i < count; i++) { + list.add(FREE_MSG_KEY_PREFIX + i); + } + return list.toArray(new String[0]); + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index f7ec968bc0a5..9b4a9e003863 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -26,114 +26,111 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.suites.HapiSuite.flattened; import com.hedera.services.bdd.junit.HapiTest; +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.support.TestLifecycle; import com.hederahashgraph.api.proto.java.TokenType; +import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; -public class TopicCustomFeeTest { +@HapiTestLifecycle +@DisplayName("Topic custom fees") +public class TopicCustomFeeTest extends TopicCustomFeeBase { - @HapiTest - @DisplayName("Create topic with all keys") - final Stream createTopicWithAllKeys() { - final var adminKey = "adminKey"; - final var submitKey = "submitKey"; - final var feeScheduleKey = "feeScheduleKey"; - return hapiTest( - newKeyNamed(adminKey), - newKeyNamed(submitKey), - newKeyNamed(feeScheduleKey), - newKeyNamed("firstFMK"), - newKeyNamed("secondFMK"), - newKeyNamed("thirdFMK"), - newKeyNamed("fourthFMK"), - cryptoCreate("collector"), - createTopic("testTopic") - .adminKeyName(adminKey) - .submitKeyName(submitKey) - .feeScheduleKeyName(feeScheduleKey) - .freeMessagesKeys("firstFMK", "secondFMK", "thirdFMK"), - getTopicInfo("testTopic") - .hasAdminKey(adminKey) - .hasSubmitKey(submitKey) - .hasFeeScheduleKey(feeScheduleKey) - .hasFreeMessagesKeys(List.of("firstFMK", "secondFMK", "thirdFMK"))); - } + @Nested + @DisplayName("Topic create") + class TopicCreate { - @HapiTest - @DisplayName("Create topic with submitKey and feeScheduleKey") - final Stream createTopicWithSubmitKeyAndFeeScheduleKey() { - final var submitKey = "submitKey"; - final var feeScheduleKey = "feeScheduleKey"; - return hapiTest( - newKeyNamed(submitKey), - newKeyNamed(feeScheduleKey), - createTopic("testTopic").submitKeyName(submitKey).feeScheduleKeyName(feeScheduleKey), - getTopicInfo("testTopic").hasSubmitKey(submitKey).hasFeeScheduleKey(feeScheduleKey)); - } + @Nested + @DisplayName("Positive scenarios") + class TopicCreatePositiveScenarios { - @HapiTest - @DisplayName("Create topic with only feeScheduleKey") - final Stream createTopicWithOnlyFeeScheduleKey() { - final var feeScheduleKey = "feeScheduleKey"; - return hapiTest( - newKeyNamed(feeScheduleKey), - createTopic("testTopic").feeScheduleKeyName(feeScheduleKey), - getTopicInfo("testTopic").hasFeeScheduleKey(feeScheduleKey)); - } + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } - @HapiTest - @DisplayName("Create topic with 1 Hbar fixed fee") - final Stream createTopicWithOneHbarFixedFee() { - final var adminKey = "adminKey"; - final var submitKey = "submitKey"; - final var feeScheduleKey = "feeScheduleKey"; - final var collector = "collector"; - return hapiTest( - newKeyNamed(adminKey), - newKeyNamed(submitKey), - newKeyNamed(feeScheduleKey), - cryptoCreate(collector), - createTopic("testTopic") - .adminKeyName(adminKey) - .submitKeyName(submitKey) - .feeScheduleKeyName(feeScheduleKey) - .withConsensusCustomFee(fixedConsensusHbarFee(1, collector)), - // todo check if we need to sign with feeScheduleKey on create? - getTopicInfo("testTopic") - .hasAdminKey(adminKey) - .hasSubmitKey(submitKey) - .hasFeeScheduleKey(feeScheduleKey) - .hasCustom(expectedConsensusFixedHbarFee(1, collector))); - } + @HapiTest + @DisplayName("Create topic with all keys") + final Stream createTopicWithAllKeys() { + return hapiTest(flattened( + newNamedKeysForFMKL(5), + cryptoCreate("collector"), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .freeMessagesKeys(freeMsgKeyNames(5)), + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY) + .hasFreeMessagesKeys(List.of(freeMsgKeyNames(5))))); + } + + @HapiTest + @DisplayName("Create topic with submitKey and feeScheduleKey") + final Stream createTopicWithSubmitKeyAndFeeScheduleKey() { + return hapiTest( + createTopic(TOPIC).submitKeyName(SUBMIT_KEY).feeScheduleKeyName(FEE_SCHEDULE_KEY), + getTopicInfo(TOPIC).hasSubmitKey(SUBMIT_KEY).hasFeeScheduleKey(FEE_SCHEDULE_KEY)); + } + + @HapiTest + @DisplayName("Create topic with only feeScheduleKey") + final Stream createTopicWithOnlyFeeScheduleKey() { + return hapiTest( + createTopic(TOPIC).feeScheduleKeyName(FEE_SCHEDULE_KEY), + getTopicInfo(TOPIC).hasFeeScheduleKey(FEE_SCHEDULE_KEY)); + } + + @HapiTest + @DisplayName("Create topic with 1 Hbar fixed fee") + final Stream createTopicWithOneHbarFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(1, collector)), + // todo check if we need to sign with feeScheduleKey on create? + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY) + .hasCustom(expectedConsensusFixedHbarFee(1, collector))); + } - @HapiTest - @DisplayName("Create topic with 1 HTS fixed fee") - final Stream createTopicWithOneHTSFixedFee() { - final var adminKey = "adminKey"; - final var submitKey = "submitKey"; - final var feeScheduleKey = "feeScheduleKey"; - final var collector = "collector"; - return hapiTest( - newKeyNamed(adminKey), - newKeyNamed(submitKey), - newKeyNamed(feeScheduleKey), - cryptoCreate(collector), - tokenCreate("testToken").tokenType(TokenType.FUNGIBLE_COMMON).initialSupply(500), - tokenAssociate(collector, "testToken"), - createTopic("testTopic") - .adminKeyName(adminKey) - .submitKeyName(submitKey) - .feeScheduleKeyName(feeScheduleKey) - .withConsensusCustomFee(fixedConsensusHtsFee(1, "testToken", collector)), - getTopicInfo("testTopic") - .hasAdminKey(adminKey) - .hasSubmitKey(submitKey) - .hasFeeScheduleKey(feeScheduleKey) - .hasCustom(expectedConsensusFixedHTSFee(1, "testToken", collector))); + @HapiTest + @DisplayName("Create topic with 1 HTS fixed fee") + final Stream createTopicWithOneHTSFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenCreate("testToken") + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, "testToken"), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHtsFee(1, "testToken", collector)), + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY) + .hasCustom(expectedConsensusFixedHTSFee(1, "testToken", collector))); + } + } } } From 960cae51409a2320eac65274635e88fbcb22bd2c Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Sep 2024 16:36:03 +0300 Subject: [PATCH 08/94] Fix checkModuleDirectivesScope error Signed-off-by: Zhivko Kelchev --- hedera-node/hedera-consensus-service-impl/build.gradle.kts | 1 - .../src/main/java/module-info.java | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/build.gradle.kts b/hedera-node/hedera-consensus-service-impl/build.gradle.kts index 67f55402d6f2..f70a7ac31704 100644 --- a/hedera-node/hedera-consensus-service-impl/build.gradle.kts +++ b/hedera-node/hedera-consensus-service-impl/build.gradle.kts @@ -26,7 +26,6 @@ mainModuleInfo { annotationProcessor("dagger.compiler") } testModuleInfo { requires("com.hedera.node.app.service.consensus.impl") requires("com.swirlds.state.api.test.fixtures") - requires("com.hedera.node.app.service.token") requires("com.hedera.node.app.spi.test.fixtures") requires("com.hedera.node.config.test.fixtures") requires("com.google.protobuf") diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java index bda3380fca53..5665b34dc129 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java @@ -2,7 +2,6 @@ module com.hedera.node.app.service.consensus.impl { requires transitive com.hedera.node.app.service.consensus; - requires transitive com.hedera.node.app.service.token.impl; requires transitive com.hedera.node.app.service.token; requires transitive com.hedera.node.app.spi; requires transitive com.hedera.node.hapi; @@ -13,6 +12,7 @@ requires transitive java.compiler; // javax.annotation.processing.Generated requires transitive javax.inject; requires com.hedera.node.app.hapi.utils; + requires com.hedera.node.app.service.token.impl; requires com.hedera.node.config; requires org.apache.logging.log4j; requires static com.github.spotbugs.annotations; From a97d4ca32fd8509ddaea1a6c2569b75a402bddc0 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Sep 2024 17:05:56 +0300 Subject: [PATCH 09/94] Use copyBuilder() Signed-off-by: Zhivko Kelchev --- .../impl/handlers/ConsensusUpdateTopicHandler.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java index a93ceed65dbd..9d4edac5188f 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java @@ -155,17 +155,7 @@ public void handle(@NonNull final HandleContext handleContext) { validateMaybeNewAttributes(handleContext, op, topic); // Now we apply the mutations to a builder - final var builder = new Topic.Builder(); - // But first copy over the immutable topic attributes to the builder - builder.topicId(topic.topicId()); - builder.sequenceNumber(topic.sequenceNumber()); - builder.runningHash(topic.runningHash()); - builder.deleted(topic.deleted()); - // TODO: added so unit tests pass (fix this when implementing the actual logic) - builder.feeScheduleKey(topic.feeScheduleKey()); - builder.freeMessagesKeyList(topic.freeMessagesKeyList()); - builder.customFees(topic.customFees()); - // And then resolve mutable attributes, and put the new topic back + final var builder = topic.copyBuilder(); resolveMutableBuilderAttributes(handleContext, op, builder, topic); topicStore.put(builder.build()); } From 80c29cbcb0034e8cc1384e4fe09a0b5a03384748 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Sep 2024 17:19:48 +0300 Subject: [PATCH 10/94] Fix indentation Signed-off-by: Zhivko Kelchev --- .../services/state/consensus/topic.proto | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 4414c8b47031..b9c1727f7eae 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -104,12 +104,12 @@ message Topic { Key submit_key = 10; /** - * Access control for update/delete of custom fees. - *

- * If this field is unset, the current custom fees CANNOT be changed.
- * If this field is set, that `Key` MUST sign any transaction to update - * the custom fee schedule for this topic. - */ + * Access control for update/delete of custom fees. + *

+ * If this field is unset, the current custom fees CANNOT be changed.
+ * If this field is set, that `Key` MUST sign any transaction to update + * the custom fee schedule for this topic. + */ Key fee_schedule_key = 11; /** From 54bf31d2d0f18eb8c1a2b5d6e6445f1f89db5174 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Wed, 11 Sep 2024 15:17:57 +0300 Subject: [PATCH 11/94] Topic create hapi tests Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 6 + .../bdd/suites/hip991/TopicCustomFeeTest.java | 136 ++++++++++++++++-- 2 files changed, 133 insertions(+), 9 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 26d6125cf50d..23db856c4fe3 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -18,7 +18,9 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import com.google.protobuf.ByteString; import com.hedera.services.bdd.spec.SpecOperation; +import com.hederahashgraph.api.proto.java.Key; import java.util.ArrayList; public class TopicCustomFeeBase { @@ -28,6 +30,10 @@ public class TopicCustomFeeBase { protected static final String FEE_SCHEDULE_KEY = "feeScheduleKey"; protected static final String FREE_MSG_KEY_PREFIX = "freeMessageKey_"; + // This key is truly invalid, as all Ed25519 public keys must be 32 bytes long + protected static final Key STRUCTURALLY_INVALID_KEY = + Key.newBuilder().setEd25519(ByteString.fromHex("ff")).build(); + protected static SpecOperation[] setupBaseKeys() { return new SpecOperation[] {newKeyNamed(ADMIN_KEY), newKeyNamed(SUBMIT_KEY), newKeyNamed(FEE_SCHEDULE_KEY)}; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 9b4a9e003863..abeba1ddccf8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -20,13 +20,21 @@ import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.HapiSuite.flattened; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FMKL_CONTAINS_DUPLICATED_KEYS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; @@ -59,24 +67,22 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { @HapiTest @DisplayName("Create topic with all keys") + // TOPIC_FEE_001 final Stream createTopicWithAllKeys() { return hapiTest(flattened( - newNamedKeysForFMKL(5), - cryptoCreate("collector"), createTopic(TOPIC) .adminKeyName(ADMIN_KEY) .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .freeMessagesKeys(freeMsgKeyNames(5)), + .feeScheduleKeyName(FEE_SCHEDULE_KEY), getTopicInfo(TOPIC) .hasAdminKey(ADMIN_KEY) .hasSubmitKey(SUBMIT_KEY) - .hasFeeScheduleKey(FEE_SCHEDULE_KEY) - .hasFreeMessagesKeys(List.of(freeMsgKeyNames(5))))); + .hasFeeScheduleKey(FEE_SCHEDULE_KEY))); } @HapiTest @DisplayName("Create topic with submitKey and feeScheduleKey") + // TOPIC_FEE_002 final Stream createTopicWithSubmitKeyAndFeeScheduleKey() { return hapiTest( createTopic(TOPIC).submitKeyName(SUBMIT_KEY).feeScheduleKeyName(FEE_SCHEDULE_KEY), @@ -85,6 +91,7 @@ final Stream createTopicWithSubmitKeyAndFeeScheduleKey() { @HapiTest @DisplayName("Create topic with only feeScheduleKey") + // TOPIC_FEE_003 final Stream createTopicWithOnlyFeeScheduleKey() { return hapiTest( createTopic(TOPIC).feeScheduleKeyName(FEE_SCHEDULE_KEY), @@ -93,6 +100,7 @@ final Stream createTopicWithOnlyFeeScheduleKey() { @HapiTest @DisplayName("Create topic with 1 Hbar fixed fee") + // TOPIC_FEE_004 final Stream createTopicWithOneHbarFixedFee() { final var collector = "collector"; return hapiTest( @@ -101,17 +109,17 @@ final Stream createTopicWithOneHbarFixedFee() { .adminKeyName(ADMIN_KEY) .submitKeyName(SUBMIT_KEY) .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .withConsensusCustomFee(fixedConsensusHbarFee(1, collector)), - // todo check if we need to sign with feeScheduleKey on create? + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), getTopicInfo(TOPIC) .hasAdminKey(ADMIN_KEY) .hasSubmitKey(SUBMIT_KEY) .hasFeeScheduleKey(FEE_SCHEDULE_KEY) - .hasCustom(expectedConsensusFixedHbarFee(1, collector))); + .hasCustom(expectedConsensusFixedHbarFee(ONE_HBAR, collector))); } @HapiTest @DisplayName("Create topic with 1 HTS fixed fee") + // TOPIC_FEE_005 final Stream createTopicWithOneHTSFixedFee() { final var collector = "collector"; return hapiTest( @@ -131,6 +139,116 @@ final Stream createTopicWithOneHTSFixedFee() { .hasFeeScheduleKey(FEE_SCHEDULE_KEY) .hasCustom(expectedConsensusFixedHTSFee(1, "testToken", collector))); } + + @HapiTest + @DisplayName("Create topic with all keys") + // TOPIC_FEE_020 + final Stream createTopicWithFMLK() { + return hapiTest(flattened( + // create 10 keys + newNamedKeysForFMKL(10), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + // set list of 10 keys + .freeMessagesKeys(freeMsgKeyNames(10)), + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY) + // assert the list + .hasFreeMessagesKeys(List.of(freeMsgKeyNames(10))))); + } + } + + @Nested + @DisplayName("Negative scenarios") + class TopicCreateNegativeScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } + + @HapiTest + @DisplayName("Create topic with duplicated signatures in FMKL") + // TOPIC_FEE_023 + final Stream createTopicWithDuplicateSignatures() { + final var testKey = "testKey"; + return hapiTest(flattened( + newKeyNamed(testKey), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .freeMessagesKeys(testKey, testKey) + .hasPrecheck(FMKL_CONTAINS_DUPLICATED_KEYS))); + } + + @HapiTest + @DisplayName("Create topic with 0 Hbar fixed fee") + // TOPIC_FEE_024 + final Stream createTopicWithZeroHbarFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(0, collector)) + .hasKnownStatus(CUSTOM_FEE_MUST_BE_POSITIVE)); + } + + @HapiTest + @DisplayName("Create topic with 0 HTS fixed fee") + // TOPIC_FEE_025 + final Stream createTopicWithZeroHTSFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenCreate("testToken") + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, "testToken"), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHtsFee(0, "testToken", collector)) + .hasKnownStatus(CUSTOM_FEE_MUST_BE_POSITIVE)); + } + + @HapiTest + @DisplayName("Create topic with invalid fee schedule key") + // TOPIC_FEE_026 + final Stream createTopicWithInvalidFeeScheduleKey() { + final var invalidKey = "invalidKey"; + return hapiTest( + withOpContext((spec, opLog) -> spec.registry().saveKey(invalidKey, STRUCTURALLY_INVALID_KEY)), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(invalidKey) + .hasKnownStatus(INVALID_CUSTOM_FEE_SCHEDULE_KEY)); + } + + @HapiTest + @DisplayName("Create topic with custom fee and deleted collector") + // TOPIC_FEE_028 + final Stream createTopicWithCustomFeeAndDeletedCollector() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + cryptoDelete(collector), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + .hasKnownStatus(ACCOUNT_DELETED)); + } } } } From 904afa81ea2f436d82928b2ef619298640408b8b Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Wed, 11 Sep 2024 17:53:27 +0300 Subject: [PATCH 12/94] Update protobuf messages Signed-off-by: Zhivko Kelchev --- .../services/consensus_create_topic.proto | 4 +- .../services/consensus_topic_info.proto | 4 +- .../services/consensus_update_topic.proto | 19 ++++++--- .../services/custom_fees.proto | 42 +++++++++++++++---- .../services/state/consensus/topic.proto | 4 +- .../ConsensusGetTopicInfoHandler.java | 2 +- .../handlers/ConsensusGetTopicInfoTest.java | 2 +- .../queries/consensus/HapiGetTopicInfo.java | 2 +- 8 files changed, 57 insertions(+), 22 deletions(-) diff --git a/hapi/hedera-protobufs/services/consensus_create_topic.proto b/hapi/hedera-protobufs/services/consensus_create_topic.proto index 6a6aa6bc780c..5e55d5570a82 100644 --- a/hapi/hedera-protobufs/services/consensus_create_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_create_topic.proto @@ -88,10 +88,10 @@ message ConsensusCreateTopicTransactionBody { * If a submit transaction is signed by _any_ key included in this set, * custom fees SHALL NOT be charged for that transaction. *

- * If free_messages_key_list is unset, the following keys are exempt + * If fee_exempt_key_list is unset, the following keys are exempt * from custom fees: adminKey, submitKey, fee_schedule_key. */ - repeated Key free_messages_key_list = 9; + repeated Key fee_exempt_key_list = 9; /** * A set of custom fee definitions.
diff --git a/hapi/hedera-protobufs/services/consensus_topic_info.proto b/hapi/hedera-protobufs/services/consensus_topic_info.proto index 9a893d143de3..00d1735d3a68 100644 --- a/hapi/hedera-protobufs/services/consensus_topic_info.proto +++ b/hapi/hedera-protobufs/services/consensus_topic_info.proto @@ -105,10 +105,10 @@ message ConsensusTopicInfo { * If a submit transaction is signed by _any_ key included in this set, * custom fees SHALL NOT be charged for that transaction. *

- * If free_messages_key_list is unset, the following keys are exempt + * If fee_exempt_key_list is unset, the following keys are exempt * from custom fees: adminKey, submitKey, fee_schedule_key. */ - repeated Key free_messages_key_list = 11; + repeated Key fee_exempt_key_list = 11; /** * A set of custom fee definitions.
diff --git a/hapi/hedera-protobufs/services/consensus_update_topic.proto b/hapi/hedera-protobufs/services/consensus_update_topic.proto index d3a9ca778cad..97f646d9367a 100644 --- a/hapi/hedera-protobufs/services/consensus_update_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_update_topic.proto @@ -100,18 +100,25 @@ message ConsensusUpdateTopicTransactionBody { Key fee_schedule_key = 10; /** - * A set of keys that are allowed to submit messages to the topic without + * A wrapper of a set of keys that are allowed to submit messages to the topic without * paying the topic's custom fees. *

- * If this list is empty, the current set of keys is unchanged. + * If the wrapper is not set, the current set of keys is unchanged.
+ * If the wrapper is set, but contains an empty list, the list will be updated to an empty list. */ - repeated Key free_messages_key_list = 11; + FeeExemptKeyList fee_exempt_key_list = 11; /** - * A set of custom fee definitions.
+ * A wrapper of a set of custom fee definitions.
* These are fees to be assessed for each submit to this topic. *

- * If this list is empty, the current set of fees is unchanged. + * If the wrapper is not set, the current set of keys is unchanged.
+ * If the wrapper is set, but contains an empty list, the list will be updated to an empty list. */ - repeated ConsensusCustomFee custom_fees = 12; + ConsensusCustomFeeList custom_fees = 12; + + /** + * Determines whether the system should check the validity of the passed keys for update. + */ + TokenKeyValidation key_verification_mode = 13; } diff --git a/hapi/hedera-protobufs/services/custom_fees.proto b/hapi/hedera-protobufs/services/custom_fees.proto index a2efee502317..7f8267ae2c4d 100644 --- a/hapi/hedera-protobufs/services/custom_fees.proto +++ b/hapi/hedera-protobufs/services/custom_fees.proto @@ -176,7 +176,7 @@ message AssessedCustomFee { /** * A custom fee definition for a consensus topic. - *

+ * * This fee definition is specific to an Hedera Consensus Service (HCS) topic * and SHOULD NOT be used in any other context.
* All fields for this message are REQUIRED.
@@ -185,12 +185,12 @@ message AssessedCustomFee { */ message ConsensusCustomFee { /** - * A fixed custom fee. - *

- * The amount of HBAR or other token described by this `FixedFee` SHALL - * be charged to the transction payer for each message submitted to a - * topic that assigns this consensus custom fee. - */ + * A fixed custom fee. + *

+ * The amount of HBAR or other token described by this `FixedFee` SHALL + * be charged to the transction payer for each message submitted to a + * topic that assigns this consensus custom fee. + */ FixedFee fixed_fee = 1; /** @@ -201,3 +201,31 @@ message ConsensusCustomFee { */ AccountID fee_collector_account_id = 2; } + +/** + * A wrapper for consensus custom fee list. + *

+ * It is used in consensus_update_topic and it helps to differentiate between + * setting an empty list and not updating the list. + */ +message ConsensusCustomFeeList { + /** + * A set of custom fee definitions.
+ * These are fees to be assessed for each submit to this topic. + */ + repeated ConsensusCustomFee fees = 1; +} + +/** + * A wrapper for fee exempt key list. + *

+ * It is used in consensus_update_topic and it helps to differentiate between + * setting an empty list and not updating the list. + */ +message FeeExemptKeyList { + /** + * A set of keys that are allowed to submit messages to the topic without + * paying the topic's custom fees. + */ + repeated Key keys = 1; +} \ No newline at end of file diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index b9c1727f7eae..6d9a060c0f80 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -117,11 +117,11 @@ message Topic { *

* If a submit transaction is signed by _any_ key from this list, * custom fees SHALL NOT be charged for that transaction.
- * If free_messages_key_list is unset, it SHALL _implicitly_ contain + * If fee_exempt_key_list is unset, it SHALL _implicitly_ contain * the key `admin_key`, the key `submit_key`, and the key * `fee_schedule_key`, if any of those keys are set. */ - repeated Key free_messages_key_list = 12; + repeated Key fee_exempt_key_list = 12; /** * A set of custom fee definitions.
diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java index af2e2cae4fa1..8e34bef41c32 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java @@ -164,7 +164,7 @@ private Optional infoForTopic( info.autoRenewPeriod(Duration.newBuilder().seconds(meta.autoRenewPeriod())); if (meta.hasAutoRenewAccountId()) info.autoRenewAccount(meta.autoRenewAccountId()); if (meta.hasFeeScheduleKey()) info.feeScheduleKey(meta.feeScheduleKey()); - if (!meta.freeMessagesKeyList().isEmpty()) info.freeMessagesKeyList(meta.freeMessagesKeyList()); + if (!meta.feeExemptKeyList().isEmpty()) info.feeExemptKeyList(meta.feeExemptKeyList()); if (!meta.customFees().isEmpty()) info.customFees(meta.customFees()); info.ledgerId(config.id()); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java index 329e2fb1678c..33674afb0484 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java @@ -229,7 +229,7 @@ private ConsensusTopicInfo getExpectedInfo() { .autoRenewPeriod(WELL_KNOWN_AUTO_RENEW_PERIOD) .ledgerId(new BytesConverter().convert("0x03")) .feeScheduleKey(feeScheduleKey) - .freeMessagesKeyList(key, anotherKey) + .feeExemptKeyList(key, anotherKey) .customFees(customFees) .build(); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java index ed30bf3d29df..1a3acfdf16f8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java @@ -230,7 +230,7 @@ protected void assertExpectationsGiven(HapiSpec spec) { for (var expectedFee : expectedFees) { expectedFee.accept(spec, actualFees); } - var actualFreeMessagesKeys = info.getFreeMessagesKeyListList(); + var actualFreeMessagesKeys = info.getFeeExemptKeyListList(); for (var expectedKey : expectedFreeMessagesKeyList) { assertTrue( actualFreeMessagesKeys.contains(spec.registry().getKey(expectedKey)), From 3508749425dacffb6d118ecc20087356f65bccd8 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 12 Sep 2024 16:14:59 +0300 Subject: [PATCH 13/94] Update protobufs specification text Signed-off-by: Zhivko Kelchev --- .../services/consensus_create_topic.proto | 2 +- .../services/consensus_topic_info.proto | 2 +- .../services/consensus_update_topic.proto | 37 +++++++++++++++---- .../services/custom_fees.proto | 37 +++++++++++++------ 4 files changed, 56 insertions(+), 22 deletions(-) diff --git a/hapi/hedera-protobufs/services/consensus_create_topic.proto b/hapi/hedera-protobufs/services/consensus_create_topic.proto index 5e55d5570a82..1aec9583506f 100644 --- a/hapi/hedera-protobufs/services/consensus_create_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_create_topic.proto @@ -77,7 +77,7 @@ message ConsensusCreateTopicTransactionBody { *

* If unset, custom fees CANNOT be set for this topic.
* If not set when the topic is created, this field CANNOT be set via update. - * If set when the topic is created, this field CAN be changed via update. + * If set when the topic is created, this field MAY be changed via update. */ Key fee_schedule_key = 8; diff --git a/hapi/hedera-protobufs/services/consensus_topic_info.proto b/hapi/hedera-protobufs/services/consensus_topic_info.proto index 00d1735d3a68..7d6f33470384 100644 --- a/hapi/hedera-protobufs/services/consensus_topic_info.proto +++ b/hapi/hedera-protobufs/services/consensus_topic_info.proto @@ -94,7 +94,7 @@ message ConsensusTopicInfo { *

* If unset, custom fees CANNOT be set for this topic.
* If not set when the topic is created, this field CANNOT be set via update. - * If set when the topic is created, this field CAN be changed via update. + * If set when the topic is created, this field MAY be changed via update. */ Key fee_schedule_key = 10; diff --git a/hapi/hedera-protobufs/services/consensus_update_topic.proto b/hapi/hedera-protobufs/services/consensus_update_topic.proto index 97f646d9367a..24379241214e 100644 --- a/hapi/hedera-protobufs/services/consensus_update_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_update_topic.proto @@ -94,17 +94,19 @@ message ConsensusUpdateTopicTransactionBody { *

* If unset in state, this field MUST NOT be set.
* If unset in this transaction and set in state, the current value SHALL - * be removed.
- * If set in this transaction, the existing key MUST sign this transaction. + * be removed.
+ * If this field is set, the existing key MUST sign this transaction. */ Key fee_schedule_key = 10; /** - * A wrapper of a set of keys that are allowed to submit messages to the topic without + * A wrapper of a set of keys.
+ * These keys are permitted to submit messages to the topic without * paying the topic's custom fees. *

- * If the wrapper is not set, the current set of keys is unchanged.
- * If the wrapper is set, but contains an empty list, the list will be updated to an empty list. + * If this field is not set, the current set of keys SHALL NOT change.
+ * If this field is set, but contains an empty list, any existing fee-exempt + * keys SHALL be removed. */ FeeExemptKeyList fee_exempt_key_list = 11; @@ -112,13 +114,32 @@ message ConsensusUpdateTopicTransactionBody { * A wrapper of a set of custom fee definitions.
* These are fees to be assessed for each submit to this topic. *

- * If the wrapper is not set, the current set of keys is unchanged.
- * If the wrapper is set, but contains an empty list, the list will be updated to an empty list. + * If this field is not set, the current set of custom fees + * SHALL NOT change.
+ * If this field is set, but contains an empty list, all current custom fees + * SHALL be removed. */ ConsensusCustomFeeList custom_fees = 12; /** - * Determines whether the system should check the validity of the passed keys for update. + * A key validation mode.
+ * Any key may be updated by a transaction signed by the topic `admin_key`. + * Each role key may _also_ sign a transaction to update that key. + * If a role key signs an update to change that role key both old + * and new key must sign the transaction, _unless_ this field is set + * to `NO_VALIDATION`, in which case the _new_ key is not required to + * sign the transaction (the existing key is still required).
+ * The primary intent for this field is to allow a role key (e.g. a + * `fee_schedule_key`) holder to "remove" that key from the token by signing + * a transaction to set that role key to an empty `KeyList`. + *

+ * If set to `FULL_VALIDATION`, either the `admin_key` or _both_ current + * and new key MUST sign this transaction to update a "key" field for the + * identified token.
+ * If set to `NO_VALIDATION`, either the `admin_key` or the current + * key MUST sign this transaction to update a "key" field for the + * identified token.
+ * This field SHALL be treated as `FULL_VALIDATION` if not set. */ TokenKeyValidation key_verification_mode = 13; } diff --git a/hapi/hedera-protobufs/services/custom_fees.proto b/hapi/hedera-protobufs/services/custom_fees.proto index 7f8267ae2c4d..9e247a809015 100644 --- a/hapi/hedera-protobufs/services/custom_fees.proto +++ b/hapi/hedera-protobufs/services/custom_fees.proto @@ -203,29 +203,42 @@ message ConsensusCustomFee { } /** - * A wrapper for consensus custom fee list. - *

- * It is used in consensus_update_topic and it helps to differentiate between - * setting an empty list and not updating the list. + * A wrapper around a consensus custom fee list.
+ * This wrapper exists to enable an update transaction to differentiate between + * a field that is not set and an empty list of custom fees. + * + * An _unset_ field of this type SHALL NOT modify existing values.
+ * A _set_ field of this type with an empty `fees` list SHALL remove any + * existing values. */ message ConsensusCustomFeeList { /** * A set of custom fee definitions.
- * These are fees to be assessed for each submit to this topic. + * These are fees to be assessed for each submit to a topic. */ repeated ConsensusCustomFee fees = 1; } /** - * A wrapper for fee exempt key list. - *

- * It is used in consensus_update_topic and it helps to differentiate between - * setting an empty list and not updating the list. + * A wrapper for fee exempt key list.
+ * This wrapper exists to enable an update transaction to differentiate between + * a field that is not set and an empty list of keys. + * + * An _unset_ field of this type SHALL NOT modify existing values.
+ * A _set_ field of this type with an empty `keys` list SHALL remove any + * existing values. */ message FeeExemptKeyList { /** - * A set of keys that are allowed to submit messages to the topic without - * paying the topic's custom fees. + * A set of keys. + * The keys in this list are permitted to submit messages to the + * topic without paying the topic's custom fees. + *

+ * If a submit transaction is signed by _any_ key included in this set, + * custom fees SHALL NOT be charged for that transaction. + *

+ * This list SHALL _implicitly_ include the following keys + * `adminKey`, `submitKey`, `fee_schedule_key`. */ repeated Key keys = 1; -} \ No newline at end of file +} From a720252a175cba701bd66401b9e0babd0c48d4ff Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 12 Sep 2024 17:06:45 +0300 Subject: [PATCH 14/94] Replace FMLK with FEKL in HapiGetTopicInfo Signed-off-by: Zhivko Kelchev --- .../bdd/spec/queries/consensus/HapiGetTopicInfo.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java index 1a3acfdf16f8..d6c6fa3063ed 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java @@ -66,7 +66,7 @@ public class HapiGetTopicInfo extends HapiQueryOp { private Optional submitKey = Optional.empty(); private Optional autoRenewAccount = Optional.empty(); private Optional feeScheduleKey = Optional.empty(); - private final List expectedFreeMessagesKeyList = new ArrayList<>(); + private final List expectedFeeExemptKeyList = new ArrayList<>(); private final List>> expectedFees = new ArrayList<>(); private boolean saveRunningHash = false; private Optional seqNoInfoObserver = Optional.empty(); @@ -163,8 +163,8 @@ public HapiGetTopicInfo savingSeqNoTo(LongConsumer consumer) { return this; } - public HapiGetTopicInfo hasFreeMessagesKeys(List freeMessagesKeyAssertion) { - expectedFreeMessagesKeyList.addAll(freeMessagesKeyAssertion); + public HapiGetTopicInfo hasFeeExemptKeys(List feeExemptKeyAssertion) { + expectedFeeExemptKeyList.addAll(feeExemptKeyAssertion); return this; } @@ -230,10 +230,10 @@ protected void assertExpectationsGiven(HapiSpec spec) { for (var expectedFee : expectedFees) { expectedFee.accept(spec, actualFees); } - var actualFreeMessagesKeys = info.getFeeExemptKeyListList(); - for (var expectedKey : expectedFreeMessagesKeyList) { + var actualFeeExemptKeys = info.getFeeExemptKeyListList(); + for (var expectedKey : expectedFeeExemptKeyList) { assertTrue( - actualFreeMessagesKeys.contains(spec.registry().getKey(expectedKey)), + actualFeeExemptKeys.contains(spec.registry().getKey(expectedKey)), "Doesn't contain free messages key!"); } expectedLedgerId.ifPresent(id -> Assertions.assertEquals(id, info.getLedgerId())); From 0f9a89705ba11119f21d3b12eacafbd529c15294 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 12 Sep 2024 17:15:59 +0300 Subject: [PATCH 15/94] Replace FMLK with FEKL in HapiGetTopicInfo Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 12 +++++----- .../hedera/node/config/data/TopicsConfig.java | 2 +- .../handlers/ConsensusCreateTopicHandler.java | 22 +++++++++---------- .../consensus/HapiTopicCreate.java | 10 ++++----- .../bdd/suites/hip991/TopicCustomFeeBase.java | 10 ++++----- .../bdd/suites/hip991/TopicCustomFeeTest.java | 18 +++++++-------- 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index d8c5a0cfe22d..06a0d36c8639 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1595,17 +1595,17 @@ enum ResponseCodeEnum { INVALID_TOKEN_IN_PENDING_AIRDROP = 369; /** - * The number of entries in the free messages key list exceed the maximum. + * The number of entries in the fee exempt key list exceed the maximum. */ - MAX_ENTRIES_FOR_FMKL_EXCEEDED = 370; + MAX_ENTRIES_FOR_FEKL_EXCEEDED = 370; /** - * There is a duplicate key in the free messages key list. + * There is a duplicate key in the fee exempt key list. */ - FMKL_CONTAINS_DUPLICATED_KEYS = 371; + FEKL_CONTAINS_DUPLICATED_KEYS = 371; /** - * There is an invalid key in the free messages key list. + * There is an invalid key in the fee exempt key list. */ - INVALID_KEY_IN_FMKL = 372; + INVALID_KEY_IN_FEKL = 372; } diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java index 6379e5c70535..f5dd1e8875c7 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java @@ -24,4 +24,4 @@ public record TopicsConfig( @ConfigProperty(defaultValue = "1000000") @NetworkProperty long maxNumber, @ConfigProperty(defaultValue = "10") @NetworkProperty int maxCustoFeeEntriesForTopics, - @ConfigProperty(defaultValue = "10") @NetworkProperty int maxEntriesForFreeMessagesKeyList) {} + @ConfigProperty(defaultValue = "10") @NetworkProperty int maxEntriesForFeeExemptKeyList) {} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index bf53b51f719c..e0320673c736 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -20,13 +20,13 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTORENEW_DURATION_NOT_IN_RANGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.BAD_ENCODING; import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEES_LIST_TOO_LONG; -import static com.hedera.hapi.node.base.ResponseCodeEnum.FMKL_CONTAINS_DUPLICATED_KEYS; +import static com.hedera.hapi.node.base.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_EXPIRATION_TIME; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FMKL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FEKL; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; -import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FMKL_EXCEEDED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FEKL_EXCEEDED; import static com.hedera.node.app.hapi.utils.fee.ConsensusServiceFeeBuilder.getConsensusCreateTopicFee; import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.RUNNING_HASH_BYTE_ARRAY_SIZE; import static com.hedera.node.app.spi.validation.AttributeValidator.isImmutableKey; @@ -85,8 +85,8 @@ public ConsensusCreateTopicHandler(@NonNull final ConsensusCustomFeesValidator c @Override public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { final var op = txn.consensusCreateTopicOrThrow(); - final var uniqueKeysCount = op.freeMessagesKeyList().stream().distinct().count(); - validateTruePreCheck(uniqueKeysCount == op.freeMessagesKeyList().size(), FMKL_CONTAINS_DUPLICATED_KEYS); + final var uniqueKeysCount = op.feeExemptKeyList().stream().distinct().count(); + validateTruePreCheck(uniqueKeysCount == op.feeExemptKeyList().size(), FEKL_CONTAINS_DUPLICATED_KEYS); } @Override @@ -201,13 +201,13 @@ private void validateSemantics( } // validate size of the list and the keys - if (!op.freeMessagesKeyList().isEmpty()) { + if (!op.feeExemptKeyList().isEmpty()) { validateTrue( - op.freeMessagesKeyList().size() <= topicConfig.maxEntriesForFreeMessagesKeyList(), - MAX_ENTRIES_FOR_FMKL_EXCEEDED); - op.freeMessagesKeyList() - .forEach(key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FMKL)); - builder.freeMessagesKeyList(op.freeMessagesKeyList()); + op.feeExemptKeyList().size() <= topicConfig.maxEntriesForFeeExemptKeyList(), + MAX_ENTRIES_FOR_FEKL_EXCEEDED); + op.feeExemptKeyList() + .forEach(key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEKL)); + builder.feeExemptKeyList(op.feeExemptKeyList()); } // validate custom fees diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java index 76dd711d3e0a..63b32dd55a3a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java @@ -65,7 +65,7 @@ public class HapiTopicCreate extends HapiTxnOp { private Optional feeScheduleKeyName = Optional.empty(); private Optional feeScheduleKeyShape = Optional.empty(); private final List> feeScheduleSuppliers = new ArrayList<>(); - private Optional>> freeMesssagesKeyNamesList = Optional.empty(); + private Optional>> feeExemptKeyNamesList = Optional.empty(); private Optional> freeMesssageKeyList = Optional.empty(); /** For some test we need the capability to build transaction has no autoRenewPeiord */ @@ -100,8 +100,8 @@ public HapiTopicCreate feeScheduleKeyName(final String s) { return this; } - public HapiTopicCreate freeMessagesKeys(String... keys) { - freeMesssagesKeyNamesList = Optional.of(Stream.of(keys) + public HapiTopicCreate feeExemptKeys(String... keys) { + feeExemptKeyNamesList = Optional.of(Stream.of(keys) .>map(k -> spec -> spec.registry().getKey(k)) .collect(toList())); return self(); @@ -170,7 +170,7 @@ protected Consumer opBodyDef(final HapiSpec spec) throw autoRenewAccountId.ifPresent(id -> b.setAutoRenewAccount(asId(id, spec))); autoRenewPeriod.ifPresent(secs -> b.setAutoRenewPeriod(asDuration(secs))); feeScheduleKey.ifPresent(b::setFeeScheduleKey); - freeMesssageKeyList.ifPresent(keys -> keys.forEach(b::addFreeMessagesKeyList)); + freeMesssageKeyList.ifPresent(keys -> keys.forEach(b::addFeeExemptKeyList)); if (!feeScheduleSuppliers.isEmpty()) { for (final var supplier : feeScheduleSuppliers) { b.addCustomFees(supplier.apply(spec)); @@ -196,7 +196,7 @@ private void genKeysFor(final HapiSpec spec) { feeScheduleKey = Optional.of(netOf(spec, feeScheduleKeyName, feeScheduleKeyShape)); } - freeMesssagesKeyNamesList.ifPresent(functions -> freeMesssageKeyList = Optional.of(functions.stream() + feeExemptKeyNamesList.ifPresent(functions -> freeMesssageKeyList = Optional.of(functions.stream() .map(f -> f.apply(spec)) .filter(k -> k != null && k != Key.getDefaultInstance()) .collect(toList()))); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 23db856c4fe3..bce2af1837a3 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -28,7 +28,7 @@ public class TopicCustomFeeBase { protected static final String ADMIN_KEY = "adminKey"; protected static final String SUBMIT_KEY = "submitKey"; protected static final String FEE_SCHEDULE_KEY = "feeScheduleKey"; - protected static final String FREE_MSG_KEY_PREFIX = "freeMessageKey_"; + protected static final String FEE_EXEMPT_KEY_PREFIX = "feeExemptKey_"; // This key is truly invalid, as all Ed25519 public keys must be 32 bytes long protected static final Key STRUCTURALLY_INVALID_KEY = @@ -38,18 +38,18 @@ protected static SpecOperation[] setupBaseKeys() { return new SpecOperation[] {newKeyNamed(ADMIN_KEY), newKeyNamed(SUBMIT_KEY), newKeyNamed(FEE_SCHEDULE_KEY)}; } - protected static SpecOperation[] newNamedKeysForFMKL(int count) { + protected static SpecOperation[] newNamedKeysForFEKL(int count) { final var list = new ArrayList(); for (int i = 0; i < count; i++) { - list.add(newKeyNamed(FREE_MSG_KEY_PREFIX + i)); + list.add(newKeyNamed(FEE_EXEMPT_KEY_PREFIX + i)); } return list.toArray(new SpecOperation[0]); } - protected static String[] freeMsgKeyNames(int count) { + protected static String[] feeExemptKeyNames(int count) { final var list = new ArrayList(); for (int i = 0; i < count; i++) { - list.add(FREE_MSG_KEY_PREFIX + i); + list.add(FEE_EXEMPT_KEY_PREFIX + i); } return list.toArray(new String[0]); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index abeba1ddccf8..f0ef848b7c15 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -33,7 +33,7 @@ import static com.hedera.services.bdd.suites.HapiSuite.flattened; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FMKL_CONTAINS_DUPLICATED_KEYS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import com.hedera.services.bdd.junit.HapiTest; @@ -141,24 +141,24 @@ final Stream createTopicWithOneHTSFixedFee() { } @HapiTest - @DisplayName("Create topic with all keys") + @DisplayName("Create topic with 10 keys in FEKL") // TOPIC_FEE_020 - final Stream createTopicWithFMLK() { + final Stream createTopicWithFEKL() { return hapiTest(flattened( // create 10 keys - newNamedKeysForFMKL(10), + newNamedKeysForFEKL(10), createTopic(TOPIC) .adminKeyName(ADMIN_KEY) .submitKeyName(SUBMIT_KEY) .feeScheduleKeyName(FEE_SCHEDULE_KEY) // set list of 10 keys - .freeMessagesKeys(freeMsgKeyNames(10)), + .feeExemptKeys(feeExemptKeyNames(10)), getTopicInfo(TOPIC) .hasAdminKey(ADMIN_KEY) .hasSubmitKey(SUBMIT_KEY) .hasFeeScheduleKey(FEE_SCHEDULE_KEY) // assert the list - .hasFreeMessagesKeys(List.of(freeMsgKeyNames(10))))); + .hasFeeExemptKeys(List.of(feeExemptKeyNames(10))))); } } @@ -172,7 +172,7 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { } @HapiTest - @DisplayName("Create topic with duplicated signatures in FMKL") + @DisplayName("Create topic with duplicated signatures in FEKL") // TOPIC_FEE_023 final Stream createTopicWithDuplicateSignatures() { final var testKey = "testKey"; @@ -182,8 +182,8 @@ final Stream createTopicWithDuplicateSignatures() { .adminKeyName(ADMIN_KEY) .submitKeyName(SUBMIT_KEY) .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .freeMessagesKeys(testKey, testKey) - .hasPrecheck(FMKL_CONTAINS_DUPLICATED_KEYS))); + .feeExemptKeys(testKey, testKey) + .hasPrecheck(FEKL_CONTAINS_DUPLICATED_KEYS))); } @HapiTest From e52365f8e83d02e0657df79d143c66ce48ae3232 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 13 Sep 2024 17:20:02 +0300 Subject: [PATCH 16/94] Add FEKL and custom fee amount validation Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 13 +++++++--- .../handlers/ConsensusCreateTopicHandler.java | 2 ++ .../ConsensusCustomFeesValidator.java | 17 +++++++++---- .../token/impl/util/TokenHandlerHelper.java | 25 ++++++++++++++++--- 4 files changed, 46 insertions(+), 11 deletions(-) diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 06a0d36c8639..0e0619f11c6b 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1595,17 +1595,24 @@ enum ResponseCodeEnum { INVALID_TOKEN_IN_PENDING_AIRDROP = 369; /** - * The number of entries in the fee exempt key list exceed the maximum. + * The provided fee exempt key list size exceeded the limit. */ MAX_ENTRIES_FOR_FEKL_EXCEEDED = 370; /** - * There is a duplicate key in the fee exempt key list. + * The provided fee exempt key list contains duplicated keys. */ FEKL_CONTAINS_DUPLICATED_KEYS = 371; /** - * There is an invalid key in the fee exempt key list. + * The provided fee exempt key list contains an invalid key. */ INVALID_KEY_IN_FEKL = 372; + + /** + * Custom fee list is missing. + *

+ * Fee exempt key list MAY be set only if custom fee list is set. + */ + MISSING_CUSTOM_FEES = 373; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index e0320673c736..a6ef057d7971 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -27,6 +27,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FEKL; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FEKL_EXCEEDED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MISSING_CUSTOM_FEES; import static com.hedera.node.app.hapi.utils.fee.ConsensusServiceFeeBuilder.getConsensusCreateTopicFee; import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.RUNNING_HASH_BYTE_ARRAY_SIZE; import static com.hedera.node.app.spi.validation.AttributeValidator.isImmutableKey; @@ -205,6 +206,7 @@ private void validateSemantics( validateTrue( op.feeExemptKeyList().size() <= topicConfig.maxEntriesForFeeExemptKeyList(), MAX_ENTRIES_FOR_FEKL_EXCEEDED); + validateTrue(!op.customFees().isEmpty(), MISSING_CUSTOM_FEES); op.feeExemptKeyList() .forEach(key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEKL)); builder.feeExemptKeyList(op.feeExemptKeyList()); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java index 8837ad820739..c37a3b567c75 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java @@ -16,14 +16,16 @@ package com.hedera.node.app.service.consensus.impl.validators; +import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY; import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON; import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEE_NOT_FULLY_SPECIFIED; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CUSTOM_FEE_COLLECTOR; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID_IN_CUSTOM_FEES; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR; +import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.TokenValidations.REQUIRE_NOT_PAUSED; +import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsable; import static com.hedera.node.app.service.token.impl.util.TokenHandlerHelper.getIfUsableForAliasedId; -import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -91,7 +93,11 @@ private void validateFixedFeeForCreation( validateTrue(fixedFee.amount() > 0, CUSTOM_FEE_MUST_BE_POSITIVE); if (fixedFee.hasDenominatingTokenId()) { validateExplicitTokenDenomination( - fee.feeCollectorAccountId(), fixedFee.denominatingTokenId(), tokenRelationStore, tokenStore); + fee.feeCollectorAccountId(), + fixedFee.denominatingTokenId(), + fixedFee.amount(), + tokenRelationStore, + tokenStore); } } @@ -100,17 +106,18 @@ private void validateFixedFeeForCreation( * * @param feeCollectorNum The fee collector account number. * @param tokenNum The token number used for token denomination. + * @param feeAmount The fee amount. * @param tokenRelationStore The token relation store. * @param tokenStore The token store. */ private void validateExplicitTokenDenomination( @NonNull final AccountID feeCollectorNum, @NonNull final TokenID tokenNum, + @NonNull final long feeAmount, @NonNull final ReadableTokenRelationStore tokenRelationStore, @NonNull final ReadableTokenStore tokenStore) { - final var denomToken = tokenStore.get(tokenNum); - validateTrue(denomToken != null, INVALID_TOKEN_ID_IN_CUSTOM_FEES); - validateFalse(denomToken.paused(), INVALID_TOKEN_ID_IN_CUSTOM_FEES); + final var denomToken = getIfUsable(tokenNum, tokenStore, REQUIRE_NOT_PAUSED, INVALID_TOKEN_ID_IN_CUSTOM_FEES); + validateTrue(feeAmount <= denomToken.maxSupply(), AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY); validateTrue(isFungibleCommon(denomToken.tokenType()), CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON); validateTrue(tokenRelationStore.get(feeCollectorNum, tokenNum) != null, TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR); } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java index 1e3709b099b1..c875e4a5c834 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java @@ -188,15 +188,34 @@ public static Token getIfUsable( @NonNull final TokenID tokenId, @NonNull final ReadableTokenStore tokenStore, @NonNull final TokenValidations tokenValidations) { + return getIfUsable(tokenId, tokenStore, tokenValidations, null); + } + + /** + * Returns the token if it exists and is usable. A {@link HandleException} is thrown if the token is invalid. + * + * @param tokenId the ID of the token to get + * @param tokenStore the {@link ReadableTokenStore} to use for token retrieval + * @param tokenValidations whether validate paused token status + * @param errorIfNotUsable the error response code, if token is not usable + * @return the token if it exists and is usable + * @throws HandleException if any of the token conditions are not met + */ + @NonNull + public static Token getIfUsable( + @NonNull final TokenID tokenId, + @NonNull final ReadableTokenStore tokenStore, + @NonNull final TokenValidations tokenValidations, + @Nullable final ResponseCodeEnum errorIfNotUsable) { requireNonNull(tokenId); requireNonNull(tokenStore); requireNonNull(tokenValidations); final var token = tokenStore.get(tokenId); - validateTrue(token != null, INVALID_TOKEN_ID); - validateFalse(token.deleted(), TOKEN_WAS_DELETED); + validateTrue(token != null, errorIfNotUsable == null ? INVALID_TOKEN_ID : errorIfNotUsable); + validateFalse(token.deleted(), errorIfNotUsable == null ? TOKEN_WAS_DELETED : errorIfNotUsable); if (tokenValidations == REQUIRE_NOT_PAUSED) { - validateFalse(token.paused(), TOKEN_IS_PAUSED); + validateFalse(token.paused(), errorIfNotUsable == null ? TOKEN_IS_PAUSED : errorIfNotUsable); } return token; } From c54f1f58c0a84b10da364dd6c992fab81a851d36 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Sun, 15 Sep 2024 12:48:51 +0300 Subject: [PATCH 17/94] fix tests Signed-off-by: Zhivko Kelchev --- .../impl/validators/ConsensusCustomFeesValidator.java | 2 +- .../impl/test/handlers/ConsensusCreateTopicTest.java | 6 +++++- .../services/bdd/suites/hip991/TopicCustomFeeTest.java | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java index c37a3b567c75..bb49a000c1a8 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java @@ -117,7 +117,7 @@ private void validateExplicitTokenDenomination( @NonNull final ReadableTokenRelationStore tokenRelationStore, @NonNull final ReadableTokenStore tokenStore) { final var denomToken = getIfUsable(tokenNum, tokenStore, REQUIRE_NOT_PAUSED, INVALID_TOKEN_ID_IN_CUSTOM_FEES); - validateTrue(feeAmount <= denomToken.maxSupply(), AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY); + validateTrue(feeAmount >= denomToken.maxSupply(), AMOUNT_EXCEEDS_TOKEN_MAX_SUPPLY); validateTrue(isFungibleCommon(denomToken.tokenType()), CUSTOM_FEE_DENOMINATION_MUST_BE_FUNGIBLE_COMMON); validateTrue(tokenRelationStore.get(feeCollectorNum, tokenNum) != null, TOKEN_NOT_ASSOCIATED_TO_FEE_COLLECTOR); } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java index 409f639c8bed..60a0ee2d87da 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java @@ -43,6 +43,7 @@ import com.hedera.hapi.node.state.consensus.Topic; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.records.ConsensusCreateTopicStreamBuilder; @@ -124,6 +125,7 @@ void setUp() { .getOrCreateConfig(); topicStore = new WritableTopicStore(writableStates, config, storeMetricsService); given(handleContext.configuration()).willReturn(config); + given(handleContext.storeFactory().readableStore(ReadableTopicStore.class)).willReturn(topicStore); given(storeFactory.writableStore(WritableTopicStore.class)).willReturn(topicStore); given(handleContext.savepointStack()).willReturn(stack); given(stack.getBaseBuilder(ConsensusCreateTopicStreamBuilder.class)).willReturn(recordBuilder); @@ -393,6 +395,7 @@ void failsWhenMaxRegimeExceeds() { final var adminKey = SIMPLE_KEY_A; final var submitKey = SIMPLE_KEY_B; final var txnBody = newCreateTxn(adminKey, submitKey, true); + final var op = txnBody.consensusCreateTopic(); given(handleContext.body()).willReturn(txnBody); final var writableState = writableTopicStateWithOneKey(); @@ -400,7 +403,8 @@ void failsWhenMaxRegimeExceeds() { final var topicStore = new WritableTopicStore(writableStates, config, storeMetricsService); assertEquals(1, topicStore.sizeOfState()); given(storeFactory.writableStore(WritableTopicStore.class)).willReturn(topicStore); - + given(storeFactory.readableStore(ReadableTopicStore.class)).willReturn(topicStore); + given(handleContext.expiryValidator()).willReturn(expiryValidator); given(handleContext.attributeValidator()).willReturn(validator); final var config = HederaTestConfigBuilder.create() .withValue("topics.maxNumber", 1L) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index f0ef848b7c15..1bc10812182d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -144,13 +144,16 @@ final Stream createTopicWithOneHTSFixedFee() { @DisplayName("Create topic with 10 keys in FEKL") // TOPIC_FEE_020 final Stream createTopicWithFEKL() { + final var collector = "collector"; return hapiTest(flattened( // create 10 keys newNamedKeysForFEKL(10), + cryptoCreate(collector), createTopic(TOPIC) .adminKeyName(ADMIN_KEY) .submitKeyName(SUBMIT_KEY) .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(5, collector)) // set list of 10 keys .feeExemptKeys(feeExemptKeyNames(10)), getTopicInfo(TOPIC) From 63687f0ad71b8e13dadb7f80161adccdc8eb624f Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Sun, 15 Sep 2024 13:22:25 +0300 Subject: [PATCH 18/94] spotless Signed-off-by: Zhivko Kelchev --- .../consensus/impl/test/handlers/ConsensusCreateTopicTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java index 60a0ee2d87da..20c31dddbb01 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java @@ -125,7 +125,8 @@ void setUp() { .getOrCreateConfig(); topicStore = new WritableTopicStore(writableStates, config, storeMetricsService); given(handleContext.configuration()).willReturn(config); - given(handleContext.storeFactory().readableStore(ReadableTopicStore.class)).willReturn(topicStore); + given(handleContext.storeFactory().readableStore(ReadableTopicStore.class)) + .willReturn(topicStore); given(storeFactory.writableStore(WritableTopicStore.class)).willReturn(topicStore); given(handleContext.savepointStack()).willReturn(stack); given(stack.getBaseBuilder(ConsensusCreateTopicStreamBuilder.class)).willReturn(recordBuilder); From c339784138869e7da7d43032dcde7646e506a907 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 17 Sep 2024 12:42:26 +0300 Subject: [PATCH 19/94] fix merge conflicts Signed-off-by: Zhivko Kelchev --- .../services/bdd/spec/transactions/token/CustomFeeSpecs.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java index 9e6938866003..dad2ca3005fd 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java @@ -127,10 +127,6 @@ static CustomFee builtFixedHbar(long amount, String collector, boolean allCollec return baseFixedBuilder(amount, collector, allCollectorsExempt, spec).build(); } - static ConsensusCustomFee builtFixedTopicHbar(long amount, String collector, HapiSpec spec) { - return baseFixedTopicBuilder(amount, collector, spec).build(); - } - static FixedFee builtFixedHbarSansCollector(long amount) { return FixedFee.newBuilder().setAmount(amount).build(); } From 25ee202870479d4564e49c95142df15357a95d35 Mon Sep 17 00:00:00 2001 From: ibankov Date: Tue, 17 Sep 2024 15:18:05 +0300 Subject: [PATCH 20/94] Topic allowances protobufs Signed-off-by: ibankov --- .../services/basic_types.proto | 59 +++++++++++++++++ .../consensus_approve_topic_allowance.proto | 65 +++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto diff --git a/hapi/hedera-protobufs/services/basic_types.proto b/hapi/hedera-protobufs/services/basic_types.proto index e3a50ccbbb2a..6eecad38b6f2 100644 --- a/hapi/hedera-protobufs/services/basic_types.proto +++ b/hapi/hedera-protobufs/services/basic_types.proto @@ -1773,3 +1773,62 @@ message PendingAirdropValue { */ uint64 amount = 1; } + +/** + * An approved allowance of hbar transfers for a spender.
+ * This message SHALL be used to record the allowance of hbars that an account has approved + * for sending messages to a given topic. + */ +message ConsensusCryptoFeeScheduleAllowance { + /** + * The account ID of the hbar owner (ie. the grantor of the allowance). + */ + AccountID owner = 1; + + /** + * The topic ID enabled to spend fees from the hbar allowance. + */ + TopicID topicId = 2; + + /** + * The maximum amount of the spender's allowance in tinybars. + */ + uint64 amount = 3; + + /** + * The maximum amount of the spender's token allowance per message. + */ + uint64 amount_per_message = 4; +} + +/** + * An approved allowance of hbar transfers for a spender.
+ * This message SHALL be used to record the allowance of fungible tokens + * that an account has approved for sending messages to a given topic. + */ +message ConsensusTokenFeeScheduleAllowance { + /** + * The token that the allowance pertains to. + */ + TokenID tokenId = 1; + + /** + * The account ID of the token owner (ie. the grantor of the allowance). + */ + AccountID owner = 2; + + /** + * The topic ID enabled to spend fees from the token allowance. + */ + TopicID topicId = 3; + + /** + * The maximum amount of the spender's token allowance. + */ + uint64 amount = 4; + + /** + * The maximum amount of the spender's token allowance per message. + */ + uint64 amount_per_message = 5; +} diff --git a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto new file mode 100644 index 000000000000..96ab6a4eea6b --- /dev/null +++ b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto @@ -0,0 +1,65 @@ +/** + * # Approve Allowance for Topic + * Messages used to approve allowance for a given topic. + * + * ### Keywords + * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + * "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + * document are to be interpreted as described in [RFC2119](https://www.ietf.org/rfc/rfc2119). + */ +syntax = "proto3"; + +package proto; + +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +option java_package = "com.hederahashgraph.api.proto.java"; +// <<>> This comment is special code for setting PBJ Compiler java package +option java_multiple_files = true; + +import "basic_types.proto"; + +/** + * Approve Topic Allowance
+ //TODO: Add more description + * Complete one or more pending transfers on behalf of the + * recipient(s) for an airdrop. + * + * The sender MUST have sufficient balance to fulfill the airdrop at the + * time of claim. If the sender does not have sufficient balance, the + * claim SHALL fail.
+ * Each pending airdrop successfully claimed SHALL be removed from state and + * SHALL NOT be available to claim again.
+ * Each claim SHALL be represented in the transaction body and + * SHALL NOT be restated in the record file.
+ * All claims MUST succeed for this transaction to succeed. + * + * ### Record Stream Effects + * The completed transfers SHALL be present in the transfer list. + */ +message ConsensusApproveAllowanceTransactionBody { + /** + * List of hbar allowances approved by the account owner. + */ + repeated ConsensusCryptoFeeScheduleAllowance consensus_crypto_fee_schedule_allowances = 4; + + /** + * List of fungible token allowances approved by the account owner. + */ + repeated ConsensusTokenFeeScheduleAllowance consensus_token_fee_schedule_allowances = 5; +} \ No newline at end of file From 0ed094c627cd4f28330e4444eaeedf82d088b2a8 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 10:41:02 +0300 Subject: [PATCH 21/94] extend protobufs Signed-off-by: ibankov --- .../services/basic_types.proto | 5 +++++ .../consensus_approve_topic_allowance.proto | 19 +++++-------------- .../services/transaction_body.proto | 6 ++++++ .../java/com/hedera/hapi/util/HapiUtils.java | 1 + 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/hapi/hedera-protobufs/services/basic_types.proto b/hapi/hedera-protobufs/services/basic_types.proto index 6eecad38b6f2..b1ec168a6d18 100644 --- a/hapi/hedera-protobufs/services/basic_types.proto +++ b/hapi/hedera-protobufs/services/basic_types.proto @@ -1249,6 +1249,11 @@ enum HederaFunctionality { * Claim one or more pending airdrops */ TokenClaimAirdrop = 95; + + /** + * Approve allowance for a given topic + */ + ConsensusApproveAllowance = 96; } /** diff --git a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto index 96ab6a4eea6b..636376d82ab1 100644 --- a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto +++ b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto @@ -36,21 +36,12 @@ import "basic_types.proto"; /** * Approve Topic Allowance
- //TODO: Add more description - * Complete one or more pending transfers on behalf of the - * recipient(s) for an airdrop. + * Approve allowance for submitting a message to a topic with custom fee. * - * The sender MUST have sufficient balance to fulfill the airdrop at the - * time of claim. If the sender does not have sufficient balance, the - * claim SHALL fail.
- * Each pending airdrop successfully claimed SHALL be removed from state and - * SHALL NOT be available to claim again.
- * Each claim SHALL be represented in the transaction body and - * SHALL NOT be restated in the record file.
- * All claims MUST succeed for this transaction to succeed. - * - * ### Record Stream Effects - * The completed transfers SHALL be present in the transfer list. + * The topic MUST have a given custom fee in order to set allowance for that fee, + * otherwise the transaction SHALL fail.
+ * Setting the amount to zero in 'CryptoAllowance' or 'TokenAllowance' + * SHALL remove the respective allowance for the spender
*/ message ConsensusApproveAllowanceTransactionBody { /** diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index cd4546147654..6c0ea209c735 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -54,6 +54,7 @@ import "consensus_create_topic.proto"; import "consensus_update_topic.proto"; import "consensus_delete_topic.proto"; import "consensus_submit_message.proto"; +import "consensus_approve_topic_allowance.proto"; import "unchecked_submit.proto"; @@ -418,5 +419,10 @@ message TransactionBody { * A transaction body for a `claimAirdrop` request. */ TokenClaimAirdropTransactionBody tokenClaimAirdrop = 60; + + /** + * A transaction body for a `consensusApproveAllowance` request. + */ + ConsensusApproveAllowanceTransactionBody consensusApproveAllowance = 61; } } diff --git a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java index e0f23a353d3d..07de67e3fab5 100644 --- a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java +++ b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java @@ -185,6 +185,7 @@ public static HederaFunctionality functionOf(final TransactionBody txn) throws U case CONSENSUS_UPDATE_TOPIC -> HederaFunctionality.CONSENSUS_UPDATE_TOPIC; case CONSENSUS_DELETE_TOPIC -> HederaFunctionality.CONSENSUS_DELETE_TOPIC; case CONSENSUS_SUBMIT_MESSAGE -> HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE; + case CONSENSUS_APPROVE_ALLOWANCE -> HederaFunctionality.CONSENSUS_APPROVE_ALLOWANCE; case CONTRACT_CALL -> HederaFunctionality.CONTRACT_CALL; case CONTRACT_CREATE_INSTANCE -> HederaFunctionality.CONTRACT_CREATE; case CONTRACT_UPDATE_INSTANCE -> HederaFunctionality.CONTRACT_UPDATE; From 9ddc6a1bde170e3aeb2dcee180e37b72488cd32d Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 10:46:55 +0300 Subject: [PATCH 22/94] consensus approve allowance proto Signed-off-by: ibankov --- .../services/basic_types.proto | 64 +++++++++++++++++++ .../consensus_approve_topic_allowance.proto | 56 ++++++++++++++++ .../services/transaction_body.proto | 6 ++ .../java/com/hedera/hapi/util/HapiUtils.java | 1 + 4 files changed, 127 insertions(+) create mode 100644 hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto diff --git a/hapi/hedera-protobufs/services/basic_types.proto b/hapi/hedera-protobufs/services/basic_types.proto index e3a50ccbbb2a..b1ec168a6d18 100644 --- a/hapi/hedera-protobufs/services/basic_types.proto +++ b/hapi/hedera-protobufs/services/basic_types.proto @@ -1249,6 +1249,11 @@ enum HederaFunctionality { * Claim one or more pending airdrops */ TokenClaimAirdrop = 95; + + /** + * Approve allowance for a given topic + */ + ConsensusApproveAllowance = 96; } /** @@ -1773,3 +1778,62 @@ message PendingAirdropValue { */ uint64 amount = 1; } + +/** + * An approved allowance of hbar transfers for a spender.
+ * This message SHALL be used to record the allowance of hbars that an account has approved + * for sending messages to a given topic. + */ +message ConsensusCryptoFeeScheduleAllowance { + /** + * The account ID of the hbar owner (ie. the grantor of the allowance). + */ + AccountID owner = 1; + + /** + * The topic ID enabled to spend fees from the hbar allowance. + */ + TopicID topicId = 2; + + /** + * The maximum amount of the spender's allowance in tinybars. + */ + uint64 amount = 3; + + /** + * The maximum amount of the spender's token allowance per message. + */ + uint64 amount_per_message = 4; +} + +/** + * An approved allowance of hbar transfers for a spender.
+ * This message SHALL be used to record the allowance of fungible tokens + * that an account has approved for sending messages to a given topic. + */ +message ConsensusTokenFeeScheduleAllowance { + /** + * The token that the allowance pertains to. + */ + TokenID tokenId = 1; + + /** + * The account ID of the token owner (ie. the grantor of the allowance). + */ + AccountID owner = 2; + + /** + * The topic ID enabled to spend fees from the token allowance. + */ + TopicID topicId = 3; + + /** + * The maximum amount of the spender's token allowance. + */ + uint64 amount = 4; + + /** + * The maximum amount of the spender's token allowance per message. + */ + uint64 amount_per_message = 5; +} diff --git a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto new file mode 100644 index 000000000000..3ef1c4ce7b8c --- /dev/null +++ b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto @@ -0,0 +1,56 @@ +/** + * # Approve Allowance for Topic + * Messages used to approve allowance for a given topic. + * + * ### Keywords + * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", + * "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this + * document are to be interpreted as described in [RFC2119](https://www.ietf.org/rfc/rfc2119). + */ +syntax = "proto3"; + +package proto; + +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +option java_package = "com.hederahashgraph.api.proto.java"; +// <<>> This comment is special code for setting PBJ Compiler java package +option java_multiple_files = true; + +import "basic_types.proto"; + +/** + * Approve Topic Allowance
+ * Approve allowance for submitting a message to a topic with custom fee. + * + * The topic MUST have a given custom fee in order to set allowance for that fee, + * otherwise the transaction SHALL fail.
+ * Setting the amount to zero in 'CryptoAllowance' or 'TokenAllowance' + * SHALL remove the respective allowance for the spender
+ */ +message ConsensusApproveAllowanceTransactionBody { + /** + * List of hbar allowances approved by the account owner. + */ + repeated ConsensusCryptoFeeScheduleAllowance consensus_crypto_fee_schedule_allowances = 4; + + /** + * List of fungible token allowances approved by the account owner. + */ + repeated ConsensusTokenFeeScheduleAllowance consensus_token_fee_schedule_allowances = 5; +} diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index cd4546147654..6c0ea209c735 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -54,6 +54,7 @@ import "consensus_create_topic.proto"; import "consensus_update_topic.proto"; import "consensus_delete_topic.proto"; import "consensus_submit_message.proto"; +import "consensus_approve_topic_allowance.proto"; import "unchecked_submit.proto"; @@ -418,5 +419,10 @@ message TransactionBody { * A transaction body for a `claimAirdrop` request. */ TokenClaimAirdropTransactionBody tokenClaimAirdrop = 60; + + /** + * A transaction body for a `consensusApproveAllowance` request. + */ + ConsensusApproveAllowanceTransactionBody consensusApproveAllowance = 61; } } diff --git a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java index e0f23a353d3d..07de67e3fab5 100644 --- a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java +++ b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java @@ -185,6 +185,7 @@ public static HederaFunctionality functionOf(final TransactionBody txn) throws U case CONSENSUS_UPDATE_TOPIC -> HederaFunctionality.CONSENSUS_UPDATE_TOPIC; case CONSENSUS_DELETE_TOPIC -> HederaFunctionality.CONSENSUS_DELETE_TOPIC; case CONSENSUS_SUBMIT_MESSAGE -> HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE; + case CONSENSUS_APPROVE_ALLOWANCE -> HederaFunctionality.CONSENSUS_APPROVE_ALLOWANCE; case CONTRACT_CALL -> HederaFunctionality.CONTRACT_CALL; case CONTRACT_CREATE_INSTANCE -> HederaFunctionality.CONTRACT_CREATE; case CONTRACT_UPDATE_INSTANCE -> HederaFunctionality.CONTRACT_UPDATE; From f473cbbd8de04707f74c8a3cb107490444c30aa5 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 12:05:25 +0300 Subject: [PATCH 23/94] consensus approve allowance handler Signed-off-by: ibankov --- .../services/response_code.proto | 10 ++ .../ConsensusApproveAllowanceHandler.java | 98 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 0e0619f11c6b..1195b1fe33b0 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1615,4 +1615,14 @@ enum ResponseCodeEnum { * Fee exempt key list MAY be set only if custom fee list is set. */ MISSING_CUSTOM_FEES = 373; + + /** + * Allowance per message is higher than total allowance. + */ + ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE = 374; + + /** + * Repeated AccountId/TopicId pair in the transaction body. + */ + REPEATED_ALLOWANCE_IN_TRANSACTION_BODY = 375; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java new file mode 100644 index 000000000000..add9882a23f1 --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.handlers; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hedera.node.app.spi.validation.Validations.mustExist; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.HandleException; +import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.app.spi.workflows.PreHandleContext; +import com.hedera.node.app.spi.workflows.TransactionHandler; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.HashMap; + +/** + * This class contains all workflow-related functionality regarding {@link HederaFunctionality#CONSENSUS_APPROVE_ALLOWANCE}. + */ +public class ConsensusApproveAllowanceHandler implements TransactionHandler { + @Override + public void preHandle(@NonNull PreHandleContext context) throws PreCheckException { + // TODO: Implement this method + } + + @Override + public void pureChecks(@NonNull TransactionBody txn) throws PreCheckException { + requireNonNull(txn); + final var op = txn.consensusApproveAllowanceOrThrow(); + + // The transaction must have at least one type of allowance. + final var cryptoAllowances = op.consensusCryptoFeeScheduleAllowances(); + final var tokenAllowances = op.consensusTokenFeeScheduleAllowances(); + final var totalAllowancesSize = cryptoAllowances.size() + tokenAllowances.size(); + validateTruePreCheck(totalAllowancesSize != 0, EMPTY_ALLOWANCES); + + // validate hbar allowances + final var uniqueMap = new HashMap<>(); + for (var hbarAllowance : cryptoAllowances) { + // Check if a given AccountId/TopicId pair already exists in the crypto allowances list + validateFalsePreCheck( + uniqueMap.containsKey(hbarAllowance.owner()) + && uniqueMap.get(hbarAllowance.owner()).equals(hbarAllowance.topicId()), + ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); + // Validate the allowance amount and amount per message + validateTruePreCheck(hbarAllowance.amount() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(hbarAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck( + hbarAllowance.amount() > hbarAllowance.amountPerMessage(), + ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); + // Add the unique (AccountID, TopicID) pair to the map + uniqueMap.put(hbarAllowance.owner(), hbarAllowance.topicId()); + } + // validate token allowances + uniqueMap.clear(); + for (var tokenAllowance : tokenAllowances) { + // Check if a given AccountId/TopicId pair already exists in the token allowances list + validateFalsePreCheck( + uniqueMap.containsKey(tokenAllowance.owner()) + && uniqueMap.get(tokenAllowance.owner()).equals(tokenAllowance.tokenId()), + ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); + // Validate the allowance amount and amount per message + validateTruePreCheck(tokenAllowance.amount() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck( + tokenAllowance.amount() > tokenAllowance.amountPerMessage(), + ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); + mustExist(tokenAllowance.tokenId(), INVALID_TOKEN_ID); + // Add the unique (AccountID, TopicID) pair to the map + uniqueMap.put(tokenAllowance.owner(), tokenAllowance.topicId()); + } + } + + @Override + public void handle(@NonNull HandleContext context) throws HandleException { + // TODO: Implement this method + } +} From 11b7bb4df4fe10802c1c0298ae92c23e45df03d4 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 12:08:19 +0300 Subject: [PATCH 24/94] api permissions config Signed-off-by: ibankov --- .../java/com/hedera/node/config/data/ApiPermissionConfig.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java index dfde84696748..98417998cc92 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java @@ -16,6 +16,7 @@ package com.hedera.node.config.data; +import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_APPROVE_ALLOWANCE; import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_CREATE_TOPIC; import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_DELETE_TOPIC; import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_GET_TOPIC_INFO; @@ -222,6 +223,7 @@ public record ApiPermissionConfig( @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange deleteTopic, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange submitMessage, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange getTopicInfo, + @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange approveTopicAllowance, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange ethereumTransaction, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange scheduleCreate, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange scheduleSign, @@ -285,6 +287,7 @@ public record ApiPermissionConfig( permissionKeys.put(CONSENSUS_UPDATE_TOPIC, c -> c.updateTopic); permissionKeys.put(CONSENSUS_DELETE_TOPIC, c -> c.deleteTopic); permissionKeys.put(CONSENSUS_SUBMIT_MESSAGE, c -> c.submitMessage); + permissionKeys.put(CONSENSUS_APPROVE_ALLOWANCE, c -> c.approveTopicAllowance); permissionKeys.put(TOKEN_CREATE, c -> c.tokenCreate); permissionKeys.put(TOKEN_FREEZE_ACCOUNT, c -> c.tokenFreezeAccount); permissionKeys.put(TOKEN_UNFREEZE_ACCOUNT, c -> c.tokenUnfreezeAccount); From 1ed4d1988748f438db456b94ba2fbc8e6317e616 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 13:26:26 +0300 Subject: [PATCH 25/94] validator and unit tests Signed-off-by: ibankov --- .../ConsensusApproveAllowanceHandler.java | 89 +++++----- .../ConsensusAllowancesValidator.java | 97 +++++++++++ .../ConsensusAllowanceValidatorTest.java | 163 ++++++++++++++++++ 3 files changed, 299 insertions(+), 50 deletions(-) create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java create mode 100644 hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java index add9882a23f1..86296665353a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java @@ -16,83 +16,72 @@ package com.hedera.node.app.service.consensus.impl.handlers; -import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; -import static com.hedera.node.app.spi.validation.Validations.mustExist; -import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; -import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.spi.workflows.PreHandleContext; import com.hedera.node.app.spi.workflows.TransactionHandler; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.HashMap; + +import javax.inject.Inject; +import javax.inject.Singleton; /** * This class contains all workflow-related functionality regarding {@link HederaFunctionality#CONSENSUS_APPROVE_ALLOWANCE}. */ +@Singleton public class ConsensusApproveAllowanceHandler implements TransactionHandler { + private final ConsensusAllowancesValidator validator; + + /** + * Default constructor for injection. + * @param allowancesValidator allowances validator + */ + @Inject + public ConsensusApproveAllowanceHandler(@NonNull final ConsensusAllowancesValidator allowancesValidator) { + requireNonNull(allowancesValidator); + this.validator = allowancesValidator; + } + @Override public void preHandle(@NonNull PreHandleContext context) throws PreCheckException { - // TODO: Implement this method + requireNonNull(context); + final var txn = context.body(); + final var payerId = context.payer(); + final var op = txn.consensusApproveAllowanceOrThrow(); + + for (final var allowance : op.consensusCryptoFeeScheduleAllowances()) { + final var owner = allowance.owner(); + if (owner != null && !owner.equals(payerId)) { + context.requireKeyOrThrow(owner, INVALID_ALLOWANCE_OWNER_ID); + } + } + + for (final var allowance : op.consensusTokenFeeScheduleAllowances()) { + final var owner = allowance.owner(); + if (owner != null && !owner.equals(payerId)) { + context.requireKeyOrThrow(owner, INVALID_ALLOWANCE_OWNER_ID); + } + } } @Override public void pureChecks(@NonNull TransactionBody txn) throws PreCheckException { requireNonNull(txn); final var op = txn.consensusApproveAllowanceOrThrow(); - - // The transaction must have at least one type of allowance. - final var cryptoAllowances = op.consensusCryptoFeeScheduleAllowances(); - final var tokenAllowances = op.consensusTokenFeeScheduleAllowances(); - final var totalAllowancesSize = cryptoAllowances.size() + tokenAllowances.size(); - validateTruePreCheck(totalAllowancesSize != 0, EMPTY_ALLOWANCES); - - // validate hbar allowances - final var uniqueMap = new HashMap<>(); - for (var hbarAllowance : cryptoAllowances) { - // Check if a given AccountId/TopicId pair already exists in the crypto allowances list - validateFalsePreCheck( - uniqueMap.containsKey(hbarAllowance.owner()) - && uniqueMap.get(hbarAllowance.owner()).equals(hbarAllowance.topicId()), - ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); - // Validate the allowance amount and amount per message - validateTruePreCheck(hbarAllowance.amount() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck(hbarAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck( - hbarAllowance.amount() > hbarAllowance.amountPerMessage(), - ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); - // Add the unique (AccountID, TopicID) pair to the map - uniqueMap.put(hbarAllowance.owner(), hbarAllowance.topicId()); - } - // validate token allowances - uniqueMap.clear(); - for (var tokenAllowance : tokenAllowances) { - // Check if a given AccountId/TopicId pair already exists in the token allowances list - validateFalsePreCheck( - uniqueMap.containsKey(tokenAllowance.owner()) - && uniqueMap.get(tokenAllowance.owner()).equals(tokenAllowance.tokenId()), - ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); - // Validate the allowance amount and amount per message - validateTruePreCheck(tokenAllowance.amount() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck( - tokenAllowance.amount() > tokenAllowance.amountPerMessage(), - ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); - mustExist(tokenAllowance.tokenId(), INVALID_TOKEN_ID); - // Add the unique (AccountID, TopicID) pair to the map - uniqueMap.put(tokenAllowance.owner(), tokenAllowance.topicId()); - } + validator.pureChecks(op); } @Override public void handle(@NonNull HandleContext context) throws HandleException { + context.storeFactory().writableStore(WritableAccountStore.class); // TODO: Implement this method } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java new file mode 100644 index 000000000000..130786d9dede --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.validators; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hedera.node.app.spi.validation.Validations.mustExist; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; +import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; +import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; +import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.token.ConsensusApproveAllowanceTransactionBody; +import com.hedera.node.app.spi.workflows.PreCheckException; + +import javax.inject.Inject; +import java.util.HashMap; +import java.util.List; + +public class ConsensusAllowancesValidator { + + /** + * Constructs a {@link ConsensusAllowancesValidator} instance. + */ + @Inject + public ConsensusAllowancesValidator() { + // Needed for Dagger injection + } + + public void pureChecks(ConsensusApproveAllowanceTransactionBody op) throws PreCheckException { + // The transaction must have at least one type of allowance. + final var cryptoAllowances = op.consensusCryptoFeeScheduleAllowances(); + final var tokenAllowances = op.consensusTokenFeeScheduleAllowances(); + final var totalAllowancesSize = cryptoAllowances.size() + tokenAllowances.size(); + validateTruePreCheck(totalAllowancesSize != 0, EMPTY_ALLOWANCES); + validateCryptoAllowances(cryptoAllowances); + validateTokenAllowances(tokenAllowances); + } + + private static void validateCryptoAllowances(List cryptoAllowances) + throws PreCheckException { + final var uniqueMap = new HashMap(); + for (var hbarAllowance : cryptoAllowances) { + // Check if a given AccountId/TopicId pair already exists in the crypto allowances list + validateFalsePreCheck( + uniqueMap.containsKey(hbarAllowance.owner()) + && uniqueMap.get(hbarAllowance.owner()).equals(hbarAllowance.topicId()), + ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); + // Validate the allowance amount and amount per message + validateTruePreCheck(hbarAllowance.amount() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(hbarAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck( + hbarAllowance.amount() > hbarAllowance.amountPerMessage(), + ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); + // Add the unique (AccountID, TopicID) pair to the map + uniqueMap.put(hbarAllowance.owner(), hbarAllowance.topicId()); + } + } + + private static void validateTokenAllowances(List tokenAllowances) + throws PreCheckException { + final var uniqueMap = new HashMap(); + for (var tokenAllowance : tokenAllowances) { + // Check if a given AccountId/TopicId pair already exists in the token allowances list + validateFalsePreCheck( + uniqueMap.containsKey(tokenAllowance.owner()) + && uniqueMap.get(tokenAllowance.owner()).equals(tokenAllowance.topicId()), + ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); + // Validate the allowance amount and amount per message + validateTruePreCheck(tokenAllowance.amount() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck( + tokenAllowance.amount() > tokenAllowance.amountPerMessage(), + ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); + mustExist(tokenAllowance.tokenId(), INVALID_TOKEN_ID); + // Add the unique (AccountID, TopicID) pair to the map + uniqueMap.put(tokenAllowance.owner(), tokenAllowance.topicId()); + } + } +} diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java new file mode 100644 index 000000000000..de08bb0526a6 --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java @@ -0,0 +1,163 @@ +package com.hedera.node.app.service.consensus.impl.test.validators; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT; +import static com.hedera.hapi.node.base.ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; +import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.token.ConsensusApproveAllowanceTransactionBody; +import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; +import com.hedera.node.app.spi.workflows.PreCheckException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +class ConsensusAllowancesValidatorTest { + + private ConsensusAllowancesValidator validator; + + @BeforeEach + void setUp() { + validator = new ConsensusAllowancesValidator(); + } + + @Test + void validAllowancesPasses() throws PreCheckException { + // Arrange + var cryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .amount(100L) + .amountPerMessage(10L) + .build(); + + var tokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .amount(200L) + .amountPerMessage(20L) + .tokenId(TokenID.DEFAULT) + .build(); + + var op = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusCryptoFeeScheduleAllowances(cryptoAllowance) + .consensusTokenFeeScheduleAllowances(tokenAllowance) + .build(); + + // Act & Assert: Should pass without exception + validator.pureChecks(op); + } + + @Test + void emptyAllowancesThrows() { + // Arrange: Create an operation with no allowances + var op = ConsensusApproveAllowanceTransactionBody.newBuilder().build(); + + // Act & Assert: Should throw PreCheckException with EMPTY_ALLOWANCES response + var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); + assertEquals(exception.responseCode(), EMPTY_ALLOWANCES); + } + + @Test + void repeatedTokenAllowancesThrows() { + // Arrange: Create two token allowances with the same owner and topicId + var tokenAllowance1 = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .amount(100L) + .amountPerMessage(10L) + .tokenId(TokenID.DEFAULT) + .build(); + + var tokenAllowance2 = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) // Same AccountID and TopicID as tokenAllowance1 + .amount(200L) + .amountPerMessage(20L) + .tokenId(TokenID.DEFAULT) + .build(); + + // Build the operation containing the token allowances + var op = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusTokenFeeScheduleAllowances(tokenAllowance1, tokenAllowance2) + .build(); + + // Act & Assert: Should throw PreCheckException with REPEATED_ALLOWANCE_IN_TRANSACTION_BODY + var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); + assertEquals(REPEATED_ALLOWANCE_IN_TRANSACTION_BODY, exception.responseCode()); + } + + @Test + void repeatedCryptoAllowancesThrows() { + // Arrange: Create two crypto allowances with the same owner and topicId + var cryptoAllowance1 = ConsensusCryptoFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .amount(100L) + .amountPerMessage(10L) + .build(); + + var cryptoAllowance2 = ConsensusCryptoFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) // Same AccountID and TopicID as cryptoAllowance1 + .amount(200L) + .amountPerMessage(20L) + .build(); + + // Build the operation containing the crypto allowances + var op = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusCryptoFeeScheduleAllowances(cryptoAllowance1, cryptoAllowance2) + .build(); + + // Act & Assert: Should throw PreCheckException with REPEATED_ALLOWANCE_IN_TRANSACTION_BODY + var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); + assertEquals(REPEATED_ALLOWANCE_IN_TRANSACTION_BODY, exception.responseCode()); + } + + @Test + void invalidTokenIdThrows() { + // Arrange + var tokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .amount(100L) + .amountPerMessage(10L) + .tokenId((TokenID) null) // Invalid token ID + .build(); + + var op = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusTokenFeeScheduleAllowances(tokenAllowance) + .build(); + + // Act & Assert: Should throw PreCheckException with INVALID_TOKEN_ID code + var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); + assertEquals(INVALID_TOKEN_ID, exception.responseCode()); + } + + @Test + void negativeAmountsThrows() { + // Arrange + var cryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .amount(-100L) // Negative allowance + .amountPerMessage(10L) + .build(); + + var op = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusCryptoFeeScheduleAllowances(cryptoAllowance) + .build(); + + // Act & Assert: Should throw PreCheckException with NEGATIVE_ALLOWANCE_AMOUNT code + var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); + assertEquals(NEGATIVE_ALLOWANCE_AMOUNT, exception.responseCode()); + } +} + From d620eb66317f8a2cb5d3302ada1a55ea22194cf8 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 14:05:56 +0300 Subject: [PATCH 26/94] fix protos and add builders Signed-off-by: ibankov --- .../services/state/consensus/topic.proto | 38 +++++++++++++ .../handlers/ConsensusDeleteTopicTest.java | 24 ++++----- .../impl/test/handlers/ConsensusTestBase.java | 53 +++++++++---------- .../test/handlers/ConsensusTestUtils.java | 25 ++++----- 4 files changed, 84 insertions(+), 56 deletions(-) diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 7aee87952a09..8218d73c4ec2 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -137,4 +137,42 @@ message Topic { * charged _in addition to_ the base network and node fees. */ repeated ConsensusCustomFee custom_fees = 13; + + /** + * (Optional) List of crypto allowances for the topic. + * It contains account number for which the allowance is approved to and + * the amount approved for that account. + */ + repeated TopicCryptoAllowance crypto_allowances = 14; + + /** + * (Optional) List of fungible token allowances for the topic. + * It contains account number for which the allowance is approved to and the token number. + * It also contains and the amount approved for that account. + */ + repeated TopicFungibleTokenAllowance token_allowances = 30; +} + +/** + * Allowance with a given amount granted by account for this topic. + * This allows the spender to send messages to this topic while utilizing + * the allocated allowance to cover the associated cost. + */ +message TopicCryptoAllowance { + AccountID spender_id = 1; + uint64 amount = 2; + uint64 amount_per_message = 3; +} + +/** + * Allowance granted by and account for a specific fungible token and this topic. + * This also contains the amount of the token that is approved for the account. + * This allows the spender to send messages to this topic while utilizing + * the allocated fungible token allowance to cover the associated cost. + */ +message TopicFungibleTokenAllowance { + TokenID token_id = 1; + AccountID spender_id = 2; + uint64 amount = 3; + uint64 amount_per_message = 4; } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java index aeb1abbf9843..3b1b2fa5007f 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java @@ -191,20 +191,16 @@ void adminKeyDoesntExist() { final var txn = newDeleteTxn(); given(handleContext.body()).willReturn(txn); - topic = new Topic( - topicId, - sequenceNumber, - expirationTime, - autoRenewSecs, - AccountID.newBuilder().accountNum(10L).build(), - false, - Bytes.wrap(runningHash), - memo, - null, - null, - null, - null, - null); + topic = Topic.newBuilder() + .topicId(topicId) + .sequenceNumber(sequenceNumber) + .expirationSecond(expirationTime) + .autoRenewPeriod(autoRenewSecs) + .autoRenewAccountId(AccountID.newBuilder().accountNum(10L).build()) + .deleted(false) + .runningHash(Bytes.wrap(runningHash)) + .memo(memo) + .build(); writableTopicState = writableTopicStateWithOneKey(); given(writableStates.get(TOPICS_KEY)).willReturn(writableTopicState); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java index 9d1c23afa51d..5f70730dd1d8 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java @@ -211,34 +211,31 @@ protected void givenValidTopic(AccountID autoRenewAccountId, boolean deleted, bo protected void givenValidTopic( AccountID autoRenewAccountId, boolean deleted, boolean withAdminKey, boolean withSubmitKey) { - topic = new Topic( - topicId, - sequenceNumber, - expirationTime, - autoRenewSecs, - autoRenewAccountId, - deleted, - Bytes.wrap(runningHash), - memo, - withAdminKey ? key : null, - withSubmitKey ? key : null, - feeScheduleKey, - List.of(key, anotherKey), - customFees); - topicNoKeys = new Topic( - topicId, - sequenceNumber, - expirationTime, - autoRenewSecs, - autoRenewAccountId, - deleted, - Bytes.wrap(runningHash), - memo, - null, - null, - feeScheduleKey, - List.of(key, anotherKey), - customFees); + topic = Topic.newBuilder() + .topicId(topicId) + .sequenceNumber(sequenceNumber) + .expirationSecond(expirationTime) + .autoRenewPeriod(autoRenewSecs) + .autoRenewAccountId(autoRenewAccountId) + .deleted(deleted) + .runningHash(Bytes.wrap(runningHash)) + .memo(memo) + .adminKey(withAdminKey ? key : null) + .submitKey(withSubmitKey ? key : null) + .feeScheduleKey(feeScheduleKey) + .feeExemptKeyList(List.of(key, anotherKey)) + .customFees(customFees) + .build(); + topicNoKeys = Topic.newBuilder() + .topicId(topicId) + .sequenceNumber(sequenceNumber) + .expirationSecond(expirationTime) + .autoRenewPeriod(autoRenewSecs) + .autoRenewAccountId(autoRenewAccountId) + .deleted(deleted) + .runningHash(Bytes.wrap(runningHash)) + .memo(memo) + .build(); } protected Topic createTopic() { diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java index ea69bd5640f9..96a02fc14629 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java @@ -65,19 +65,16 @@ static void mockTopicLookup(Key adminKey, Key submitKey, ReadableTopicStore topi } static Topic newTopic(Key admin, Key submit) { - return new Topic( - TopicID.newBuilder().topicNum(123L).build(), - -1L, - 0L, - -1L, - AccountID.newBuilder().accountNum(1234567L).build(), - false, - null, - "memo", - admin, - submit, - null, - null, - null); + return Topic.newBuilder() + .topicId(TopicID.newBuilder().topicNum(123L).build()) + .sequenceNumber(-1L) + .expirationSecond(0L) + .autoRenewPeriod(-1L) + .autoRenewAccountId(AccountID.newBuilder().accountNum(1234567L).build()) + .deleted(false) + .memo("memo") + .adminKey(admin) + .submitKey(submit) + .build(); } } From ee2a6b4dd2489df09dcd8fc0845d31b8ac290f3a Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 14:18:59 +0300 Subject: [PATCH 27/94] fix indentation Signed-off-by: ibankov --- hapi/hedera-protobufs/services/basic_types.proto | 12 ++++++------ .../services/state/consensus/topic.proto | 10 +++++----- .../hedera-protobufs/services/transaction_body.proto | 10 +++++----- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/hapi/hedera-protobufs/services/basic_types.proto b/hapi/hedera-protobufs/services/basic_types.proto index b1ec168a6d18..261f76bb7398 100644 --- a/hapi/hedera-protobufs/services/basic_types.proto +++ b/hapi/hedera-protobufs/services/basic_types.proto @@ -1241,17 +1241,17 @@ enum HederaFunctionality { TokenAirdrop = 93; /** - * Remove one or more pending airdrops from state on behalf of the sender(s) for each airdrop. - */ + * Remove one or more pending airdrops from state on behalf of the sender(s) for each airdrop. + */ TokenCancelAirdrop = 94; /** - * Claim one or more pending airdrops + * Claim one or more pending airdrops */ TokenClaimAirdrop = 95; /** - * Approve allowance for a given topic + * Approve allowance for a given topic */ ConsensusApproveAllowance = 96; } @@ -1813,8 +1813,8 @@ message ConsensusCryptoFeeScheduleAllowance { */ message ConsensusTokenFeeScheduleAllowance { /** - * The token that the allowance pertains to. - */ + * The token that the allowance pertains to. + */ TokenID tokenId = 1; /** diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 8218d73c4ec2..e0f43c183010 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -154,10 +154,10 @@ message Topic { } /** - * Allowance with a given amount granted by account for this topic. - * This allows the spender to send messages to this topic while utilizing - * the allocated allowance to cover the associated cost. - */ + * An allowance with a given amount granted by account for this topic. + * This allows the spender to send messages to this topic while utilizing + * the allocated allowance to cover the associated cost. + */ message TopicCryptoAllowance { AccountID spender_id = 1; uint64 amount = 2; @@ -165,7 +165,7 @@ message TopicCryptoAllowance { } /** - * Allowance granted by and account for a specific fungible token and this topic. + * An allowance granted by and account for a specific fungible token and this topic. * This also contains the amount of the token that is approved for the account. * This allows the spender to send messages to this topic while utilizing * the allocated fungible token allowance to cover the associated cost. diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index 6c0ea209c735..05bf59a9200a 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -411,18 +411,18 @@ message TransactionBody { TokenAirdropTransactionBody tokenAirdrop = 58; /** - * A transaction body for a `cancelAirdrop` request. - */ + * A transaction body for a `cancelAirdrop` request. + */ TokenCancelAirdropTransactionBody tokenCancelAirdrop = 59; /** - * A transaction body for a `claimAirdrop` request. + * A transaction body for a `claimAirdrop` request. */ TokenClaimAirdropTransactionBody tokenClaimAirdrop = 60; /** - * A transaction body for a `consensusApproveAllowance` request. - */ + * A transaction body for a `consensusApproveAllowance` request. + */ ConsensusApproveAllowanceTransactionBody consensusApproveAllowance = 61; } } From 54afd4583e82247c98041ae37d05c272f150c943 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 14:45:35 +0300 Subject: [PATCH 28/94] improving descriptions of allowance lists Signed-off-by: ibankov --- .../services/state/consensus/topic.proto | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index e0f43c183010..3166354ecf32 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -52,6 +52,8 @@ option java_multiple_files = true; * 8. (Optional) A fee schedule key whose signature must be active for the topic's custom fees to be updated. * 9. (Optional) A list of keys that can submit messages without paying custom fees. * 10. (Optional) A list of custom fees to be assessed for each message submitted to the topic. + * 11. (Optional) A list of crypto allowances for the topic. + * 12. (Optional) A list of fungible token allowances for the topic. */ message Topic { /** @@ -110,7 +112,7 @@ message Topic { * If this field is unset, the current custom fees CANNOT be changed.
* If this field is set, that `Key` MUST sign any transaction to update * the custom fee schedule for this topic. - */ + */ Key fee_schedule_key = 11; /** @@ -139,15 +141,24 @@ message Topic { repeated ConsensusCustomFee custom_fees = 13; /** - * (Optional) List of crypto allowances for the topic. + * A list of crypto allowances for the topic. + *

+ * If a submit transaction is submitted by a user with an allowance, + * the custom fee SHALL be payed with the allowance
+ * If the allowance is not enough to pay the fee, the transaction SHALL fail.
+ * If an allowance amount is set to 0, the allowance SHALL be removed.
* It contains account number for which the allowance is approved to and * the amount approved for that account. */ repeated TopicCryptoAllowance crypto_allowances = 14; /** - * (Optional) List of fungible token allowances for the topic. - * It contains account number for which the allowance is approved to and the token number. + * List of fungible token allowances for the topic. + *

+ * If a submit transaction is submitted by a user with an allowance, + * the custom fee SHALL be payed with the token allowance
+ * If the allowance is not enough to pay the fee, the transaction SHALL fail.
+ * It contains account number for which the allowance is approved to and the token number.
* It also contains and the amount approved for that account. */ repeated TopicFungibleTokenAllowance token_allowances = 30; From f4cf0541b62bef35b5f09bcce6d2815706a03e95 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 15:57:38 +0300 Subject: [PATCH 29/94] add further documentation Signed-off-by: ibankov --- .../services/state/consensus/topic.proto | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 3166354ecf32..913c10e3f524 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -165,25 +165,51 @@ message Topic { } /** - * An allowance with a given amount granted by account for this topic. + * Representation of crypto allowance for a topic in the network Merkle tree. + * + * A crypto allowance for a topic represents the hbar funds allocated by an account to cover the fees + * required for submitting messages when the topic is configured with 'custom fees'. * This allows the spender to send messages to this topic while utilizing * the allocated allowance to cover the associated cost. */ message TopicCryptoAllowance { + /** + * The account ID of the spender. + */ AccountID spender_id = 1; + /** + * The total amount of hbar allocated for the allowance. + */ uint64 amount = 2; + /** + * The amount of hbar allocated per message. + */ uint64 amount_per_message = 3; } /** - * An allowance granted by and account for a specific fungible token and this topic. - * This also contains the amount of the token that is approved for the account. + * Representation of fungible token allowance for a topic in the network Merkle tree. + + * A fungible token allowance for a topic represents the fungible token amounts allocated by an account + * to cover the fees required for submitting messages when the topic is configured with 'custom fees'. * This allows the spender to send messages to this topic while utilizing * the allocated fungible token allowance to cover the associated cost. */ message TopicFungibleTokenAllowance { + /** + * The ID of the fungible token. + */ TokenID token_id = 1; + /** + * The account ID of the spender. + */ AccountID spender_id = 2; + /** + * The total amount of fungible tokens allocated for the allowance. + */ uint64 amount = 3; + /** + * The amount of fungible tokens allocated per message. + */ uint64 amount_per_message = 4; } From c2fdf527497baa267d7491302a028cf4749fd960 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 19 Sep 2024 17:37:36 +0300 Subject: [PATCH 30/94] Add grcp endpoint Signed-off-by: ibankov --- hapi/hedera-protobufs/services/consensus_service.proto | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/hapi/hedera-protobufs/services/consensus_service.proto b/hapi/hedera-protobufs/services/consensus_service.proto index fbee23f131b6..ea0d88c9abca 100644 --- a/hapi/hedera-protobufs/services/consensus_service.proto +++ b/hapi/hedera-protobufs/services/consensus_service.proto @@ -120,4 +120,12 @@ service ConsensusService { * Request is [ConsensusSubmitMessageTransactionBody](#proto.ConsensusSubmitMessageTransactionBody) */ rpc submitMessage (Transaction) returns (TransactionResponse); + + /** + * Approve allowance for custom fees. + *

+ * Set account allowances for a topic. This includes total allowance and allowance per message. + * Request is [ConsensusApproveAllowanceTransactionBody](#proto.ConsensusApproveAllowanceTransactionBody) + */ + rpc approveAllowance (Transaction) returns (TransactionResponse); } From af5e2e3bd3fd16f9008e289200ea88d60ea300d2 Mon Sep 17 00:00:00 2001 From: ibankov Date: Fri, 20 Sep 2024 15:19:34 +0300 Subject: [PATCH 31/94] addressing PR comments Signed-off-by: ibankov --- .../consensus_approve_topic_allowance.proto | 24 ++++++++++++++++--- .../services/state/consensus/topic.proto | 10 ++++---- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto index 3ef1c4ce7b8c..f3e29c78cfa2 100644 --- a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto +++ b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto @@ -5,7 +5,9 @@ * ### Keywords * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", * "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this - * document are to be interpreted as described in [RFC2119](https://www.ietf.org/rfc/rfc2119). + * document are to be interpreted as described in + * [RFC2119](https://www.ietf.org/rfc/rfc2119) and clarified in + * [RFC8174](https://www.ietf.org/rfc/rfc8174). */ syntax = "proto3"; @@ -45,12 +47,28 @@ import "basic_types.proto"; */ message ConsensusApproveAllowanceTransactionBody { /** - * List of hbar allowances approved by the account owner. + * A list of Fee Schedule Allowances.
+ * This list details hbar allowances approved by the account owner. + *

+ * This list MAY be empty.
+ * If this list is empty, the `consensus_token_fee_schedule_allowances` + * list MUST NOT be empty.
+ * Amounts assigned here SHALL NOT be available to transfer.
+ * Consensus Custom Fee Schedule charges SHALL be deducted from allowances + * in this list. */ repeated ConsensusCryptoFeeScheduleAllowance consensus_crypto_fee_schedule_allowances = 4; /** - * List of fungible token allowances approved by the account owner. + * A list of Fee Schedule Allowances.
+ * This list details fungible token allowances approved by the account owner. + *

+ * This list MAY be empty.
+ * If this list is empty, the `consensus_crypto_fee_schedule_allowances` + * list MUST NOT be empty.
+ * Amounts assigned here SHALL NOT be available to transfer.
+ * Consensus Custom Fee Schedule charges SHALL be deducted from allowances + * in this list. */ repeated ConsensusTokenFeeScheduleAllowance consensus_token_fee_schedule_allowances = 5; } diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 913c10e3f524..0d5c88894856 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -141,19 +141,19 @@ message Topic { repeated ConsensusCustomFee custom_fees = 13; /** - * A list of crypto allowances for the topic. + * A list of crypto allowances for this topic. *

* If a submit transaction is submitted by a user with an allowance, - * the custom fee SHALL be payed with the allowance
+ * the custom fee SHALL be paid with the allowance.
* If the allowance is not enough to pay the fee, the transaction SHALL fail.
* If an allowance amount is set to 0, the allowance SHALL be removed.
- * It contains account number for which the allowance is approved to and - * the amount approved for that account. + * It SHALL contain account identifier for which the allowance is approved, + * and the amount approved for that account. */ repeated TopicCryptoAllowance crypto_allowances = 14; /** - * List of fungible token allowances for the topic. + * A list of fungible token allowances for this topic. *

* If a submit transaction is submitted by a user with an allowance, * the custom fee SHALL be payed with the token allowance
From b7c2e84c727c816fdabfbd8de5e7bb73edc969e3 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 26 Sep 2024 15:20:02 +0300 Subject: [PATCH 32/94] approve allowance initial commit Signed-off-by: ibankov --- .../services/response_code.proto | 13 +- .../mainnet/upgrade/throttles.json | 1 + .../testnet/upgrade/throttles.json | 1 + .../dispatcher/TransactionDispatcher.java | 1 + .../dispatcher/TransactionHandlers.java | 2 + .../handle/HandleWorkflowModule.java | 1 + .../ConsensusApproveAllowanceHandler.java | 231 +++++++++++++++++- .../impl/handlers/ConsensusHandlers.java | 16 +- .../impl/util/ConsensusHandlerHelper.java | 61 +++++ .../ConsensusAllowancesValidator.java | 88 ++++++- .../test/handlers/ConsensusHandlersTest.java | 14 +- .../ConsensusAllowanceValidatorTest.java | 18 +- .../consensus/ConsensusServiceDefinition.java | 8 +- .../ConsensusServiceDefinitionTest.java | 3 +- .../main/resources/genesis/throttles-dev.json | 1 + .../src/main/resources/genesis/throttles.json | 1 + .../bdd/junit/hedera/utils/GrpcUtils.java | 2 + .../bdd/spec/transactions/TxnFactory.java | 6 + .../bdd/spec/transactions/TxnVerbs.java | 5 + .../consensus/HapiTopicApproveAllowance.java | 128 ++++++++++ .../bdd/suites/hip991/TopicCustomFeeBase.java | 2 + .../bdd/suites/hip991/TopicCustomFeeTest.java | 28 +++ 22 files changed, 615 insertions(+), 16 deletions(-) create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 1195b1fe33b0..bdc0c00d36e8 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1617,12 +1617,17 @@ enum ResponseCodeEnum { MISSING_CUSTOM_FEES = 373; /** - * Allowance per message is higher than total allowance. - */ + * Allowance per message is higher than total allowance. + */ ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE = 374; /** - * Repeated AccountId/TopicId pair in the transaction body. - */ + * Repeated AccountId/TopicId pair in the transaction body. + */ REPEATED_ALLOWANCE_IN_TRANSACTION_BODY = 375; + + /** + * The topic has been marked as deleted. + */ + TOPIC_DELETED = 376; } diff --git a/hedera-node/configuration/mainnet/upgrade/throttles.json b/hedera-node/configuration/mainnet/upgrade/throttles.json index 06ae864ff7c7..60c6b59d6fc8 100644 --- a/hedera-node/configuration/mainnet/upgrade/throttles.json +++ b/hedera-node/configuration/mainnet/upgrade/throttles.json @@ -21,6 +21,7 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", + "ConsensusApproveAllowance", "TokenGetNftInfo", "TokenGetInfo", "ScheduleDelete", diff --git a/hedera-node/configuration/testnet/upgrade/throttles.json b/hedera-node/configuration/testnet/upgrade/throttles.json index e1d83b109ac0..bb02f0108cbc 100644 --- a/hedera-node/configuration/testnet/upgrade/throttles.json +++ b/hedera-node/configuration/testnet/upgrade/throttles.json @@ -22,6 +22,7 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", + "ConsensusApproveAllowance", "TokenGetNftInfo", "TokenGetInfo", "ScheduleDelete", diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java index 98fc4abe168d..d7b3f06200ff 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java @@ -155,6 +155,7 @@ private TransactionHandler getHandler(@NonNull final TransactionBody txBody) { case CONSENSUS_UPDATE_TOPIC -> handlers.consensusUpdateTopicHandler(); case CONSENSUS_DELETE_TOPIC -> handlers.consensusDeleteTopicHandler(); case CONSENSUS_SUBMIT_MESSAGE -> handlers.consensusSubmitMessageHandler(); + case CONSENSUS_APPROVE_ALLOWANCE -> handlers.consensusApproveAllowanceHandler(); case CONTRACT_CREATE_INSTANCE -> handlers.contractCreateHandler(); case CONTRACT_UPDATE_INSTANCE -> handlers.contractUpdateHandler(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java index d361c5838d8c..b0ac8efa8100 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java @@ -19,6 +19,7 @@ import com.hedera.node.app.service.addressbook.impl.handlers.NodeCreateHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeDeleteHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeUpdateHandler; +import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusDeleteTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusSubmitMessageHandler; @@ -81,6 +82,7 @@ public record TransactionHandlers( @NonNull ConsensusUpdateTopicHandler consensusUpdateTopicHandler, @NonNull ConsensusDeleteTopicHandler consensusDeleteTopicHandler, @NonNull ConsensusSubmitMessageHandler consensusSubmitMessageHandler, + @NonNull ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler, @NonNull ContractCreateHandler contractCreateHandler, @NonNull ContractUpdateHandler contractUpdateHandler, @NonNull ContractCallHandler contractCallHandler, diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java index a1b3cbad6c06..d430b02a29f6 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java @@ -95,6 +95,7 @@ static TransactionHandlers provideTransactionHandlers( consensusHandlers.consensusUpdateTopicHandler(), consensusHandlers.consensusDeleteTopicHandler(), consensusHandlers.consensusSubmitMessageHandler(), + consensusHandlers.consensusApproveAllowanceHandler(), contractHandlers.get().contractCreateHandler(), contractHandlers.get().contractUpdateHandler(), contractHandlers.get().contractCallHandler(), diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java index 86296665353a..9b2b0a286fbf 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java @@ -17,19 +17,29 @@ package com.hedera.node.app.service.consensus.impl.handlers; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; +import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; +import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; -import com.hedera.node.app.service.token.impl.WritableAccountStore; +import com.hedera.node.app.spi.fees.FeeContext; +import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.spi.workflows.PreHandleContext; import com.hedera.node.app.spi.workflows.TransactionHandler; import edu.umd.cs.findbugs.annotations.NonNull; - +import java.util.ArrayList; +import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; @@ -80,8 +90,221 @@ public void pureChecks(@NonNull TransactionBody txn) throws PreCheckException { } @Override - public void handle(@NonNull HandleContext context) throws HandleException { - context.storeFactory().writableStore(WritableAccountStore.class); + public void handle(@NonNull HandleContext handleContext) throws HandleException { + requireNonNull(handleContext, "The argument 'context' must not be null"); + + final var topicStore = handleContext.storeFactory().writableStore(WritableTopicStore.class); + final var op = handleContext.body().consensusApproveAllowanceOrThrow(); + + validator.validateSemantics(handleContext, op, topicStore); + + // Apply all changes to the state modifications. We need to look up payer for each modification, since payer + // would have been modified by a previous allowance change + approveAllowance(handleContext, topicStore); + } + + @NonNull + @Override + public Fees calculateFees(@NonNull final FeeContext feeContext) { + requireNonNull(feeContext); // TODO: Implement this method + return Fees.FREE; + } + + /** + * Apply all changes to the state modifications for crypto and token allowances. + * @param context the handle context + * @param topicStore the topic store + * @throws HandleException if there is an error applying the changes + */ + private void approveAllowance(@NonNull final HandleContext context, @NonNull final WritableTopicStore topicStore) { + requireNonNull(context); + requireNonNull(topicStore); + + final var op = context.body().consensusApproveAllowanceOrThrow(); + final var cryptoAllowances = op.consensusCryptoFeeScheduleAllowances(); + final var tokenAllowances = op.consensusTokenFeeScheduleAllowances(); + + /* --- Apply changes to state --- */ + applyCryptoAllowances(cryptoAllowances, topicStore); + applyFungibleTokenAllowances(tokenAllowances, topicStore); + } + + /** + * Applies all changes needed for Crypto allowances from the transaction. + * If the topic already has an allowance, the allowance value will be replaced with values + * from transaction. If the amount specified is 0, the allowance will be removed. + * @param topicCryptoAllowances the list of crypto allowances + * @param topicStore the topic store + */ + private void applyCryptoAllowances( + @NonNull final List topicCryptoAllowances, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(topicCryptoAllowances); + requireNonNull(topicStore); + + for (final var allowance : topicCryptoAllowances) { + final var ownerId = allowance.owner(); + final var topicId = allowance.topicIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); + + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + + updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); + final var copy = + topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); + + topicStore.put(copy); + } + } + + /** + * Updates the crypto allowance amount if the allowance exists, otherwise adds a new allowance. + * If the amount is zero removes the allowance if it exists in the list. + * @param mutableAllowances the list of mutable allowances of owner + * @param amount the amount + * @param spenderId the spender id + */ + private void updateCryptoAllowance( + final List mutableAllowances, + final long amount, + final long amountPerMessage, + final AccountID spenderId) { + final var newAllowanceBuilder = TopicCryptoAllowance.newBuilder().spenderId(spenderId); + // get the index of the allowance with same spender in existing list + final var index = lookupSpender(mutableAllowances, spenderId); + // If given amount is zero, if the element exists remove it, otherwise do nothing + if (amount == 0) { + if (index != -1) { + // If amount is 0, remove the allowance + mutableAllowances.remove(index); + } + return; + } + if (index != -1) { + mutableAllowances.set( + index, + newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } else { + mutableAllowances.add(newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } + } + + /** + * Applies all changes needed for fungible token allowances from the transaction. If the key + * {token, spender} already has an allowance, the allowance value will be replaced with values + * from transaction. + * @param tokenAllowances the list of token allowances + * @param topicStore the topic store + */ + private void applyFungibleTokenAllowances( + @NonNull final List tokenAllowances, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(tokenAllowances); + requireNonNull(topicStore); + + for (final var allowance : tokenAllowances) { + final var ownerId = allowance.owner(); + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + final var tokenId = allowance.tokenIdOrThrow(); + final var topicId = allowance.topicIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + + final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); + + updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); + final var copy = + topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); + + topicStore.put(copy); + } + } + + /** + * Updates the token allowance amount if the allowance for given tokenNuma dn spenderNum exists, + * otherwise adds a new allowance. + * If the amount is zero removes the allowance if it exists in the list + * @param mutableAllowances the list of mutable allowances of owner + * @param amount the amount + * @param spenderId the spender number + * @param tokenId the token number + */ + private void updateTokenAllowance( + final List mutableAllowances, + final long amount, + final long amountPerMessage, + final AccountID spenderId, + final TokenID tokenId) { + final var newAllowanceBuilder = + TopicFungibleTokenAllowance.newBuilder().spenderId(spenderId).tokenId(tokenId); + final var index = lookupSpenderAndToken(mutableAllowances, spenderId, tokenId); + // If given amount is zero, if the element exists remove it + if (amount == 0) { + if (index != -1) { + mutableAllowances.remove(index); + } + return; + } + if (index != -1) { + mutableAllowances.set( + index, + newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } else { + mutableAllowances.add(newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } + } + + /** + * Returns the index of the allowance with the given spender in the list if it exists, + * otherwise returns -1. + * @param topicCryptoAllowances list of allowances + * @param spenderNum spender account number + * @return index of the allowance if it exists, otherwise -1 + */ + private int lookupSpender(final List topicCryptoAllowances, final AccountID spenderNum) { + for (int i = 0; i < topicCryptoAllowances.size(); i++) { + final var allowance = topicCryptoAllowances.get(i); + if (allowance.spenderIdOrThrow().equals(spenderNum)) { + return i; + } + } + return -1; + } + + /** + * Returns the index of the allowance with the given spender and token in the list if it exists, + * otherwise returns -1. + * @param topicTokenAllowances list of allowances + * @param spenderId spender account number + * @param tokenId token number + * @return index of the allowance if it exists, otherwise -1 + */ + private int lookupSpenderAndToken( + final List topicTokenAllowances, + final AccountID spenderId, + final TokenID tokenId) { + for (int i = 0; i < topicTokenAllowances.size(); i++) { + final var allowance = topicTokenAllowances.get(i); + if (allowance.spenderIdOrThrow().equals(spenderId) + && allowance.tokenIdOrThrow().equals(tokenId)) { + return i; + } + } + return -1; } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java index 84cc13ab8897..87b130a70ea6 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java @@ -37,6 +37,8 @@ public class ConsensusHandlers { private final ConsensusUpdateTopicHandler consensusUpdateTopicHandler; + private final ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler; + /** * Constructor for ConsensusHandlers. */ @@ -46,7 +48,8 @@ public ConsensusHandlers( @NonNull final ConsensusDeleteTopicHandler consensusDeleteTopicHandler, @NonNull final ConsensusGetTopicInfoHandler consensusGetTopicInfoHandler, @NonNull final ConsensusSubmitMessageHandler consensusSubmitMessageHandler, - @NonNull final ConsensusUpdateTopicHandler consensusUpdateTopicHandler) { + @NonNull final ConsensusUpdateTopicHandler consensusUpdateTopicHandler, + @NonNull final ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler) { this.consensusCreateTopicHandler = Objects.requireNonNull(consensusCreateTopicHandler, "consensusCreateTopicHandler must not be null"); this.consensusDeleteTopicHandler = @@ -57,6 +60,8 @@ public ConsensusHandlers( Objects.requireNonNull(consensusSubmitMessageHandler, "consensusSubmitMessageHandler must not be null"); this.consensusUpdateTopicHandler = Objects.requireNonNull(consensusUpdateTopicHandler, "consensusUpdateTopicHandler must not be null"); + this.consensusApproveAllowanceHandler = Objects.requireNonNull( + consensusApproveAllowanceHandler, "consensusApproveAllowanceHandler must not be null"); } /** @@ -103,4 +108,13 @@ public ConsensusSubmitMessageHandler consensusSubmitMessageHandler() { public ConsensusUpdateTopicHandler consensusUpdateTopicHandler() { return consensusUpdateTopicHandler; } + + /** + * Get the consensusApproveAllowanceHandler. + * + * @return the consensusApproveAllowanceHandler + */ + public ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler() { + return consensusApproveAllowanceHandler; + } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java new file mode 100644 index 000000000000..b228ebd4ff82 --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.util; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TOPIC_DELETED; +import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.state.consensus.Topic; +import com.hedera.node.app.service.consensus.ReadableTopicStore; +import com.hedera.node.app.spi.workflows.HandleException; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * Class for retrieving objects in a certain context. For example, during a {@code handler.handle(...)} call. + * This allows compartmentalizing common validation logic without requiring store implementations to + * throw inappropriately-contextual exceptions, and also abstracts duplicated business logic out of + * multiple handlers. + */ +public class ConsensusHandlerHelper { + + private ConsensusHandlerHelper() { + throw new UnsupportedOperationException("Utility class only"); + } + + /** + * Returns the topic if it exists and is usable. A {@link HandleException} is thrown if the topic is invalid. + * + * @param topicId the ID of the topic to get + * @param topicStore the {@link ReadableTopicStore} to use for topic retrieval + * @return the topic if it exists and is usable + * @throws HandleException if any of the topic conditions are not met + */ + @NonNull + public static Topic getIfUsable(@NonNull final TopicID topicId, @NonNull final ReadableTopicStore topicStore) { + requireNonNull(topicId); + requireNonNull(topicStore); + + final var topic = topicStore.getTopic(topicId); + validateTrue(topic != null, INVALID_TOPIC_ID); + validateFalse(topic.deleted(), TOPIC_DELETED); + return topic; + } +} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java index 130786d9dede..68f80e6c0bc4 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java @@ -17,22 +17,39 @@ package com.hedera.node.app.service.consensus.impl.validators; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_SPENDER_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TOPIC_DELETED; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; import static com.hedera.node.app.spi.validation.Validations.mustExist; +import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; +import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.base.TokenType; import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.token.ConsensusApproveAllowanceTransactionBody; +import com.hedera.node.app.service.consensus.ReadableTopicStore; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.ReadableTokenRelationStore; +import com.hedera.node.app.service.token.ReadableTokenStore; +import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreCheckException; - -import javax.inject.Inject; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import java.util.HashMap; import java.util.List; +import javax.inject.Inject; public class ConsensusAllowancesValidator { @@ -54,6 +71,72 @@ public void pureChecks(ConsensusApproveAllowanceTransactionBody op) throws PreCh validateTokenAllowances(tokenAllowances); } + public void validateSemantics( + @NonNull final HandleContext context, + @NonNull final ConsensusApproveAllowanceTransactionBody op, + @NonNull final ReadableTopicStore topicStore) { + final var accountStore = context.storeFactory().readableStore(ReadableAccountStore.class); + final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); + final var tokenRelStore = context.storeFactory().readableStore(ReadableTokenRelationStore.class); + + for (final var cryptoAllowance : op.consensusCryptoFeeScheduleAllowances()) { + final var topicId = cryptoAllowance.topicIdOrThrow(); + final var ownerId = cryptoAllowance.ownerOrThrow(); + + // validate spender account + final var spenderAccount = accountStore.getAccountById(ownerId); + validateSpender(cryptoAllowance.amount(), spenderAccount); + validateTopic(topicId, topicStore); + } + + for (var tokenAllowance : op.consensusTokenFeeScheduleAllowances()) { + final var topicId = tokenAllowance.topicIdOrThrow(); + final var ownerId = tokenAllowance.ownerOrThrow(); + final var tokenId = tokenAllowance.tokenIdOrThrow(); + + final var token = tokenStore.get(tokenId); + // check if token exists + validateTrue(token != null, INVALID_TOKEN_ID); + + // validate spender account + final var spenderAccount = accountStore.getAccountById(ownerId); + validateTrue(TokenType.FUNGIBLE_COMMON.equals(token.tokenType()), NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES); + + // validate token amount + final var amount = tokenAllowance.amount(); + validateSpender(amount, spenderAccount); + final var relation = tokenRelStore.get(ownerId, tokenId); + validateTrue(relation != null, TOKEN_NOT_ASSOCIATED_TO_ACCOUNT); + + validateTopic(topicId, topicStore); + } + } + + /** + * Validates that either the amount to be approved is 0, or the spender account actually exists and has not been + * deleted. + * + * @param amount If 0, then always valid. Otherwise, we check the spender account. + * @param spenderAccount If the amount is not zero, then this must be non-null and not deleted. + */ + private void validateSpender(final long amount, @Nullable final Account spenderAccount) { + validateTrue( + amount == 0 || (spenderAccount != null && !spenderAccount.deleted()), INVALID_ALLOWANCE_SPENDER_ID); + } + + /** + * Validates that the topic exists and has not been deleted. + * + * @param topicID Validates that this is non-null and not deleted. + */ + private void validateTopic(@Nullable final TopicID topicID, @NonNull final ReadableTopicStore topicStore) { + requireNonNull(topicStore); + + validateTrue(topicID != null, INVALID_TOPIC_ID); + final var topic = getIfUsable(topicID, topicStore); + validateFalse(topic.deleted(), TOPIC_DELETED); + } + private static void validateCryptoAllowances(List cryptoAllowances) throws PreCheckException { final var uniqueMap = new HashMap(); @@ -76,6 +159,7 @@ private static void validateCryptoAllowances(List tokenAllowances) throws PreCheckException { + // TODO: add check for tokenID final var uniqueMap = new HashMap(); for (var tokenAllowance : tokenAllowances) { // Check if a given AccountId/TopicId pair already exists in the token allowances list diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java index 33e7ca5250a8..a85226b0c63e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; +import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusDeleteTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusGetTopicInfoHandler; @@ -34,6 +35,7 @@ class ConsensusHandlersTest { private ConsensusGetTopicInfoHandler consensusGetTopicInfoHandler; private ConsensusSubmitMessageHandler consensusSubmitMessageHandler; private ConsensusUpdateTopicHandler consensusUpdateTopicHandler; + private ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler; private ConsensusHandlers consensusHandlers; @@ -44,13 +46,15 @@ public void setUp() { consensusGetTopicInfoHandler = mock(ConsensusGetTopicInfoHandler.class); consensusSubmitMessageHandler = mock(ConsensusSubmitMessageHandler.class); consensusUpdateTopicHandler = mock(ConsensusUpdateTopicHandler.class); + consensusApproveAllowanceHandler = mock(ConsensusApproveAllowanceHandler.class); consensusHandlers = new ConsensusHandlers( consensusCreateTopicHandler, consensusDeleteTopicHandler, consensusGetTopicInfoHandler, consensusSubmitMessageHandler, - consensusUpdateTopicHandler); + consensusUpdateTopicHandler, + consensusApproveAllowanceHandler); } @Test @@ -92,4 +96,12 @@ void consensusUpdateTopicHandlerReturnsCorrectInstance() { consensusHandlers.consensusUpdateTopicHandler(), "consensusUpdateTopicHandler does not return correct instance"); } + + @Test + void consensusApproveAllowanceHandlerReturnsCorrectInstance() { + assertEquals( + consensusApproveAllowanceHandler, + consensusHandlers.consensusApproveAllowanceHandler(), + "consensusApproveAllowanceHandler does not return correct instance"); + } } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java index de08bb0526a6..9e5bd693c7c5 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java @@ -1,3 +1,19 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.hedera.node.app.service.consensus.impl.test.validators; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; @@ -18,7 +34,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; - class ConsensusAllowancesValidatorTest { private ConsensusAllowancesValidator validator; @@ -160,4 +175,3 @@ void negativeAmountsThrows() { assertEquals(NEGATIVE_ALLOWANCE_AMOUNT, exception.responseCode()); } } - diff --git a/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java b/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java index 6071781e3196..66ec9fb2884c 100644 --- a/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java +++ b/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java @@ -82,7 +82,13 @@ public final class ConsensusServiceDefinition implements RpcServiceDefinition { // topicRunningHash. // Request is [ConsensusSubmitMessageTransactionBody](#proto.ConsensusSubmitMessageTransactionBody) // - new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class)); + new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class), + // + // Approve allowance for custom fees. + // Set account allowances for a topic. This includes total allowance and allowance per message. + // Request is [ConsensusApproveAllowanceTransactionBody](#proto.ConsensusApproveAllowanceTransactionBody) + // + new RpcMethodDefinition<>("approveAllowance", Transaction.class, TransactionResponse.class)); private ConsensusServiceDefinition() { // Just something to keep the Gradle build believing we have a non-transitive diff --git a/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java b/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java index 77fd56a05dcb..9f7cb1725cee 100644 --- a/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java +++ b/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java @@ -40,6 +40,7 @@ void methodsDefined() { new RpcMethodDefinition<>("updateTopic", Transaction.class, TransactionResponse.class), new RpcMethodDefinition<>("deleteTopic", Transaction.class, TransactionResponse.class), new RpcMethodDefinition<>("getTopicInfo", Query.class, Response.class), - new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class)); + new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class), + new RpcMethodDefinition<>("approveAllowance", Transaction.class, TransactionResponse.class)); } } diff --git a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json index 3585961b5488..f5ae5709e98a 100644 --- a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json +++ b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json @@ -21,6 +21,7 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", + "ConsensusApproveAllowance", "TokenGetInfo", "TokenGetNftInfo", "TokenGetNftInfos", diff --git a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json index 753f3c656931..76b451d98ed9 100644 --- a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json +++ b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json @@ -21,6 +21,7 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", + "ConsensusApproveAllowance", "TokenGetNftInfo", "TokenGetInfo", "ScheduleDelete", diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java index 7eecf09f69b4..646a4daee6f1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java @@ -151,6 +151,8 @@ public static TransactionResponse submit( .deleteTopic(transaction); case ConsensusSubmitMessage -> clients.getConsSvcStub(nodeAccountId, false) .submitMessage(transaction); + case ConsensusApproveAllowance -> clients.getConsSvcStub(nodeAccountId, false) + .approveAllowance(transaction); case UncheckedSubmit -> clients.getNetworkSvcStub(nodeAccountId, false) .uncheckedSubmit(transaction); case TokenCreate -> clients.getTokenSvcStub(nodeAccountId, false).createToken(transaction); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java index a936867ee309..d1ae183bd0ef 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java @@ -25,6 +25,7 @@ import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.HapiSpecSetup; import com.hedera.services.bdd.spec.utilops.mod.BodyMutation; +import com.hederahashgraph.api.proto.java.ConsensusApproveAllowanceTransactionBody; import com.hederahashgraph.api.proto.java.ConsensusCreateTopicTransactionBody; import com.hederahashgraph.api.proto.java.ConsensusDeleteTopicTransactionBody; import com.hederahashgraph.api.proto.java.ConsensusSubmitMessageTransactionBody; @@ -462,4 +463,9 @@ public Consumer defaultDefTokenClaimAi public Consumer defaultDefTokenAirdropTransactionBody() { return builder -> {}; } + + public Consumer + defaultDefConsensusApproveAllowanceTransactionBody() { + return builder -> {}; + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java index b1dd836bf42e..d00e98933ace 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java @@ -48,6 +48,7 @@ import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.queries.crypto.ReferenceType; import com.hedera.services.bdd.spec.transactions.consensus.HapiMessageSubmit; +import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicApproveAllowance; import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicCreate; import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicDelete; import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicUpdate; @@ -230,6 +231,10 @@ public static HapiMessageSubmit submitMessageTo(Function topi return new HapiMessageSubmit(topicFn); } + public static HapiTopicApproveAllowance approveTopicAllowance() { + return new HapiTopicApproveAllowance(); + } + /* FILE */ public static HapiFileCreate fileCreate(String fileName) { return new HapiFileCreate(fileName); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java new file mode 100644 index 000000000000..2861b9054fdb --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.spec.transactions.consensus; + +import static com.hedera.services.bdd.spec.transactions.TxnUtils.asId; +import static com.hedera.services.bdd.spec.transactions.TxnUtils.asTokenId; +import static com.hedera.services.bdd.spec.transactions.TxnUtils.asTopicId; + +import com.google.common.base.MoreObjects; +import com.hedera.services.bdd.spec.HapiSpec; +import com.hedera.services.bdd.spec.transactions.HapiTxnOp; +import com.hederahashgraph.api.proto.java.ConsensusApproveAllowanceTransactionBody; +import com.hederahashgraph.api.proto.java.ConsensusCryptoFeeScheduleAllowance; +import com.hederahashgraph.api.proto.java.ConsensusTokenFeeScheduleAllowance; +import com.hederahashgraph.api.proto.java.HederaFunctionality; +import com.hederahashgraph.api.proto.java.Transaction; +import com.hederahashgraph.api.proto.java.TransactionBody; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class HapiTopicApproveAllowance extends HapiTxnOp { + private final List cryptoAllowances = new ArrayList<>(); + private final List tokenAllowances = new ArrayList<>(); + + public HapiTopicApproveAllowance() {} + + @Override + public HederaFunctionality type() { + return HederaFunctionality.ConsensusApproveAllowance; + } + + @Override + protected HapiTopicApproveAllowance self() { + return this; + } + + public HapiTopicApproveAllowance addCryptoAllowance( + String owner, String topic, long allowance, long allowancePerMessage) { + cryptoAllowances.add(CryptoAllowances.from(owner, topic, allowance, allowancePerMessage)); + return this; + } + + public HapiTopicApproveAllowance addTokenAllowance( + String owner, String token, String topic, long allowance, long allowancePerMessage) { + tokenAllowances.add(TokenAllowances.from(owner, token, topic, allowance, allowancePerMessage)); + return this; + } + + @Override + protected Consumer opBodyDef(HapiSpec spec) throws Throwable { + List callowances = new ArrayList<>(); + List tallowances = new ArrayList<>(); + calculateAllowances(spec, callowances, tallowances); + ConsensusApproveAllowanceTransactionBody opBody = spec.txns() + .body( + ConsensusApproveAllowanceTransactionBody.class, b -> { + b.addAllConsensusCryptoFeeScheduleAllowances(callowances); + b.addAllConsensusTokenFeeScheduleAllowances(tallowances); + }); + return b -> b.setConsensusApproveAllowance(opBody); + } + + @Override + protected long feeFor(HapiSpec spec, Transaction txn, int numPayerKeys) throws Throwable { + return 0; + } + + @Override + protected MoreObjects.ToStringHelper toStringHelper() { + return super.toStringHelper().add("cryptoAllowances", cryptoAllowances).add("tokenAllowances", tokenAllowances); + } + + // @Override + // protected void updateStateOf(HapiSpec spec) { + // // No state changes + // } + + private void calculateAllowances( + final HapiSpec spec, + final List callowances, + final List tallowances) { + for (var entry : cryptoAllowances) { + final var builder = ConsensusCryptoFeeScheduleAllowance.newBuilder() + .setOwner(asId(entry.owner(), spec)) + .setAmount(entry.amount()) + .setAmountPerMessage(entry.amountPerMessage()) + .setTopicId(asTopicId(entry.topic(), spec)); + callowances.add(builder.build()); + } + + for (var entry : tokenAllowances) { + final var builder = ConsensusTokenFeeScheduleAllowance.newBuilder() + .setOwner(asId(entry.owner, spec)) + .setTokenId(asTokenId(entry.token, spec)) + .setTopicId(asTopicId(entry.topic, spec)) + .setAmount(entry.amount) + .setAmountPerMessage(entry.amountPerMessage); + tallowances.add(builder.build()); + } + } + + private record CryptoAllowances(String owner, String topic, Long amount, Long amountPerMessage) { + static CryptoAllowances from(String owner, String topic, Long amount, Long amountPerMessage) { + return new CryptoAllowances(owner, topic, amount, amountPerMessage); + } + } + + private record TokenAllowances(String owner, String token, String topic, Long amount, Long amountPerMessage) { + static TokenAllowances from(String owner, String token, String topic, Long amount, Long amountPerMessage) { + return new TokenAllowances(owner, token, topic, amount, amountPerMessage); + } + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index bce2af1837a3..7bbfca58f048 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -25,6 +25,8 @@ public class TopicCustomFeeBase { protected static final String TOPIC = "topic"; + protected static final String OWNER = "owner"; + protected static final String FUNGIBLE_TOKEN = "fungibleToken"; protected static final String ADMIN_KEY = "adminKey"; protected static final String SUBMIT_KEY = "submitKey"; protected static final String FEE_SCHEDULE_KEY = "feeScheduleKey"; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 1bc10812182d..18e6803be8d4 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -18,6 +18,7 @@ import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; @@ -254,4 +255,31 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { } } } + + @Nested + @DisplayName("Topic approve allowance") + class TopicApproveAllowance { + + @Nested + @DisplayName("Positive scenarios") + class ApproveAllowancePositiveScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } + + @HapiTest + @DisplayName("Approve crypto allowance for topic") + final Stream createTopicWithAllKeys() { + return hapiTest( + cryptoCreate(OWNER), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY), + approveTopicAllowance().payingWith(OWNER).addCryptoAllowance(OWNER, TOPIC, 100, 10)); + } + } + } } From 6633fb8d2ca27e6bd4f00e55c047c7bfa46f7f73 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 26 Sep 2024 16:31:49 +0300 Subject: [PATCH 33/94] fix scope Signed-off-by: ibankov --- .../node/app/services/ServiceScopeLookup.java | 3 +- .../handle/HandleWorkflowModuleTest.java | 5 +++ .../ConsensusAllowancesValidator.java | 35 +++++++++++++------ ... => ConsensusAllowancesValidatorTest.java} | 0 .../bdd/suites/hip991/TopicCustomFeeTest.java | 2 +- 5 files changed, 33 insertions(+), 12 deletions(-) rename hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/{ConsensusAllowanceValidatorTest.java => ConsensusAllowancesValidatorTest.java} (100%) diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java index 46b9ae01faaf..e64082a56f34 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java @@ -60,7 +60,8 @@ public String getServiceName(@NonNull final TransactionBody txBody) { case CONSENSUS_CREATE_TOPIC, CONSENSUS_UPDATE_TOPIC, CONSENSUS_DELETE_TOPIC, - CONSENSUS_SUBMIT_MESSAGE -> ConsensusService.NAME; + CONSENSUS_SUBMIT_MESSAGE, + CONSENSUS_APPROVE_ALLOWANCE -> ConsensusService.NAME; case CONTRACT_CREATE_INSTANCE, CONTRACT_UPDATE_INSTANCE, diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java index 1b2191338f68..410337918c72 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java @@ -23,6 +23,7 @@ import com.hedera.node.app.service.addressbook.impl.handlers.NodeCreateHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeDeleteHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeUpdateHandler; +import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusDeleteTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusHandlers; @@ -120,6 +121,9 @@ class HandleWorkflowModuleTest { @Mock private ConsensusSubmitMessageHandler consensusSubmitMessageHandler; + @Mock + private ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler; + @Mock private ContractCreateHandler contractCreateHandler; @@ -261,6 +265,7 @@ void usesComponentsToGetHandlers() { given(consensusHandlers.consensusUpdateTopicHandler()).willReturn(consensusUpdateTopicHandler); given(consensusHandlers.consensusDeleteTopicHandler()).willReturn(consensusDeleteTopicHandler); given(consensusHandlers.consensusSubmitMessageHandler()).willReturn(consensusSubmitMessageHandler); + given(consensusHandlers.consensusApproveAllowanceHandler()).willReturn(consensusApproveAllowanceHandler); given(contractHandlers.contractCreateHandler()).willReturn(contractCreateHandler); given(contractHandlers.contractUpdateHandler()).willReturn(contractUpdateHandler); given(contractHandlers.contractCallHandler()).willReturn(contractCallHandler); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java index 68f80e6c0bc4..2aa9f53bd605 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java @@ -35,6 +35,7 @@ import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenType; import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.state.token.Account; @@ -49,6 +50,7 @@ import edu.umd.cs.findbugs.annotations.Nullable; import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.inject.Inject; public class ConsensusAllowancesValidator { @@ -159,23 +161,36 @@ private static void validateCryptoAllowances(List tokenAllowances) throws PreCheckException { - // TODO: add check for tokenID - final var uniqueMap = new HashMap(); + final var uniqueMap = new HashMap>(); + for (var tokenAllowance : tokenAllowances) { - // Check if a given AccountId/TopicId pair already exists in the token allowances list - validateFalsePreCheck( - uniqueMap.containsKey(tokenAllowance.owner()) - && uniqueMap.get(tokenAllowance.owner()).equals(tokenAllowance.topicId()), - ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); + final var accountId = tokenAllowance.owner(); + final var topicId = tokenAllowance.topicId(); + final var tokenId = tokenAllowance.tokenId(); + mustExist(tokenId, INVALID_TOKEN_ID); + // Retrieve the map of AccountID -> TokenID for the given TopicID + final var accountTokenMap = uniqueMap.get(topicId); + + // If the TopicID already has an AccountID -> TokenID map, check if the AccountID exists + if (accountTokenMap != null && accountTokenMap.containsKey(accountId)) { + // If the AccountID exists, check if the TokenID matches + validateFalsePreCheck( + accountTokenMap.get(accountId).equals(tokenId), + ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); + } else { + // If the TopicID or AccountID does not exist, create the entry + uniqueMap.putIfAbsent(topicId, new HashMap<>()); + } + + // Add or update the (AccountID, TokenID) pair for the TopicID + uniqueMap.get(topicId).put(accountId, tokenId); + // Validate the allowance amount and amount per message validateTruePreCheck(tokenAllowance.amount() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck( tokenAllowance.amount() > tokenAllowance.amountPerMessage(), ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); - mustExist(tokenAllowance.tokenId(), INVALID_TOKEN_ID); - // Add the unique (AccountID, TopicID) pair to the map - uniqueMap.put(tokenAllowance.owner(), tokenAllowance.topicId()); } } } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java similarity index 100% rename from hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowanceValidatorTest.java rename to hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 18e6803be8d4..3a382e899577 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -271,7 +271,7 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { @HapiTest @DisplayName("Approve crypto allowance for topic") - final Stream createTopicWithAllKeys() { + final Stream approveAllowance() { return hapiTest( cryptoCreate(OWNER), createTopic(TOPIC) From 32b35c355debcd5273a25e1ff779df0b0c4674df Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 26 Sep 2024 17:15:02 +0300 Subject: [PATCH 34/94] add some unit tests Signed-off-by: ibankov --- .../ConsensusAllowancesValidator.java | 21 ++++--- .../ConsensusAllowancesValidatorTest.java | 62 +++++++++++++++++++ 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java index 2aa9f53bd605..723b05412fc9 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java @@ -16,11 +16,14 @@ package com.hedera.node.app.service.consensus.impl.validators; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_SPENDER_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.TOPIC_DELETED; import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; @@ -34,7 +37,6 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenType; import com.hedera.hapi.node.base.TopicID; @@ -147,13 +149,13 @@ private static void validateCryptoAllowances(List= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck(hbarAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(hbarAllowance.amount() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(hbarAllowance.amountPerMessage() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck( hbarAllowance.amount() > hbarAllowance.amountPerMessage(), - ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); + ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); // Add the unique (AccountID, TopicID) pair to the map uniqueMap.put(hbarAllowance.owner(), hbarAllowance.topicId()); } @@ -175,8 +177,7 @@ private static void validateTokenAllowances(List()); @@ -186,11 +187,11 @@ private static void validateTokenAllowances(List= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(tokenAllowance.amount() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); + validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck( tokenAllowance.amount() > tokenAllowance.amountPerMessage(), - ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); + ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); } } } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java index 9e5bd693c7c5..57dcd8d27243 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.consensus.impl.test.validators; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT; @@ -80,6 +81,34 @@ void emptyAllowancesThrows() { assertEquals(exception.responseCode(), EMPTY_ALLOWANCES); } + @Test + void testRepeatedAllowanceForSameAccountButDifferentTopic() throws PreCheckException { + // Arrange: Create allowances with the same AccountID but different TopicIDs + var tokenAllowance1 = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.newBuilder().topicNum(1).build()) + .amount(100L) + .amountPerMessage(10L) + .tokenId(TokenID.DEFAULT) + .build(); + + var tokenAllowance2 = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.newBuilder().topicNum(2).build()) + .amount(200L) + .amountPerMessage(20L) + .tokenId(TokenID.DEFAULT) + .build(); + + // Build the operation containing the token allowances + var op = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusTokenFeeScheduleAllowances(tokenAllowance1, tokenAllowance2) + .build(); + + // Act & Assert: Should pass without exception + validator.pureChecks(op); + } + @Test void repeatedTokenAllowancesThrows() { // Arrange: Create two token allowances with the same owner and topicId @@ -174,4 +203,37 @@ void negativeAmountsThrows() { var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); assertEquals(NEGATIVE_ALLOWANCE_AMOUNT, exception.responseCode()); } + + @Test + void amountPerMessageExceedsTotal() { + // Arrange + var cryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .amount(10) + .amountPerMessage(15) + .build(); + var tokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(AccountID.DEFAULT) + .topicId(TopicID.DEFAULT) + .tokenId(TokenID.DEFAULT) + .amount(10) + .amountPerMessage(15) + .build(); + + var op = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusCryptoFeeScheduleAllowances(cryptoAllowance) + .build(); + + var tokenOp = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusTokenFeeScheduleAllowances(tokenAllowance) + .build(); + + // Act & Assert: Should throw PreCheckException with ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE code + var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); + assertEquals(ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE, exception.responseCode()); + + var tokenException = assertThrows(PreCheckException.class, () -> validator.pureChecks(tokenOp)); + assertEquals(ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE, tokenException.responseCode()); + } } From 9c1f8e9a235797273a327485d0d3b966f5fa43bd Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 27 Sep 2024 17:06:41 +0300 Subject: [PATCH 35/94] submit msg Signed-off-by: Zhivko Kelchev --- .../node/app/services/ServiceScopeLookup.java | 1 + .../ConsensusApproveAllowanceHandler.java | 189 +------------ .../ConsensusSubmitMessageHandler.java | 37 +++ .../util/ConsensusApproveAllowanceHelper.java | 258 ++++++++++++++++++ .../impl/util/ConsensusCustomFeeHelper.java | 195 +++++++++++++ .../services/bdd/spec/keys/KeyFactory.java | 30 +- .../bdd/suites/hip991/TopicCustomFeeTest.java | 104 +++++++ 7 files changed, 622 insertions(+), 192 deletions(-) create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java index 46b9ae01faaf..8ce11d31ea91 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java @@ -60,6 +60,7 @@ public String getServiceName(@NonNull final TransactionBody txBody) { case CONSENSUS_CREATE_TOPIC, CONSENSUS_UPDATE_TOPIC, CONSENSUS_DELETE_TOPIC, + CONSENSUS_APPROVE_ALLOWANCE, CONSENSUS_SUBMIT_MESSAGE -> ConsensusService.NAME; case CONTRACT_CREATE_INSTANCE, diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java index 9b2b0a286fbf..c5f40a89ed12 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java @@ -17,16 +17,11 @@ package com.hedera.node.app.service.consensus.impl.handlers; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyCryptoAllowances; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyFungibleTokenAllowances; import static java.util.Objects.requireNonNull; -import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; -import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.base.TokenID; -import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; -import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; @@ -38,8 +33,6 @@ import com.hedera.node.app.spi.workflows.PreHandleContext; import com.hedera.node.app.spi.workflows.TransactionHandler; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.ArrayList; -import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; @@ -129,182 +122,4 @@ private void approveAllowance(@NonNull final HandleContext context, @NonNull fin applyCryptoAllowances(cryptoAllowances, topicStore); applyFungibleTokenAllowances(tokenAllowances, topicStore); } - - /** - * Applies all changes needed for Crypto allowances from the transaction. - * If the topic already has an allowance, the allowance value will be replaced with values - * from transaction. If the amount specified is 0, the allowance will be removed. - * @param topicCryptoAllowances the list of crypto allowances - * @param topicStore the topic store - */ - private void applyCryptoAllowances( - @NonNull final List topicCryptoAllowances, - @NonNull final WritableTopicStore topicStore) { - requireNonNull(topicCryptoAllowances); - requireNonNull(topicStore); - - for (final var allowance : topicCryptoAllowances) { - final var ownerId = allowance.owner(); - final var topicId = allowance.topicIdOrThrow(); - final var topic = getIfUsable(topicId, topicStore); - final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); - - final var amount = allowance.amount(); - final var amountPerMessage = allowance.amountPerMessage(); - - updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); - final var copy = - topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); - - topicStore.put(copy); - } - } - - /** - * Updates the crypto allowance amount if the allowance exists, otherwise adds a new allowance. - * If the amount is zero removes the allowance if it exists in the list. - * @param mutableAllowances the list of mutable allowances of owner - * @param amount the amount - * @param spenderId the spender id - */ - private void updateCryptoAllowance( - final List mutableAllowances, - final long amount, - final long amountPerMessage, - final AccountID spenderId) { - final var newAllowanceBuilder = TopicCryptoAllowance.newBuilder().spenderId(spenderId); - // get the index of the allowance with same spender in existing list - final var index = lookupSpender(mutableAllowances, spenderId); - // If given amount is zero, if the element exists remove it, otherwise do nothing - if (amount == 0) { - if (index != -1) { - // If amount is 0, remove the allowance - mutableAllowances.remove(index); - } - return; - } - if (index != -1) { - mutableAllowances.set( - index, - newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } else { - mutableAllowances.add(newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } - } - - /** - * Applies all changes needed for fungible token allowances from the transaction. If the key - * {token, spender} already has an allowance, the allowance value will be replaced with values - * from transaction. - * @param tokenAllowances the list of token allowances - * @param topicStore the topic store - */ - private void applyFungibleTokenAllowances( - @NonNull final List tokenAllowances, - @NonNull final WritableTopicStore topicStore) { - requireNonNull(tokenAllowances); - requireNonNull(topicStore); - - for (final var allowance : tokenAllowances) { - final var ownerId = allowance.owner(); - final var amount = allowance.amount(); - final var amountPerMessage = allowance.amountPerMessage(); - final var tokenId = allowance.tokenIdOrThrow(); - final var topicId = allowance.topicIdOrThrow(); - final var topic = getIfUsable(topicId, topicStore); - - final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); - - updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); - final var copy = - topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); - - topicStore.put(copy); - } - } - - /** - * Updates the token allowance amount if the allowance for given tokenNuma dn spenderNum exists, - * otherwise adds a new allowance. - * If the amount is zero removes the allowance if it exists in the list - * @param mutableAllowances the list of mutable allowances of owner - * @param amount the amount - * @param spenderId the spender number - * @param tokenId the token number - */ - private void updateTokenAllowance( - final List mutableAllowances, - final long amount, - final long amountPerMessage, - final AccountID spenderId, - final TokenID tokenId) { - final var newAllowanceBuilder = - TopicFungibleTokenAllowance.newBuilder().spenderId(spenderId).tokenId(tokenId); - final var index = lookupSpenderAndToken(mutableAllowances, spenderId, tokenId); - // If given amount is zero, if the element exists remove it - if (amount == 0) { - if (index != -1) { - mutableAllowances.remove(index); - } - return; - } - if (index != -1) { - mutableAllowances.set( - index, - newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } else { - mutableAllowances.add(newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } - } - - /** - * Returns the index of the allowance with the given spender in the list if it exists, - * otherwise returns -1. - * @param topicCryptoAllowances list of allowances - * @param spenderNum spender account number - * @return index of the allowance if it exists, otherwise -1 - */ - private int lookupSpender(final List topicCryptoAllowances, final AccountID spenderNum) { - for (int i = 0; i < topicCryptoAllowances.size(); i++) { - final var allowance = topicCryptoAllowances.get(i); - if (allowance.spenderIdOrThrow().equals(spenderNum)) { - return i; - } - } - return -1; - } - - /** - * Returns the index of the allowance with the given spender and token in the list if it exists, - * otherwise returns -1. - * @param topicTokenAllowances list of allowances - * @param spenderId spender account number - * @param tokenId token number - * @return index of the allowance if it exists, otherwise -1 - */ - private int lookupSpenderAndToken( - final List topicTokenAllowances, - final AccountID spenderId, - final TokenID tokenId) { - for (int i = 0; i < topicTokenAllowances.size(); i++) { - final var allowance = topicTokenAllowances.get(i); - if (allowance.spenderIdOrThrow().equals(spenderId) - && allowance.tokenIdOrThrow().equals(tokenId)) { - return i; - } - } - return -1; - } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 5e2cc16d4324..8c306907b265 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -23,11 +23,13 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_MESSAGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION; import static com.hedera.hapi.node.base.ResponseCodeEnum.MESSAGE_SIZE_TOO_LARGE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.BASIC_ENTITY_ID_SIZE; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.LONG_SIZE; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.RECEIPT_STORAGE_TIME_SEC; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.TX_HASH_SIZE; import static com.hedera.node.app.spi.validation.Validations.mustExist; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; @@ -44,6 +46,7 @@ import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; +import com.hedera.node.app.service.consensus.impl.util.ConsensusCustomFeeHelper; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; @@ -61,6 +64,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.util.HashSet; import javax.inject.Inject; import javax.inject.Singleton; @@ -99,6 +103,12 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx if (topic.hasSubmitKey()) { context.requireKeyOrThrow(topic.submitKeyOrThrow(), INVALID_SUBMIT_KEY); } + // add optional fee exempt keys in to the key verifieer + // later it will be used to validate if transaction was signed by + // any of these keys and based on that, custom fees will be charged or not + if (!topic.feeExemptKeyList().isEmpty()) { + context.optionalKeys(new HashSet<>(topic.feeExemptKeyList())); + } } /** @@ -122,6 +132,33 @@ public void handle(@NonNull final HandleContext handleContext) { final var config = handleContext.configuration().getConfigData(ConsensusConfig.class); validateTransaction(txn, config, topic); + /* handle custom fees */ + // check if payer is fee exempt + var payerIsFeeExempted = false; + if (!topic.feeExemptKeyList().isEmpty()) { + for (final var key : topic.feeExemptKeyList()) { + final var keyVerificationResult = handleContext.keyVerifier().verificationFor(key); + if (keyVerificationResult.passed()) { + payerIsFeeExempted = true; + } + } + } + if (!topic.customFees().isEmpty() && !payerIsFeeExempted) { + // validate and create synthetic body + final var syntheticBody = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); + // dispatch crypto transfer + var record = handleContext.dispatchChildTransaction( + TransactionBody.newBuilder().cryptoTransfer(syntheticBody).build(), + ConsensusSubmitMessageStreamBuilder.class, + null, + handleContext.payer(), + HandleContext.TransactionCategory.CHILD, + HandleContext.ConsensusThrottling.OFF); + validateTrue(record.status().equals(SUCCESS), record.status()); + // update total allowances + ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + } + try { final var updatedTopic = updateRunningHashAndSequenceNumber(txn, topic, handleContext.consensusNow()); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java new file mode 100644 index 000000000000..c95a17a78dd3 --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.util; + +import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; +import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; +import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; + +public class ConsensusApproveAllowanceHelper { + /** + * Applies all changes needed for Crypto allowances from the transaction. + * If the topic already has an allowance, the allowance value will be replaced with values + * from transaction. If the amount specified is 0, the allowance will be removed. + * @param topicCryptoAllowances the list of crypto allowances + * @param topicStore the topic store + */ + public static void applyCryptoAllowances( + @NonNull final List topicCryptoAllowances, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(topicCryptoAllowances); + requireNonNull(topicStore); + + for (final var allowance : topicCryptoAllowances) { + final var ownerId = allowance.owner(); + final var topicId = allowance.topicIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); + + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + + updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); + final var copy = + topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); + + topicStore.put(copy); + } + } + + public static void applyCryptoAllowances( + @NonNull final TopicID topicId, + @NonNull final TopicCryptoAllowance allowance, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(allowance); + requireNonNull(topicStore); + + final var ownerId = allowance.spenderId(); + final var topic = getIfUsable(topicId, topicStore); + final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); + + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + + updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); + final var copy = topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); + + topicStore.put(copy); + } + + /** + * Updates the crypto allowance amount if the allowance exists, otherwise adds a new allowance. + * If the amount is zero removes the allowance if it exists in the list. + * @param mutableAllowances the list of mutable allowances of owner + * @param amount the amount + * @param spenderId the spender id + */ + private static void updateCryptoAllowance( + final List mutableAllowances, + final long amount, + final long amountPerMessage, + final AccountID spenderId) { + final var newAllowanceBuilder = TopicCryptoAllowance.newBuilder().spenderId(spenderId); + // get the index of the allowance with same spender in existing list + final var index = lookupSpender(mutableAllowances, spenderId); + // If given amount is zero, if the element exists remove it, otherwise do nothing + if (amount == 0) { + if (index != -1) { + // If amount is 0, remove the allowance + mutableAllowances.remove(index); + } + return; + } + if (index != -1) { + mutableAllowances.set( + index, + newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } else { + mutableAllowances.add(newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } + } + + /** + * Applies all changes needed for fungible token allowances from the transaction. If the key + * {token, spender} already has an allowance, the allowance value will be replaced with values + * from transaction. + * @param tokenAllowances the list of token allowances + * @param topicStore the topic store + */ + public static void applyFungibleTokenAllowances( + @NonNull final List tokenAllowances, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(tokenAllowances); + requireNonNull(topicStore); + + for (final var allowance : tokenAllowances) { + final var ownerId = allowance.owner(); + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + final var tokenId = allowance.tokenIdOrThrow(); + final var topicId = allowance.topicIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + + final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); + + updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); + final var copy = + topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); + + topicStore.put(copy); + } + } + + /* + * + */ + public static void applyFungibleTokenAllowances( + @NonNull final TopicID topicId, + @NonNull final TopicFungibleTokenAllowance allowance, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(allowance); + requireNonNull(topicStore); + + final var ownerId = allowance.spenderIdOrThrow(); + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + final var tokenId = allowance.tokenIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + + final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); + + updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); + final var copy = + topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); + + topicStore.put(copy); + } + + /** + * Updates the token allowance amount if the allowance for given tokenNuma dn spenderNum exists, + * otherwise adds a new allowance. + * If the amount is zero removes the allowance if it exists in the list + * @param mutableAllowances the list of mutable allowances of owner + * @param amount the amount + * @param spenderId the spender number + * @param tokenId the token number + */ + private static void updateTokenAllowance( + final List mutableAllowances, + final long amount, + final long amountPerMessage, + final AccountID spenderId, + final TokenID tokenId) { + final var newAllowanceBuilder = + TopicFungibleTokenAllowance.newBuilder().spenderId(spenderId).tokenId(tokenId); + final var index = lookupSpenderAndToken(mutableAllowances, spenderId, tokenId); + // If given amount is zero, if the element exists remove it + if (amount == 0) { + if (index != -1) { + mutableAllowances.remove(index); + } + return; + } + if (index != -1) { + mutableAllowances.set( + index, + newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } else { + mutableAllowances.add(newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } + } + + /** + * Returns the index of the allowance with the given spender in the list if it exists, + * otherwise returns -1. + * @param topicCryptoAllowances list of allowances + * @param spenderNum spender account number + * @return index of the allowance if it exists, otherwise -1 + */ + private static int lookupSpender( + final List topicCryptoAllowances, final AccountID spenderNum) { + for (int i = 0; i < topicCryptoAllowances.size(); i++) { + final var allowance = topicCryptoAllowances.get(i); + if (allowance.spenderIdOrThrow().equals(spenderNum)) { + return i; + } + } + return -1; + } + + /** + * Returns the index of the allowance with the given spender and token in the list if it exists, + * otherwise returns -1. + * @param topicTokenAllowances list of allowances + * @param spenderId spender account number + * @param tokenId token number + * @return index of the allowance if it exists, otherwise -1 + */ + private static int lookupSpenderAndToken( + final List topicTokenAllowances, + final AccountID spenderId, + final TokenID tokenId) { + for (int i = 0; i < topicTokenAllowances.size(); i++) { + final var allowance = topicTokenAllowances.get(i); + if (allowance.spenderIdOrThrow().equals(spenderId) + && allowance.tokenIdOrThrow().equals(tokenId)) { + return i; + } + } + return -1; + } +} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java new file mode 100644 index 000000000000..c04ac7e2a98a --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.util; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyCryptoAllowances; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyFungibleTokenAllowances; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.AccountAmount; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.base.TransferList; +import com.hedera.hapi.node.state.consensus.Topic; +import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; +import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.ConsensusCustomFee; +import com.hedera.hapi.node.transaction.FixedFee; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.spi.workflows.HandleContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ConsensusCustomFeeHelper { + + public static CryptoTransferTransactionBody assessCustomFee(Topic topic, HandleContext context) { + final var payer = context.payer(); + final var topicStore = context.storeFactory().writableStore(WritableTopicStore.class); + // lookup for hbar allowance + TopicCryptoAllowance hbarAllowance = null; + for (final var allowance : topic.cryptoAllowances()) { + if (payer.equals(allowance.spenderId())) { + hbarAllowance = allowance; + } + } + // lookup for fungible token allowance + Map tokenAllowanceMap = new HashMap<>(); + for (final var allowance : topic.tokenAllowances()) { + if (payer.equals(allowance.spenderId())) { + tokenAllowanceMap.put(allowance.tokenId(), allowance); + } + } + + final var tokenTransfers = new ArrayList(); + List hbarTransfers = new ArrayList<>(); + for (ConsensusCustomFee fee : topic.customFees()) { + final var fixedFee = fee.fixedFeeOrThrow(); + // build crypto transfer body for the first layer of custom fees, + // if there is a second layer it will be assessed in crypto transfer handler + if (fixedFee.hasDenominatingTokenId()) { + final var tokenId = fixedFee.denominatingTokenId(); + validateTokenAllowance(tokenAllowanceMap, fixedFee); + tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); + } else { + validateHbarAllowance(hbarAllowance, fixedFee); + hbarTransfers = mergeTransfers( + hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); + } + } + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); + + return syntheticBodyBuilder + .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) + .build(); + } + + private static void validateTokenAllowance( + Map tokenAllowanceMap, FixedFee fixedFee) { + final var allowance = tokenAllowanceMap.get(fixedFee.denominatingTokenId()); + validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); + validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + } + + private static void validateHbarAllowance(TopicCryptoAllowance allowance, FixedFee fixedFee) { + validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); + validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + } + + public static void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { + // todo adjust allowance + // extract the code for updating the allowance amounts from ConsensusApproveAllowanceHandler and reuse it here + } + + private static List buildCustomFeeHbarTransferList( + AccountID payer, AccountID collector, FixedFee fee) { + return List.of( + AccountAmount.newBuilder() + .accountID(payer) + .amount(-fee.amount()) + .build(), + AccountAmount.newBuilder() + .accountID(collector) + .amount(fee.amount()) + .build()); + } + + private static TokenTransferList buildCustomFeeTokenTransferList( + AccountID payer, AccountID collector, FixedFee fee) { + return TokenTransferList.newBuilder() + .token(fee.denominatingTokenId()) + .transfers( + AccountAmount.newBuilder() + .accountID(payer) + .amount(-fee.amount()) + .build(), + AccountAmount.newBuilder() + .accountID(collector) + .amount(fee.amount()) + .build()) + .build(); + } + + private static CryptoTransferTransactionBody.Builder tokenTransfers( + @NonNull TokenTransferList... tokenTransferLists) { + if (repeatsTokenId(tokenTransferLists)) { + final Map consolidatedTokenTransfers = new LinkedHashMap<>(); + for (final var tokenTransferList : tokenTransferLists) { + consolidatedTokenTransfers.merge( + tokenTransferList.tokenOrThrow(), + tokenTransferList, + ConsensusCustomFeeHelper::mergeTokenTransferLists); + } + tokenTransferLists = consolidatedTokenTransfers.values().toArray(TokenTransferList[]::new); + } + return CryptoTransferTransactionBody.newBuilder().tokenTransfers(tokenTransferLists); + } + + private static TokenTransferList mergeTokenTransferLists( + @NonNull final TokenTransferList from, @NonNull final TokenTransferList to) { + return from.copyBuilder() + .transfers(mergeTransfers(from.transfers(), to.transfers())) + .build(); + } + + private static List mergeTransfers( + @NonNull final List from, @NonNull final List to) { + requireNonNull(from); + requireNonNull(to); + final Map consolidated = new LinkedHashMap<>(); + consolidateInto(consolidated, from); + consolidateInto(consolidated, to); + return consolidated.values().stream().toList(); + } + + private static void consolidateInto( + @NonNull final Map consolidated, @NonNull final List transfers) { + for (final var transfer : transfers) { + consolidated.merge(transfer.accountID(), transfer, ConsensusCustomFeeHelper::mergeAdjusts); + } + } + + private static AccountAmount mergeAdjusts(@NonNull final AccountAmount from, @NonNull final AccountAmount to) { + return from.copyBuilder() + .amount(from.amount() + to.amount()) + .isApproval(from.isApproval() || to.isApproval()) + .build(); + } + + private static boolean repeatsTokenId(@NonNull final TokenTransferList[] tokenTransferList) { + return tokenTransferList.length > 1 + && Arrays.stream(tokenTransferList) + .map(TokenTransferList::token) + .collect(Collectors.toSet()) + .size() + < tokenTransferList.length; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java index b841e7fe0149..32fb0849ca67 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java @@ -665,12 +665,32 @@ private void signRecursively(final Key key, final SigControl controller) throws case SIG_ON: signIfNecessary(key); break; + case PREDEFINED: + signPredefinedNatureKey(key, controller); + break; default: - final KeyList composite = TxnUtils.getCompositeList(key); - final SigControl[] childControls = controller.getChildControls(); - for (int i = 0; i < childControls.length; i++) { - signRecursively(composite.getKeys(i), childControls[i]); - } + signCompositeKeys(key, controller); + } + } + + private void signPredefinedNatureKey(Key key, SigControl control) throws GeneralSecurityException { + // if key is composite sign recursively + if (key.hasKeyList() || key.hasThresholdKey()) { + signCompositeKeys(key, control); + return; + } + + // skip contract id keys + if (!(key.hasContractID() || key.hasDelegatableContractId())) { + signIfNecessary(key); + } + } + + private void signCompositeKeys(Key key, SigControl control) throws GeneralSecurityException { + final KeyList composite = TxnUtils.getCompositeList(key); + final SigControl[] childControls = control.getChildControls(); + for (int i = 0; i < childControls.length; i++) { + signRecursively(composite.getKeys(i), childControls[i]); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 18e6803be8d4..4a3d24cfa979 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -17,25 +17,36 @@ package com.hedera.services.bdd.suites.hip991; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.keys.KeyShape.PREDEFINED_SHAPE; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.KeyShape.threshOf; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.flattened; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; @@ -256,6 +267,99 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { } } + @Nested + @DisplayName("Submit message") + class SubmitMessage { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } + + @HapiTest + @DisplayName("submit") + final Stream submitMessage() { + final var collector = "collector"; + final var payer = "submitter"; + final var treasury = "treasury"; + final var token = "testToken"; + final var secondToken = "secondToken"; + final var denomToken = "denomToken"; + final var simpleKey = "simpleKey"; + final var simpleKey2 = "simpleKey2"; + final var invalidKey = "invalidKey"; + final var threshKey = "threshKey"; + + return hapiTest( + // create keys + newKeyNamed(invalidKey), + newKeyNamed(simpleKey), + newKeyNamed(simpleKey2), + newKeyNamed(threshKey) + .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) + .signedWith(sigs(simpleKey2, simpleKey))), + // create accounts and denomination token + cryptoCreate(collector).balance(0L), + cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), + cryptoCreate(treasury), + tokenCreate(denomToken) + .treasury(treasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, denomToken), + tokenAssociate(payer, denomToken), + tokenCreate(token) + .treasury(treasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .withCustom(fixedHtsFee(1, denomToken, collector)) + .initialSupply(500), + tokenCreate(secondToken) + .treasury(treasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, token, secondToken), + tokenAssociate(payer, token, secondToken), + cryptoTransfer( + moving(2, token).between(treasury, payer), + moving(1, secondToken).between(treasury, payer), + moving(1, denomToken).between(treasury, payer)), + + // create topic with custom fees + createTopic(TOPIC) + // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, + // collector)) + // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, + // collector)) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + .feeExemptKeys(threshKey) + .hasKnownStatus(SUCCESS), + + // add allowance + approveTopicAllowance() + .payingWith(payer) + .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), + + // submit message + submitMessageTo(TOPIC) + .message("TEST") + .signedBy(invalidKey, payer) + .payingWith(payer) + .via("submit"), + + // check records + getTxnRecord("submit").andAllChildRecords().logged(), + + // assert balances + getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + // .hasTokenBalance(token, 2) + // .hasTokenBalance(denomToken,1) + // .hasTokenBalance(secondToken, 1), + // getAccountBalance(payer) + // .hasTokenBalance(token, 0) + // .hasTokenBalance(secondToken, 0)); + } + } + @Nested @DisplayName("Topic approve allowance") class TopicApproveAllowance { From c32fba1b1f95a830101285fc3701f3673e5eb190 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 30 Sep 2024 17:16:22 +0300 Subject: [PATCH 36/94] Add hapi tests Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 53 ++- .../bdd/suites/hip991/TopicCustomFeeTest.java | 304 +++++++++++++----- 2 files changed, 271 insertions(+), 86 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 7bbfca58f048..b9ae1d16998e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -16,11 +16,20 @@ package com.hedera.services.bdd.suites.hip991; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_MILLION_HBARS; import com.google.protobuf.ByteString; import com.hedera.services.bdd.spec.SpecOperation; import com.hederahashgraph.api.proto.java.Key; +import com.hederahashgraph.api.proto.java.TokenType; import java.util.ArrayList; public class TopicCustomFeeBase { @@ -31,7 +40,11 @@ public class TopicCustomFeeBase { protected static final String SUBMIT_KEY = "submitKey"; protected static final String FEE_SCHEDULE_KEY = "feeScheduleKey"; protected static final String FEE_EXEMPT_KEY_PREFIX = "feeExemptKey_"; - + /* Submit message entities */ + protected static final String SUBMITTER = "submitter"; + protected static final String TOKEN_TREASURY = "tokenTreasury"; + protected static final String BASE_TOKEN = "baseToken"; + protected static final String MULTI_LAYER_FEE_PREFIX = "multiLayerFeePrefix_"; // This key is truly invalid, as all Ed25519 public keys must be 32 bytes long protected static final Key STRUCTURALLY_INVALID_KEY = Key.newBuilder().setEd25519(ByteString.fromHex("ff")).build(); @@ -40,6 +53,44 @@ protected static SpecOperation[] setupBaseKeys() { return new SpecOperation[] {newKeyNamed(ADMIN_KEY), newKeyNamed(SUBMIT_KEY), newKeyNamed(FEE_SCHEDULE_KEY)}; } + protected static SpecOperation[] associateFeeTokensAndSubmitter() { + return new SpecOperation[] { + cryptoCreate(SUBMITTER).balance(ONE_MILLION_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(BASE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(500L), + tokenAssociate(SUBMITTER, BASE_TOKEN), + cryptoTransfer(moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER)) + }; + } + + /** + * Create and transfer multiple tokens with fixed hbar custom fee to account. + * @param account account to transfer tokens + * @param numberOfTokens the count of tokens to be transferred + * @return array of spec operations + */ + protected SpecOperation[] transferMultiLayerFeeTokensTo(String account, int numberOfTokens) { + final var treasury = MULTI_LAYER_FEE_PREFIX + TOKEN_TREASURY; + final var list = new ArrayList(); + list.add(cryptoCreate(treasury)); + for (int i = 0; i < numberOfTokens; i++) { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_" + i; + final var collectorName = MULTI_LAYER_FEE_PREFIX + "collector_" + i; + list.add(cryptoCreate(collectorName).balance(0L)); + list.add(tokenCreate(tokenName) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(treasury) + .initialSupply(500L) + .withCustom(fixedHbarFee(ONE_HBAR, collectorName))); + list.add(tokenAssociate(account, tokenName)); + list.add(cryptoTransfer(moving(500L, tokenName).between(treasury, account))); + } + return list.toArray(new SpecOperation[0]); + } + protected static SpecOperation[] newNamedKeysForFEKL(int count) { final var list = new ArrayList(); for (int i = 0; i < count; i++) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 394026060386..188bdea0b783 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -47,12 +47,15 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.SpecOperation; import com.hederahashgraph.api.proto.java.TokenType; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; @@ -271,92 +274,223 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { @DisplayName("Submit message") class SubmitMessage { - @BeforeAll - static void beforeAll(@NonNull final TestLifecycle lifecycle) { - lifecycle.doAdhoc(setupBaseKeys()); - } +// @HapiTest +// @DisplayName("submit") +// final Stream submitMessage() { +// final var collector = "collector"; +// final var payer = "submitter"; +// final var treasury = "treasury"; +// final var token = "testToken"; +// final var secondToken = "secondToken"; +// final var denomToken = "denomToken"; +// final var simpleKey = "simpleKey"; +// final var simpleKey2 = "simpleKey2"; +// final var invalidKey = "invalidKey"; +// final var threshKey = "threshKey"; +// +// return hapiTest( +// // create keys +// newKeyNamed(invalidKey), +// newKeyNamed(simpleKey), +// newKeyNamed(simpleKey2), +// newKeyNamed(threshKey) +// .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) +// .signedWith(sigs(simpleKey2, simpleKey))), +// // create accounts and denomination token +// cryptoCreate(collector).balance(0L), +// cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), +// cryptoCreate(treasury), +// tokenCreate(denomToken) +// .treasury(treasury) +// .tokenType(TokenType.FUNGIBLE_COMMON) +// .initialSupply(500), +// tokenAssociate(collector, denomToken), +// tokenAssociate(payer, denomToken), +// tokenCreate(token) +// .treasury(treasury) +// .tokenType(TokenType.FUNGIBLE_COMMON) +// .withCustom(fixedHtsFee(1, denomToken, collector)) +// .initialSupply(500), +// tokenCreate(secondToken) +// .treasury(treasury) +// .tokenType(TokenType.FUNGIBLE_COMMON) +// .initialSupply(500), +// tokenAssociate(collector, token, secondToken), +// tokenAssociate(payer, token, secondToken), +// cryptoTransfer( +// moving(2, token).between(treasury, payer), +// moving(1, secondToken).between(treasury, payer), +// moving(1, denomToken).between(treasury, payer)), +// +// // create topic with custom fees +// createTopic(TOPIC) +// // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, +// // collector)) +// // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, +// // collector)) +// .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) +// .feeExemptKeys(threshKey) +// .hasKnownStatus(SUCCESS), +// +// // add allowance +// approveTopicAllowance() +// .payingWith(payer) +// .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), +// +// // submit message +// submitMessageTo(TOPIC) +// .message("TEST") +// .signedBy(invalidKey, payer) +// .payingWith(payer) +// .via("submit"), +// +// // check records +// getTxnRecord("submit").andAllChildRecords().logged(), +// +// // assert balances +// getAccountBalance(collector).hasTinyBars(ONE_HBAR)); +// // .hasTokenBalance(token, 2) +// // .hasTokenBalance(denomToken,1) +// // .hasTokenBalance(secondToken, 1), +// // getAccountBalance(payer) +// // .hasTokenBalance(token, 0) +// // .hasTokenBalance(secondToken, 0)); +// } + + @Nested + @DisplayName("Positive scenarios") + class SubmitMessagesPositiveScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(associateFeeTokensAndSubmitter()); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") + final Stream messageSubmitToPublicTopicWithFee1Hbar() { + final var collector = "collector"; + final var submitter = "submitter"; + return hapiTest( + cryptoCreate(collector).balance(0L), + cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(submitter), + submitMessageTo(TOPIC).message("TEST").payingWith(submitter), + getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") + final Stream messageSubmitToPublicTopicWithFee1token() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 2 layer fee") + final Stream messageSubmitToPublicTopicWith2layerFee() { + final var topicFeeCollector = "collector"; + final var token = MULTI_LAYER_FEE_PREFIX + "token_0"; + final var tokenFeeCollector = MULTI_LAYER_FEE_PREFIX + "collector_0"; + return hapiTest(flattened( + cryptoCreate(topicFeeCollector).balance(0L), + // create denomination token and transfer it to the submitter + transferMultiLayerFeeTokensTo(SUBMITTER, 1), + tokenAssociate(topicFeeCollector, token), + // create topic with multilayer fee + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector).hasTinyBars(ONE_HBAR))); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 10 different 2 layer fees") + final Stream messageSubmitToPublicTopicWith10different2layerFees() { + return hapiTest(flattened( + // create 9 denomination tokens and transfer them to the submitter + transferMultiLayerFeeTokensTo(SUBMITTER, 9), + // create 9 collectors and associate them with tokens + associateAllTokensToCollectors(), + // create topic with 10 multilayer fees - 9 HTS + 1 HBAR + createTopicWith10Different2layerFees(), + approveTopicAllowanceForAllFees(), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER) + // todo for now custom fee will fail, because of limitation in cryptoTransfer + .hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED))); + // assert topic fee collector balance +// assertAllCollectorsBalances())); + } + + + + + - @HapiTest - @DisplayName("submit") - final Stream submitMessage() { - final var collector = "collector"; - final var payer = "submitter"; - final var treasury = "treasury"; - final var token = "testToken"; - final var secondToken = "secondToken"; - final var denomToken = "denomToken"; - final var simpleKey = "simpleKey"; - final var simpleKey2 = "simpleKey2"; - final var invalidKey = "invalidKey"; - final var threshKey = "threshKey"; - - return hapiTest( - // create keys - newKeyNamed(invalidKey), - newKeyNamed(simpleKey), - newKeyNamed(simpleKey2), - newKeyNamed(threshKey) - .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) - .signedWith(sigs(simpleKey2, simpleKey))), - // create accounts and denomination token - cryptoCreate(collector).balance(0L), - cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), - cryptoCreate(treasury), - tokenCreate(denomToken) - .treasury(treasury) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(500), - tokenAssociate(collector, denomToken), - tokenAssociate(payer, denomToken), - tokenCreate(token) - .treasury(treasury) - .tokenType(TokenType.FUNGIBLE_COMMON) - .withCustom(fixedHtsFee(1, denomToken, collector)) - .initialSupply(500), - tokenCreate(secondToken) - .treasury(treasury) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(500), - tokenAssociate(collector, token, secondToken), - tokenAssociate(payer, token, secondToken), - cryptoTransfer( - moving(2, token).between(treasury, payer), - moving(1, secondToken).between(treasury, payer), - moving(1, denomToken).between(treasury, payer)), - - // create topic with custom fees - createTopic(TOPIC) - // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, - // collector)) - // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, - // collector)) - .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) - .feeExemptKeys(threshKey) - .hasKnownStatus(SUCCESS), - - // add allowance - approveTopicAllowance() - .payingWith(payer) - .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), - - // submit message - submitMessageTo(TOPIC) - .message("TEST") - .signedBy(invalidKey, payer) - .payingWith(payer) - .via("submit"), - - // check records - getTxnRecord("submit").andAllChildRecords().logged(), - - // assert balances - getAccountBalance(collector).hasTinyBars(ONE_HBAR)); - // .hasTokenBalance(token, 2) - // .hasTokenBalance(denomToken,1) - // .hasTokenBalance(secondToken, 1), - // getAccountBalance(payer) - // .hasTokenBalance(token, 0) - // .hasTokenBalance(secondToken, 0)); + private SpecOperation[] associateAllTokensToCollectors() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var collectorName = "collector_"; + final var associateTokensToCollectors = new ArrayList(); + for (int i = 0; i < 9; i++) { + associateTokensToCollectors.add( + cryptoCreate(collectorName + i).balance(0L)); + associateTokensToCollectors.add(tokenAssociate(collectorName + i, tokenName + i)); + } + return associateTokensToCollectors.toArray(SpecOperation[]::new); + } + + private SpecOperation createTopicWith10Different2layerFees() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var collectorName = "collector_"; + final var topicCreateOp = createTopic(TOPIC); + for (int i = 0; i < 9; i++) { + topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, tokenName + i, collectorName + i)); + } + // add one hbar custom fee + topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); + return topicCreateOp; + } + + private SpecOperation approveTopicAllowanceForAllFees() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); + + for (int i = 0; i < 9; i++) { + approveAllowance.addTokenAllowance(SUBMITTER, tokenName + i, TOPIC, 100, 1); + } + approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); + return approveAllowance; + } + + private SpecOperation[] assertAllCollectorsBalances() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var collectorName = "collector_"; + + final var assertBalances = new ArrayList(); + + for (int i = 0; i < 9; i++) { + assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(tokenName + i, 1)); + } + // add assert for hbar fee + assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); + return assertBalances.toArray(SpecOperation[]::new); + } } } From ff651bcb19d5f50dd924a28f00ca85782863f95c Mon Sep 17 00:00:00 2001 From: ibankov Date: Tue, 1 Oct 2024 10:26:18 +0300 Subject: [PATCH 37/94] Add unit tests and reject contracts Signed-off-by: ibankov --- .../services/response_code.proto | 5 + .../node/app/store/WritableStoreFactory.java | 1 - .../ConsensusAllowancesValidator.java | 12 +- .../ConsensusApproveAllowanceTest.java | 218 ++++++++++++++++++ .../impl/test/handlers/ConsensusTestBase.java | 44 ++++ 5 files changed, 275 insertions(+), 5 deletions(-) create mode 100644 hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index bdc0c00d36e8..4ee45930b463 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1630,4 +1630,9 @@ enum ResponseCodeEnum { * The topic has been marked as deleted. */ TOPIC_DELETED = 376; + + /** + * The account receiving allowance is a smart contract. + */ + ACCOUNT_IS_CONTRACT = 377; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java index 8b225a4042bf..8dce0561d649 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java @@ -103,7 +103,6 @@ private static Map, StoreEntry> createFactoryMap() { new StoreEntry(EntityIdService.NAME, (states, config, metrics) -> new WritableEntityIdStore(states))); // Schedule Service newMap.put(WritableScheduleStore.class, new StoreEntry(ScheduleService.NAME, WritableScheduleStoreImpl::new)); - newMap.put(WritableNodeStore.class, new StoreEntry(AddressBookService.NAME, WritableNodeStore::new)); return Collections.unmodifiableMap(newMap); } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java index 723b05412fc9..120b13bc5afa 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java @@ -16,6 +16,7 @@ package com.hedera.node.app.service.consensus.impl.validators; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_IS_CONTRACT; import static com.hedera.hapi.node.base.ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_SPENDER_ID; @@ -104,11 +105,12 @@ public void validateSemantics( // validate spender account final var spenderAccount = accountStore.getAccountById(ownerId); - validateTrue(TokenType.FUNGIBLE_COMMON.equals(token.tokenType()), NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES); - - // validate token amount final var amount = tokenAllowance.amount(); validateSpender(amount, spenderAccount); + + + // validate token amount + validateTrue(TokenType.FUNGIBLE_COMMON.equals(token.tokenType()), NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES); final var relation = tokenRelStore.get(ownerId, tokenId); validateTrue(relation != null, TOKEN_NOT_ASSOCIATED_TO_ACCOUNT); @@ -124,8 +126,10 @@ public void validateSemantics( * @param spenderAccount If the amount is not zero, then this must be non-null and not deleted. */ private void validateSpender(final long amount, @Nullable final Account spenderAccount) { + validateTrue(spenderAccount!=null, INVALID_ALLOWANCE_SPENDER_ID); + validateFalse(spenderAccount.smartContract(), ACCOUNT_IS_CONTRACT); validateTrue( - amount == 0 || (spenderAccount != null && !spenderAccount.deleted()), INVALID_ALLOWANCE_SPENDER_ID); + amount == 0 || !spenderAccount.deleted(), INVALID_ALLOWANCE_SPENDER_ID); } /** diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java new file mode 100644 index 000000000000..912f1628e73f --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.test.handlers; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; +import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.state.consensus.Topic; +import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; +import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; +import com.hedera.hapi.node.token.ConsensusApproveAllowanceTransactionBody; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; +import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.app.spi.workflows.PreHandleContext; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +public class ConsensusApproveAllowanceTest extends ConsensusTestBase { + @Mock(strictness = LENIENT) + private HandleContext.SavepointStack stack; + + private ConsensusApproveAllowanceHandler subject; + + @BeforeEach + void setUp() { + subject = new ConsensusApproveAllowanceHandler(new ConsensusAllowancesValidator()); + refreshStoresWithCurrentTopicOnlyInReadable(); + given(handleContext.savepointStack()).willReturn(stack); + } + + @Test + void preHandleWithValidAllowancesShouldPass() throws PreCheckException { + // Arrange + PreHandleContext mockContext = mock(PreHandleContext.class); + var txnBody = consensusApproveAllowanceTransaction(ownerId, List.of(), List.of()); + + when(mockContext.body()).thenReturn(txnBody); + when(mockContext.payer()).thenReturn(ownerId); + + // Act & Assert: should not throw any exception + subject.preHandle(mockContext); + + verify(mockContext, times(1)).body(); + } + + @Test + void handleWithEmptyAllowancesShouldNotUpdateState() { + // Arrange + WritableTopicStore mockTopicStore = mock(WritableTopicStore.class); + var emptyTxnBody = consensusApproveAllowanceTransaction(ownerId, List.of(), List.of()); + + given(handleContext.body()).willReturn(emptyTxnBody); + + when(handleContext.storeFactory().writableStore(WritableTopicStore.class)) + .thenReturn(mockTopicStore); + + // Act + subject.handle(handleContext); + + // Assert + verify(mockTopicStore, never()).put(any()); + } + + @Test + void happyPathAddsAllowances() { + setUpStores(handleContext); + final var txn = consensusApproveAllowanceTransaction( + payerId, List.of(consensusCryptoAllowance()), List.of(consensusTokenAllowance())); + given(handleContext.body()).willReturn(txn); + final var topic = writableStore.getTopic(topicId); + assertNotNull(topic); + assertThat(topic.cryptoAllowances()).isEmpty(); + assertThat(topic.tokenAllowances()).isEmpty(); + + subject.handle(handleContext); + + final var modifiedTopic = writableStore.getTopic(topicId); + assertNotNull(modifiedTopic); + assertThat(modifiedTopic.cryptoAllowances()).hasSize(1); + assertThat(modifiedTopic.tokenAllowances()).hasSize(1); + + assertThat(modifiedTopic.cryptoAllowances().getFirst().spenderId()).isEqualTo(ownerId); + assertThat(modifiedTopic.cryptoAllowances().getFirst().amount()).isEqualTo(100); + assertThat(modifiedTopic.cryptoAllowances().getFirst().amountPerMessage()) + .isEqualTo(10); + assertThat(modifiedTopic.tokenAllowances().getFirst().spenderId()).isEqualTo(ownerId); + assertThat(modifiedTopic.tokenAllowances().getFirst().amount()).isEqualTo(100); + assertThat(modifiedTopic.tokenAllowances().getFirst().amountPerMessage()) + .isEqualTo(10); + assertThat(modifiedTopic.tokenAllowances().getFirst().tokenId()).isEqualTo(fungibleTokenId); + } + + @Test + void handleWithZeroAllowanceShouldRemoveAllowanceFromStore() { + setUpStores(handleContext); + // Mock topic store and a topic with an existing allowance + var topicFromStateId = TopicID.newBuilder().topicNum(1).build(); + List initialCryptoAllowances = new ArrayList<>(); + List initialTokenAllowances = new ArrayList<>(); + + initialCryptoAllowances.add(TopicCryptoAllowance.newBuilder() + .spenderId(ownerId) + .amount(100L) + .amountPerMessage(10L) + .build()); + initialTokenAllowances.add(TopicFungibleTokenAllowance.newBuilder() + .spenderId(ownerId) + .amount(100L) + .amountPerMessage(10L) + .tokenId(fungibleTokenId) + .build()); + + // Add the topic with the initial allowance + Topic topic = Topic.newBuilder() + .topicId(topicFromStateId) + .cryptoAllowances(initialCryptoAllowances) + .tokenAllowances(initialTokenAllowances) + .build(); + writableStore.put(topic); + + // Create an allowance transaction with amount 0 (which should remove the allowance) + ConsensusCryptoFeeScheduleAllowance zeroCryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() + .owner(ownerId) + .amount(0L) // Passing 0 to remove the allowance + .amountPerMessage(0L) + .topicId(topicFromStateId) + .build(); + ConsensusTokenFeeScheduleAllowance zeroTokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() + .owner(ownerId) + .amount(0L) // Passing 0 to remove the allowance + .amountPerMessage(0L) + .topicId(topicFromStateId) + .tokenId(fungibleTokenId) + .build(); + var allowanceTxnBody = consensusApproveAllowanceTransaction( + ownerId, List.of(zeroCryptoAllowance), List.of(zeroTokenAllowance)); + + when(handleContext.body()).thenReturn(allowanceTxnBody); + + // Act + subject.handle(handleContext); + + // Assert + Topic updatedTopic = writableStore.getTopic(topicFromStateId); + assertNotNull(updatedTopic); + assertTrue(updatedTopic.cryptoAllowances().isEmpty(), "Crypto allowance should be removed from the store"); + assertTrue(updatedTopic.tokenAllowances().isEmpty(), "Token allowance should be removed from the store"); + } + + private TransactionBody consensusApproveAllowanceTransaction( + final AccountID id, + final List cryptoAllowance, + final List tokenAllowance) { + final var transactionID = TransactionID.newBuilder().accountID(id); + final var allowanceTxnBody = ConsensusApproveAllowanceTransactionBody.newBuilder() + .consensusCryptoFeeScheduleAllowances(cryptoAllowance) + .consensusTokenFeeScheduleAllowances(tokenAllowance) + .build(); + return TransactionBody.newBuilder() + .transactionID(transactionID) + .consensusApproveAllowance(allowanceTxnBody) + .build(); + } + + private ConsensusCryptoFeeScheduleAllowance consensusCryptoAllowance() { + return ConsensusCryptoFeeScheduleAllowance.newBuilder() + .amount(100) + .amountPerMessage(10) + .topicId(topicId) + .owner(ownerId) + .build(); + } + + private ConsensusTokenFeeScheduleAllowance consensusTokenAllowance() { + return ConsensusTokenFeeScheduleAllowance.newBuilder() + .amount(100) + .amountPerMessage(10) + .tokenId(fungibleTokenId) + .topicId(topicId) + .owner(ownerId) + .build(); + } +} diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java index 5f70730dd1d8..ad0db2e23b74 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java @@ -19,19 +19,27 @@ import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.TOPICS_KEY; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.when; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.Duration; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.KeyList; import com.hedera.hapi.node.base.ThresholdKey; +import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.state.consensus.Topic; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.Token; +import com.hedera.hapi.node.state.token.TokenRelation; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.ReadableTopicStoreImpl; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.ReadableTokenRelationStore; +import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.metrics.StoreMetricsService; import com.hedera.node.app.spi.store.StoreFactory; import com.hedera.node.app.spi.workflows.HandleContext; @@ -95,7 +103,9 @@ public class ConsensusTestBase { protected final AccountID payerId = AccountID.newBuilder().accountNum(3).build(); public static final AccountID anotherPayer = AccountID.newBuilder().accountNum(13257).build(); + protected final AccountID ownerId = AccountID.newBuilder().accountNum(555).build(); protected final AccountID autoRenewId = AccountID.newBuilder().accountNum(1).build(); + protected final TokenID fungibleTokenId = TokenID.newBuilder().tokenNum(1).build(); protected final byte[] runningHash = "runningHash".getBytes(); protected final Key adminKey = key; @@ -131,6 +141,15 @@ public class ConsensusTestBase { @Mock(strictness = LENIENT) protected StoreFactory storeFactory; + @Mock(strictness = LENIENT) + private ReadableAccountStore accountStore; + + @Mock(strictness = LENIENT) + private ReadableTokenStore tokenStore; + + @Mock(strictness = LENIENT) + private ReadableTokenRelationStore tokenRelStore; + @Mock private StoreMetricsService storeMetricsService; @@ -193,6 +212,31 @@ protected MapReadableKVState emptyReadableTopicState() { return MapReadableKVState.builder(TOPICS_KEY).build(); } + protected void setUpStores(final HandleContext context) { + given(context.storeFactory()).willReturn(storeFactory); + var config = HederaTestConfigBuilder.create().getOrCreateConfig(); + when(handleContext.configuration()).thenReturn(config); + // Set up account store + var account = Account.newBuilder().accountId(ownerId).build(); + when(accountStore.getAccountById(ownerId)).thenReturn(account); + when(storeFactory.readableStore(ReadableAccountStore.class)).thenReturn(accountStore); + // Set up token store + var token = Token.newBuilder().tokenId(fungibleTokenId).build(); + var tokenRel = TokenRelation.newBuilder() + .tokenId(fungibleTokenId) + .accountId(ownerId) + .build(); + when(tokenStore.get(fungibleTokenId)).thenReturn(token); + when(tokenRelStore.get(ownerId, fungibleTokenId)).thenReturn(tokenRel); + when(storeFactory.readableStore(ReadableTokenStore.class)).thenReturn(tokenStore); + when(storeFactory.readableStore(ReadableTokenRelationStore.class)).thenReturn(tokenRelStore); + // Set up topic store + // givenValidTopic(); + writableStore.put(topic); + when(storeFactory.readableStore(ReadableTopicStore.class)).thenReturn(readableStore); + when(storeFactory.writableStore(WritableTopicStore.class)).thenReturn(writableStore); + } + protected void givenValidTopic() { givenValidTopic(autoRenewId); } From 5fec8e322de05b1f77bc1e276726de80a5e2892d Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 1 Oct 2024 17:48:48 +0300 Subject: [PATCH 38/94] Split crypto dispatches if we have to many transfers Signed-off-by: Zhivko Kelchev --- .../ConsensusSubmitMessageHandler.java | 28 +-- .../impl/util/ConsensusCustomFeeHelper.java | 46 ++++- .../ConsensusAllowancesValidator.java | 6 +- .../bdd/suites/hip991/TopicCustomFeeTest.java | 186 ++++++++---------- 4 files changed, 143 insertions(+), 123 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 8c306907b265..c5e3be9d7aca 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -145,18 +145,22 @@ public void handle(@NonNull final HandleContext handleContext) { } if (!topic.customFees().isEmpty() && !payerIsFeeExempted) { // validate and create synthetic body - final var syntheticBody = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); - // dispatch crypto transfer - var record = handleContext.dispatchChildTransaction( - TransactionBody.newBuilder().cryptoTransfer(syntheticBody).build(), - ConsensusSubmitMessageStreamBuilder.class, - null, - handleContext.payer(), - HandleContext.TransactionCategory.CHILD, - HandleContext.ConsensusThrottling.OFF); - validateTrue(record.status().equals(SUCCESS), record.status()); - // update total allowances - ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + final var syntheticBodies = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); + for (final var syntheticBody : syntheticBodies) { + // dispatch crypto transfer + var record = handleContext.dispatchChildTransaction( + TransactionBody.newBuilder() + .cryptoTransfer(syntheticBody) + .build(), + ConsensusSubmitMessageStreamBuilder.class, + null, + handleContext.payer(), + HandleContext.TransactionCategory.CHILD, + HandleContext.ConsensusThrottling.OFF); + validateTrue(record.status().equals(SUCCESS), record.status()); + // update total allowances + ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + } } try { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java index c04ac7e2a98a..acab8571799e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java @@ -36,6 +36,7 @@ import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.Arrays; @@ -47,9 +48,15 @@ public class ConsensusCustomFeeHelper { - public static CryptoTransferTransactionBody assessCustomFee(Topic topic, HandleContext context) { + public static List assessCustomFee(Topic topic, HandleContext context) { + final List transactionBodies = new ArrayList<>(); + final var payer = context.payer(); final var topicStore = context.storeFactory().writableStore(WritableTopicStore.class); + final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); + + // todo: allowance validation will be changed, when the storage situation is clear. + // lookup for hbar allowance TopicCryptoAllowance hbarAllowance = null; for (final var allowance : topic.cryptoAllowances()) { @@ -67,27 +74,52 @@ public static CryptoTransferTransactionBody assessCustomFee(Topic topic, HandleC final var tokenTransfers = new ArrayList(); List hbarTransfers = new ArrayList<>(); + // we need to count the number of balance adjustments, + // and if needed to split custom fee transfers in to two separate dispatches + final var maxTransfers = ledgerConfig.transfersMaxLen() / 2; + var transferCounts = 0; + + // build crypto transfer body for the first layer of custom fees, + // if there is a second layer it will be assessed in crypto transfer handler for (ConsensusCustomFee fee : topic.customFees()) { final var fixedFee = fee.fixedFeeOrThrow(); - // build crypto transfer body for the first layer of custom fees, - // if there is a second layer it will be assessed in crypto transfer handler if (fixedFee.hasDenominatingTokenId()) { final var tokenId = fixedFee.denominatingTokenId(); validateTokenAllowance(tokenAllowanceMap, fixedFee); + tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + // update allowance values applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); } else { validateHbarAllowance(hbarAllowance, fixedFee); hbarTransfers = mergeTransfers( hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + // update allowance values applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); } + transferCounts++; + + if (transferCounts == maxTransfers) { + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); + + transactionBodies.add(syntheticBodyBuilder + .transfers( + TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) + .build()); + + // reset lists and counter + transferCounts = 0; + tokenTransfers.clear(); + hbarTransfers.clear(); + } } - final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); - return syntheticBodyBuilder + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); + transactionBodies.add(syntheticBodyBuilder .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) - .build(); + .build()); + + return transactionBodies; } private static void validateTokenAllowance( @@ -167,7 +199,7 @@ private static List mergeTransfers( final Map consolidated = new LinkedHashMap<>(); consolidateInto(consolidated, from); consolidateInto(consolidated, to); - return consolidated.values().stream().toList(); + return new ArrayList<>(consolidated.values()); } private static void consolidateInto( diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java index 120b13bc5afa..266a208de975 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java @@ -108,7 +108,6 @@ public void validateSemantics( final var amount = tokenAllowance.amount(); validateSpender(amount, spenderAccount); - // validate token amount validateTrue(TokenType.FUNGIBLE_COMMON.equals(token.tokenType()), NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES); final var relation = tokenRelStore.get(ownerId, tokenId); @@ -126,10 +125,9 @@ public void validateSemantics( * @param spenderAccount If the amount is not zero, then this must be non-null and not deleted. */ private void validateSpender(final long amount, @Nullable final Account spenderAccount) { - validateTrue(spenderAccount!=null, INVALID_ALLOWANCE_SPENDER_ID); + validateTrue(spenderAccount != null, INVALID_ALLOWANCE_SPENDER_ID); validateFalse(spenderAccount.smartContract(), ACCOUNT_IS_CONTRACT); - validateTrue( - amount == 0 || !spenderAccount.deleted(), INVALID_ALLOWANCE_SPENDER_ID); + validateTrue(amount == 0 || !spenderAccount.deleted(), INVALID_ALLOWANCE_SPENDER_ID); } /** diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 188bdea0b783..bca220efb480 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -17,26 +17,19 @@ package com.hedera.services.bdd.suites.hip991; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; -import static com.hedera.services.bdd.spec.keys.KeyShape.PREDEFINED_SHAPE; -import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; -import static com.hedera.services.bdd.spec.keys.KeyShape.threshOf; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; -import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; @@ -46,8 +39,6 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; @@ -274,88 +265,90 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { @DisplayName("Submit message") class SubmitMessage { -// @HapiTest -// @DisplayName("submit") -// final Stream submitMessage() { -// final var collector = "collector"; -// final var payer = "submitter"; -// final var treasury = "treasury"; -// final var token = "testToken"; -// final var secondToken = "secondToken"; -// final var denomToken = "denomToken"; -// final var simpleKey = "simpleKey"; -// final var simpleKey2 = "simpleKey2"; -// final var invalidKey = "invalidKey"; -// final var threshKey = "threshKey"; -// -// return hapiTest( -// // create keys -// newKeyNamed(invalidKey), -// newKeyNamed(simpleKey), -// newKeyNamed(simpleKey2), -// newKeyNamed(threshKey) -// .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) -// .signedWith(sigs(simpleKey2, simpleKey))), -// // create accounts and denomination token -// cryptoCreate(collector).balance(0L), -// cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), -// cryptoCreate(treasury), -// tokenCreate(denomToken) -// .treasury(treasury) -// .tokenType(TokenType.FUNGIBLE_COMMON) -// .initialSupply(500), -// tokenAssociate(collector, denomToken), -// tokenAssociate(payer, denomToken), -// tokenCreate(token) -// .treasury(treasury) -// .tokenType(TokenType.FUNGIBLE_COMMON) -// .withCustom(fixedHtsFee(1, denomToken, collector)) -// .initialSupply(500), -// tokenCreate(secondToken) -// .treasury(treasury) -// .tokenType(TokenType.FUNGIBLE_COMMON) -// .initialSupply(500), -// tokenAssociate(collector, token, secondToken), -// tokenAssociate(payer, token, secondToken), -// cryptoTransfer( -// moving(2, token).between(treasury, payer), -// moving(1, secondToken).between(treasury, payer), -// moving(1, denomToken).between(treasury, payer)), -// -// // create topic with custom fees -// createTopic(TOPIC) -// // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, -// // collector)) -// // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, -// // collector)) -// .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) -// .feeExemptKeys(threshKey) -// .hasKnownStatus(SUCCESS), -// -// // add allowance -// approveTopicAllowance() -// .payingWith(payer) -// .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), -// -// // submit message -// submitMessageTo(TOPIC) -// .message("TEST") -// .signedBy(invalidKey, payer) -// .payingWith(payer) -// .via("submit"), -// -// // check records -// getTxnRecord("submit").andAllChildRecords().logged(), -// -// // assert balances -// getAccountBalance(collector).hasTinyBars(ONE_HBAR)); -// // .hasTokenBalance(token, 2) -// // .hasTokenBalance(denomToken,1) -// // .hasTokenBalance(secondToken, 1), -// // getAccountBalance(payer) -// // .hasTokenBalance(token, 0) -// // .hasTokenBalance(secondToken, 0)); -// } + // @HapiTest + // @DisplayName("submit") + // final Stream submitMessage() { + // final var collector = "collector"; + // final var payer = "submitter"; + // final var treasury = "treasury"; + // final var token = "testToken"; + // final var secondToken = "secondToken"; + // final var denomToken = "denomToken"; + // final var simpleKey = "simpleKey"; + // final var simpleKey2 = "simpleKey2"; + // final var invalidKey = "invalidKey"; + // final var threshKey = "threshKey"; + // + // return hapiTest( + // // create keys + // newKeyNamed(invalidKey), + // newKeyNamed(simpleKey), + // newKeyNamed(simpleKey2), + // newKeyNamed(threshKey) + // .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) + // .signedWith(sigs(simpleKey2, simpleKey))), + // // create accounts and denomination token + // cryptoCreate(collector).balance(0L), + // cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), + // cryptoCreate(treasury), + // tokenCreate(denomToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, denomToken), + // tokenAssociate(payer, denomToken), + // tokenCreate(token) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .withCustom(fixedHtsFee(1, denomToken, collector)) + // .initialSupply(500), + // tokenCreate(secondToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, token, secondToken), + // tokenAssociate(payer, token, secondToken), + // cryptoTransfer( + // moving(2, token).between(treasury, payer), + // moving(1, secondToken).between(treasury, payer), + // moving(1, denomToken).between(treasury, payer)), + // + // // create topic with custom fees + // createTopic(TOPIC) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // token, + // // collector)) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // secondToken, + // // collector)) + // .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + // .feeExemptKeys(threshKey) + // .hasKnownStatus(SUCCESS), + // + // // add allowance + // approveTopicAllowance() + // .payingWith(payer) + // .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), + // + // // submit message + // submitMessageTo(TOPIC) + // .message("TEST") + // .signedBy(invalidKey, payer) + // .payingWith(payer) + // .via("submit"), + // + // // check records + // getTxnRecord("submit").andAllChildRecords().logged(), + // + // // assert balances + // getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + // // .hasTokenBalance(token, 2) + // // .hasTokenBalance(denomToken,1) + // // .hasTokenBalance(secondToken, 1), + // // getAccountBalance(payer) + // // .hasTokenBalance(token, 0) + // // .hasTokenBalance(secondToken, 0)); + // } @Nested @DisplayName("Positive scenarios") @@ -431,18 +424,11 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() // create topic with 10 multilayer fees - 9 HTS + 1 HBAR createTopicWith10Different2layerFees(), approveTopicAllowanceForAllFees(), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER) - // todo for now custom fee will fail, because of limitation in cryptoTransfer - .hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED))); + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance -// assertAllCollectorsBalances())); + assertAllCollectorsBalances())); } - - - - - private SpecOperation[] associateAllTokensToCollectors() { final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; From 145a04fbd55e73bdfce9fb53b769fae9739b25c6 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 8 Oct 2024 11:13:13 +0300 Subject: [PATCH 39/94] Hapi tests, custom fee assessor refactoring Signed-off-by: Zhivko Kelchev --- ...er.java => ConsensusAllowanceUpdater.java} | 36 ++- ...r.java => ConsensusCustomFeeAssessor.java} | 68 ++++-- .../ConsensusApproveAllowanceHandler.java | 13 +- .../ConsensusSubmitMessageHandler.java | 11 +- .../ConsensusApproveAllowanceTest.java | 3 +- .../bdd/suites/hip991/TopicCustomFeeBase.java | 79 +++++-- .../bdd/suites/hip991/TopicCustomFeeTest.java | 223 ++++++++++++++++-- 7 files changed, 349 insertions(+), 84 deletions(-) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{util/ConsensusApproveAllowanceHelper.java => ConsensusAllowanceUpdater.java} (93%) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{util/ConsensusCustomFeeHelper.java => ConsensusCustomFeeAssessor.java} (81%) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java similarity index 93% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java index c95a17a78dd3..24fed8ca2a33 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl.util; +package com.hedera.node.app.service.consensus.impl; import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; import static java.util.Objects.requireNonNull; @@ -26,12 +26,26 @@ import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import edu.umd.cs.findbugs.annotations.NonNull; + +import javax.inject.Inject; +import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; -public class ConsensusApproveAllowanceHelper { +// todo update this class when allowances are done!!! +@Singleton +public class ConsensusAllowanceUpdater { + + /** + * Constructs a {@link ConsensusAllowanceUpdater} instance. + */ + @Inject + public ConsensusAllowanceUpdater() { + // Needed for Dagger injection + } + + /** * Applies all changes needed for Crypto allowances from the transaction. * If the topic already has an allowance, the allowance value will be replaced with values @@ -39,7 +53,7 @@ public class ConsensusApproveAllowanceHelper { * @param topicCryptoAllowances the list of crypto allowances * @param topicStore the topic store */ - public static void applyCryptoAllowances( + public void applyCryptoAllowances( @NonNull final List topicCryptoAllowances, @NonNull final WritableTopicStore topicStore) { requireNonNull(topicCryptoAllowances); @@ -62,7 +76,7 @@ public static void applyCryptoAllowances( } } - public static void applyCryptoAllowances( + public void applyCryptoAllowances( @NonNull final TopicID topicId, @NonNull final TopicCryptoAllowance allowance, @NonNull final WritableTopicStore topicStore) { @@ -89,7 +103,7 @@ public static void applyCryptoAllowances( * @param amount the amount * @param spenderId the spender id */ - private static void updateCryptoAllowance( + private void updateCryptoAllowance( final List mutableAllowances, final long amount, final long amountPerMessage, @@ -127,7 +141,7 @@ private static void updateCryptoAllowance( * @param tokenAllowances the list of token allowances * @param topicStore the topic store */ - public static void applyFungibleTokenAllowances( + public void applyFungibleTokenAllowances( @NonNull final List tokenAllowances, @NonNull final WritableTopicStore topicStore) { requireNonNull(tokenAllowances); @@ -154,7 +168,7 @@ public static void applyFungibleTokenAllowances( /* * */ - public static void applyFungibleTokenAllowances( + public void applyFungibleTokenAllowances( @NonNull final TopicID topicId, @NonNull final TopicFungibleTokenAllowance allowance, @NonNull final WritableTopicStore topicStore) { @@ -185,7 +199,7 @@ public static void applyFungibleTokenAllowances( * @param spenderId the spender number * @param tokenId the token number */ - private static void updateTokenAllowance( + private void updateTokenAllowance( final List mutableAllowances, final long amount, final long amountPerMessage, @@ -223,7 +237,7 @@ private static void updateTokenAllowance( * @param spenderNum spender account number * @return index of the allowance if it exists, otherwise -1 */ - private static int lookupSpender( + private int lookupSpender( final List topicCryptoAllowances, final AccountID spenderNum) { for (int i = 0; i < topicCryptoAllowances.size(); i++) { final var allowance = topicCryptoAllowances.get(i); @@ -242,7 +256,7 @@ private static int lookupSpender( * @param tokenId token number * @return index of the allowance if it exists, otherwise -1 */ - private static int lookupSpenderAndToken( + private int lookupSpenderAndToken( final List topicTokenAllowances, final AccountID spenderId, final TokenID tokenId) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java similarity index 81% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java index acab8571799e..85a20ee4462f 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java @@ -14,12 +14,10 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl.util; +package com.hedera.node.app.service.consensus.impl; import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyCryptoAllowances; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyFungibleTokenAllowances; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -34,10 +32,13 @@ import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; + +import javax.inject.Inject; +import javax.inject.Singleton; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -46,13 +47,26 @@ import java.util.Map; import java.util.stream.Collectors; -public class ConsensusCustomFeeHelper { +@Singleton +public class ConsensusCustomFeeAssessor { + + private final ConsensusAllowanceUpdater allowanceUpdater; - public static List assessCustomFee(Topic topic, HandleContext context) { + /** + * Constructs a {@link ConsensusCustomFeeAssessor} instance. + */ + @Inject + public ConsensusCustomFeeAssessor(@NonNull final ConsensusAllowanceUpdater allowanceUpdater) { + // Needed for Dagger injection + this.allowanceUpdater = requireNonNull(allowanceUpdater); + } + + public List assessCustomFee(Topic topic, HandleContext context) { final List transactionBodies = new ArrayList<>(); final var payer = context.payer(); final var topicStore = context.storeFactory().writableStore(WritableTopicStore.class); + final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); // todo: allowance validation will be changed, when the storage situation is clear. @@ -76,26 +90,36 @@ public static List assessCustomFee(Topic topic, H List hbarTransfers = new ArrayList<>(); // we need to count the number of balance adjustments, // and if needed to split custom fee transfers in to two separate dispatches - final var maxTransfers = ledgerConfig.transfersMaxLen() / 2; + // todo: add explanation for maxTransfers + final var maxTransfers = ledgerConfig.transfersMaxLen() / 3; var transferCounts = 0; // build crypto transfer body for the first layer of custom fees, // if there is a second layer it will be assessed in crypto transfer handler for (ConsensusCustomFee fee : topic.customFees()) { + // check if payer is treasury or collector + if(context.payer().equals(fee.feeCollectorAccountId())) { + continue; + } + final var fixedFee = fee.fixedFeeOrThrow(); if (fixedFee.hasDenominatingTokenId()) { - final var tokenId = fixedFee.denominatingTokenId(); - validateTokenAllowance(tokenAllowanceMap, fixedFee); + final var tokenId = fixedFee.denominatingTokenIdOrThrow(); + final var tokenTreasury = tokenStore.get(tokenId).treasuryAccountIdOrThrow(); + if(context.payer().equals(tokenTreasury)) { + continue; + } + validateTokenAllowance(tokenAllowanceMap, fixedFee); tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); // update allowance values - applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); + allowanceUpdater.applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); } else { validateHbarAllowance(hbarAllowance, fixedFee); hbarTransfers = mergeTransfers( hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); // update allowance values - applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); + allowanceUpdater.applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); } transferCounts++; @@ -114,6 +138,10 @@ public static List assessCustomFee(Topic topic, H } } + if(tokenTransfers.isEmpty() && hbarTransfers.isEmpty()) { + return transactionBodies; + } + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); transactionBodies.add(syntheticBodyBuilder .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) @@ -122,7 +150,7 @@ public static List assessCustomFee(Topic topic, H return transactionBodies; } - private static void validateTokenAllowance( + private void validateTokenAllowance( Map tokenAllowanceMap, FixedFee fixedFee) { final var allowance = tokenAllowanceMap.get(fixedFee.denominatingTokenId()); validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); @@ -130,18 +158,18 @@ private static void validateTokenAllowance( validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); } - private static void validateHbarAllowance(TopicCryptoAllowance allowance, FixedFee fixedFee) { + private void validateHbarAllowance(TopicCryptoAllowance allowance, FixedFee fixedFee) { validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); } - public static void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { + public void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { // todo adjust allowance // extract the code for updating the allowance amounts from ConsensusApproveAllowanceHandler and reuse it here } - private static List buildCustomFeeHbarTransferList( + private List buildCustomFeeHbarTransferList( AccountID payer, AccountID collector, FixedFee fee) { return List.of( AccountAmount.newBuilder() @@ -154,7 +182,7 @@ private static List buildCustomFeeHbarTransferList( .build()); } - private static TokenTransferList buildCustomFeeTokenTransferList( + private TokenTransferList buildCustomFeeTokenTransferList( AccountID payer, AccountID collector, FixedFee fee) { return TokenTransferList.newBuilder() .token(fee.denominatingTokenId()) @@ -170,7 +198,7 @@ private static TokenTransferList buildCustomFeeTokenTransferList( .build(); } - private static CryptoTransferTransactionBody.Builder tokenTransfers( + private CryptoTransferTransactionBody.Builder tokenTransfers( @NonNull TokenTransferList... tokenTransferLists) { if (repeatsTokenId(tokenTransferLists)) { final Map consolidatedTokenTransfers = new LinkedHashMap<>(); @@ -178,7 +206,7 @@ private static CryptoTransferTransactionBody.Builder tokenTransfers( consolidatedTokenTransfers.merge( tokenTransferList.tokenOrThrow(), tokenTransferList, - ConsensusCustomFeeHelper::mergeTokenTransferLists); + ConsensusCustomFeeAssessor::mergeTokenTransferLists); } tokenTransferLists = consolidatedTokenTransfers.values().toArray(TokenTransferList[]::new); } @@ -205,7 +233,7 @@ private static List mergeTransfers( private static void consolidateInto( @NonNull final Map consolidated, @NonNull final List transfers) { for (final var transfer : transfers) { - consolidated.merge(transfer.accountID(), transfer, ConsensusCustomFeeHelper::mergeAdjusts); + consolidated.merge(transfer.accountID(), transfer, ConsensusCustomFeeAssessor::mergeAdjusts); } } @@ -216,7 +244,7 @@ private static AccountAmount mergeAdjusts(@NonNull final AccountAmount from, @No .build(); } - private static boolean repeatsTokenId(@NonNull final TokenTransferList[] tokenTransferList) { + private boolean repeatsTokenId(@NonNull final TokenTransferList[] tokenTransferList) { return tokenTransferList.length > 1 && Arrays.stream(tokenTransferList) .map(TokenTransferList::token) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java index c5f40a89ed12..137bdd62ef0b 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java @@ -17,13 +17,12 @@ package com.hedera.node.app.service.consensus.impl.handlers; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyCryptoAllowances; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyFungibleTokenAllowances; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.consensus.impl.ConsensusAllowanceUpdater; import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; @@ -42,15 +41,19 @@ @Singleton public class ConsensusApproveAllowanceHandler implements TransactionHandler { private final ConsensusAllowancesValidator validator; + private final ConsensusAllowanceUpdater updater; /** * Default constructor for injection. * @param allowancesValidator allowances validator */ @Inject - public ConsensusApproveAllowanceHandler(@NonNull final ConsensusAllowancesValidator allowancesValidator) { + public ConsensusApproveAllowanceHandler( + @NonNull final ConsensusAllowancesValidator allowancesValidator, + @NonNull final ConsensusAllowanceUpdater allowanceUpdater) { requireNonNull(allowancesValidator); this.validator = allowancesValidator; + this.updater = allowanceUpdater; } @Override @@ -119,7 +122,7 @@ private void approveAllowance(@NonNull final HandleContext context, @NonNull fin final var tokenAllowances = op.consensusTokenFeeScheduleAllowances(); /* --- Apply changes to state --- */ - applyCryptoAllowances(cryptoAllowances, topicStore); - applyFungibleTokenAllowances(tokenAllowances, topicStore); + updater.applyCryptoAllowances(cryptoAllowances, topicStore); + updater.applyFungibleTokenAllowances(tokenAllowances, topicStore); } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index c5e3be9d7aca..e77aa2db599d 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -46,7 +46,7 @@ import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; -import com.hedera.node.app.service.consensus.impl.util.ConsensusCustomFeeHelper; +import com.hedera.node.app.service.consensus.impl.ConsensusCustomFeeAssessor; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; @@ -75,10 +75,11 @@ @Singleton public class ConsensusSubmitMessageHandler implements TransactionHandler { public static final long RUNNING_HASH_VERSION = 3L; + private final ConsensusCustomFeeAssessor customFeeAssessor; @Inject - public ConsensusSubmitMessageHandler() { - // Exists for injection + public ConsensusSubmitMessageHandler(@NonNull ConsensusCustomFeeAssessor customFeeAssessor) { + this.customFeeAssessor = requireNonNull(customFeeAssessor); } @Override @@ -145,7 +146,7 @@ public void handle(@NonNull final HandleContext handleContext) { } if (!topic.customFees().isEmpty() && !payerIsFeeExempted) { // validate and create synthetic body - final var syntheticBodies = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); + final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); for (final var syntheticBody : syntheticBodies) { // dispatch crypto transfer var record = handleContext.dispatchChildTransaction( @@ -159,7 +160,7 @@ var record = handleContext.dispatchChildTransaction( HandleContext.ConsensusThrottling.OFF); validateTrue(record.status().equals(SUCCESS), record.status()); // update total allowances - ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + customFeeAssessor.adjustAllowance(syntheticBody); } } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java index 912f1628e73f..76295710768a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java @@ -40,6 +40,7 @@ import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; +import com.hedera.node.app.service.consensus.impl.ConsensusAllowanceUpdater; import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -58,7 +59,7 @@ public class ConsensusApproveAllowanceTest extends ConsensusTestBase { @BeforeEach void setUp() { - subject = new ConsensusApproveAllowanceHandler(new ConsensusAllowancesValidator()); + subject = new ConsensusApproveAllowanceHandler(new ConsensusAllowancesValidator(), new ConsensusAllowanceUpdater()); refreshStoresWithCurrentTopicOnlyInReadable(); given(handleContext.savepointStack()).willReturn(stack); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index b9ae1d16998e..f0a94bcd03d4 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -21,6 +21,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; @@ -31,6 +32,7 @@ import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.TokenType; import java.util.ArrayList; +import java.util.List; public class TopicCustomFeeBase { protected static final String TOPIC = "topic"; @@ -43,8 +45,14 @@ public class TopicCustomFeeBase { /* Submit message entities */ protected static final String SUBMITTER = "submitter"; protected static final String TOKEN_TREASURY = "tokenTreasury"; + protected static final String DENOM_TREASURY = "denomTreasury"; protected static final String BASE_TOKEN = "baseToken"; - protected static final String MULTI_LAYER_FEE_PREFIX = "multiLayerFeePrefix_"; + + /* tokens with multilayer fees */ + protected static final String TOKEN_PREFIX = "token_"; + protected static final String COLLECTOR_PREFIX = "collector_"; + protected static final String DENOM_TOKEN_PREFIX = "denomToken_"; + // This key is truly invalid, as all Ed25519 public keys must be 32 bytes long protected static final Key STRUCTURALLY_INVALID_KEY = Key.newBuilder().setEd25519(ByteString.fromHex("ff")).build(); @@ -67,28 +75,63 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { } /** - * Create and transfer multiple tokens with fixed hbar custom fee to account. - * @param account account to transfer tokens + * Create and transfer multiple tokens with 2 layer custom fees to given account. + * + * @param owner account to transfer tokens * @param numberOfTokens the count of tokens to be transferred * @return array of spec operations */ - protected SpecOperation[] transferMultiLayerFeeTokensTo(String account, int numberOfTokens) { - final var treasury = MULTI_LAYER_FEE_PREFIX + TOKEN_TREASURY; - final var list = new ArrayList(); - list.add(cryptoCreate(treasury)); + protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int numberOfTokens) { + final var specOperations = new ArrayList(); + specOperations.add(cryptoCreate(DENOM_TREASURY)); + specOperations.add(cryptoCreate(TOKEN_TREASURY)); for (int i = 0; i < numberOfTokens; i++) { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_" + i; - final var collectorName = MULTI_LAYER_FEE_PREFIX + "collector_" + i; - list.add(cryptoCreate(collectorName).balance(0L)); - list.add(tokenCreate(tokenName) - .tokenType(TokenType.FUNGIBLE_COMMON) - .treasury(treasury) - .initialSupply(500L) - .withCustom(fixedHbarFee(ONE_HBAR, collectorName))); - list.add(tokenAssociate(account, tokenName)); - list.add(cryptoTransfer(moving(500L, tokenName).between(treasury, account))); + final var tokenName = TOKEN_PREFIX + i; + specOperations.addAll(createTokenWith2LayerFee(owner, tokenName, false)); } - return list.toArray(new SpecOperation[0]); + return specOperations.toArray(new SpecOperation[0]); + } + + + /** + * + * + * + * @param owner + * @param tokenName + * @param createTreasury + * @return + */ + protected static List createTokenWith2LayerFee(String owner, String tokenName, boolean createTreasury) { + final var specOperations = new ArrayList(); + final var collectorName = COLLECTOR_PREFIX + tokenName; + final var denomToken = DENOM_TOKEN_PREFIX + tokenName; + // if we generate multiple tokens, there will be no need to create treasury every time we create new token + if (createTreasury) { + specOperations.add(cryptoCreate(DENOM_TREASURY)); + specOperations.add(cryptoCreate(TOKEN_TREASURY)); + } + // create first common collector + specOperations.add(cryptoCreate(collectorName).balance(0L)); + // create denomination token with hbar fee + specOperations.add(tokenCreate(denomToken) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(DENOM_TREASURY) + .withCustom(fixedHbarFee(ONE_HBAR, collectorName))); + // associate the denomination token with the collector + specOperations.add(tokenAssociate(collectorName, denomToken)); + // create the token with fixed HTS fee + specOperations.add(tokenCreate(tokenName) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .withCustom(fixedHtsFee(1, denomToken, collectorName))); + // associate the owner with the two new tokens + specOperations.add(tokenAssociate(owner, tokenName)); + specOperations.add(tokenAssociate(owner, denomToken)); + // transfer the tokens to the owner + specOperations.add(cryptoTransfer(moving(100L, tokenName).between(TOKEN_TREASURY, owner))); + specOperations.add(cryptoTransfer(moving(100L, denomToken).between(DENOM_TREASURY, owner))); + return specOperations; } protected static SpecOperation[] newNamedKeysForFEKL(int count) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index bca220efb480..453d3a0102ee 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -23,6 +23,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; @@ -30,6 +31,8 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.logIt; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; @@ -44,6 +47,7 @@ import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; import com.hedera.services.bdd.spec.SpecOperation; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hederahashgraph.api.proto.java.TokenType; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; @@ -361,6 +365,7 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { @HapiTest @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") + // TOPIC_FEE_104 final Stream messageSubmitToPublicTopicWithFee1Hbar() { final var collector = "collector"; final var submitter = "submitter"; @@ -377,6 +382,7 @@ final Stream messageSubmitToPublicTopicWithFee1Hbar() { @HapiTest @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") + // TOPIC_FEE_105 final Stream messageSubmitToPublicTopicWithFee1token() { final var collector = "collector"; return hapiTest( @@ -391,34 +397,39 @@ final Stream messageSubmitToPublicTopicWithFee1token() { } @HapiTest - @DisplayName("MessageSubmit to a public topic with 2 layer fee") - final Stream messageSubmitToPublicTopicWith2layerFee() { + @DisplayName("MessageSubmit to a public topic with 3 layer fee") + // TOPIC_FEE_106 + final Stream messageSubmitToPublicTopicWith3layerFee() { final var topicFeeCollector = "collector"; - final var token = MULTI_LAYER_FEE_PREFIX + "token_0"; - final var tokenFeeCollector = MULTI_LAYER_FEE_PREFIX + "collector_0"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; return hapiTest(flattened( - cryptoCreate(topicFeeCollector).balance(0L), // create denomination token and transfer it to the submitter - transferMultiLayerFeeTokensTo(SUBMITTER, 1), - tokenAssociate(topicFeeCollector, token), + createTokenWith2LayerFee(SUBMITTER, token, true), // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance approveTopicAllowance() .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) .payingWith(SUBMITTER), + // submit message submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), // assert token fee collector balance - getAccountBalance(tokenFeeCollector).hasTinyBars(ONE_HBAR))); + getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 1).hasTinyBars(ONE_HBAR))); } @HapiTest - @DisplayName("MessageSubmit to a public topic with 10 different 2 layer fees") + @DisplayName("MessageSubmit to a public topic with 10 different 3 layer fees") + // TOPIC_FEE_108 final Stream messageSubmitToPublicTopicWith10different2layerFees() { return hapiTest(flattened( // create 9 denomination tokens and transfer them to the submitter - transferMultiLayerFeeTokensTo(SUBMITTER, 9), + createMultipleTokensWith2LayerFees(SUBMITTER, 9), // create 9 collectors and associate them with tokens associateAllTokensToCollectors(), // create topic with 10 multilayer fees - 9 HTS + 1 HBAR @@ -429,54 +440,218 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() assertAllCollectorsBalances())); } + // TOPIC_FEE_108 private SpecOperation[] associateAllTokensToCollectors() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; final var associateTokensToCollectors = new ArrayList(); for (int i = 0; i < 9; i++) { associateTokensToCollectors.add( cryptoCreate(collectorName + i).balance(0L)); - associateTokensToCollectors.add(tokenAssociate(collectorName + i, tokenName + i)); + associateTokensToCollectors.add(tokenAssociate(collectorName + i, TOKEN_PREFIX + i)); } return associateTokensToCollectors.toArray(SpecOperation[]::new); } - + // TOPIC_FEE_108 private SpecOperation createTopicWith10Different2layerFees() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; final var topicCreateOp = createTopic(TOPIC); for (int i = 0; i < 9; i++) { - topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, tokenName + i, collectorName + i)); + topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, TOKEN_PREFIX + i, collectorName + i)); } // add one hbar custom fee topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); return topicCreateOp; } - + // TOPIC_FEE_108 private SpecOperation approveTopicAllowanceForAllFees() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); for (int i = 0; i < 9; i++) { - approveAllowance.addTokenAllowance(SUBMITTER, tokenName + i, TOPIC, 100, 1); + approveAllowance.addTokenAllowance(SUBMITTER, TOKEN_PREFIX + i, TOPIC, 100, 1); } approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); return approveAllowance; } - + // TOPIC_FEE_108 private SpecOperation[] assertAllCollectorsBalances() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; - final var assertBalances = new ArrayList(); - + // assert token balances for (int i = 0; i < 9; i++) { - assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(tokenName + i, 1)); + assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(TOKEN_PREFIX + i, 1)); } - // add assert for hbar fee + // add assert for hbar assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); return assertBalances.toArray(SpecOperation[]::new); } + + @HapiTest + @DisplayName("Treasury submit to a public topic with 3 layer fees") + // TOPIC_FEE_109 + final Stream treasurySubmitToPublicTopicWith3layerFees() { + final var topicFeeCollector = "collector"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; + + return hapiTest(flattened( + // create denomination token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + // submit message + submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); + } + + + @HapiTest + @DisplayName("Treasury second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_110 + final Stream treasuryOfSecondLayerSubmitToPublic() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(DENOM_TREASURY, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, DENOM_TREASURY)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(DENOM_TREASURY, token, TOPIC, 100, 1) + .payingWith(DENOM_TREASURY), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), + + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); + } + + + @HapiTest + @DisplayName("Collector submit to a public topic with 3 layer fees") + // TOPIC_FEE_111 + final Stream collectorSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // transfer one token to the collector, to be able to pay the fee + cryptoCreate(topicFeeCollector).balance(ONE_HBAR), + tokenAssociate(topicFeeCollector, token), + + // create topic + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(topicFeeCollector, token, TOPIC, 100, 1) + .payingWith(topicFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), + + // assert balances + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + getAccountBalance(COLLECTOR_PREFIX+token).hasTokenBalance(denomToken, 0))); + } + + + @HapiTest + @DisplayName("Collector of second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_112 + final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var secondLayerFeeCollector = COLLECTOR_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(secondLayerFeeCollector, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, secondLayerFeeCollector)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(secondLayerFeeCollector, token, TOPIC, 100, 1) + .payingWith(secondLayerFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), + + // assert topic fee collector balance - only first layer fee should be paid + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // token fee collector should have 1 token from the first transfer and 0 from msg submit + getAccountBalance(secondLayerFeeCollector).hasTokenBalance(denomToken, 1))); + } + + @HapiTest + @DisplayName("Another collector submit message to a topic with a fee") + // TOPIC_FEE_113 + final Stream anotherCollectorSubmitMessageToATopicWithAFee() { + final var collector = "collector"; + final var anotherToken = "anotherToken"; + final var anotherCollector = COLLECTOR_PREFIX + anotherToken; + return hapiTest(flattened( + // create another token with fixed fee + createTokenWith2LayerFee(SUBMITTER, anotherToken, true), + tokenAssociate(anotherCollector, BASE_TOKEN), + cryptoTransfer( + moving(100, BASE_TOKEN).between(SUBMITTER, anotherCollector), + TokenMovement.movingHbar(ONE_HBAR).between(SUBMITTER, anotherCollector) + ), + // create topic + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + // add allowance and submit with another collector + approveTopicAllowance() + .addTokenAllowance(anotherCollector, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(anotherCollector), + submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), + // the fee was paid + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1) + )); + } + + } } From 85986915bdf2bc485d55cef17c335f1180a21a6c Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 8 Oct 2024 14:12:07 +0300 Subject: [PATCH 40/94] Refactor Signed-off-by: Zhivko Kelchev --- .../ConsensusApproveAllowanceHandler.java | 2 +- .../ConsensusSubmitMessageHandler.java | 2 +- .../customfee}/ConsensusAllowanceUpdater.java | 12 +- .../ConsensusCustomFeeAssessor.java | 26 +- .../src/main/java/module-info.java | 5 +- .../ConsensusApproveAllowanceTest.java | 5 +- .../bdd/suites/hip991/TopicCustomFeeBase.java | 4 +- .../TopicCustomFeeSubmitMessageTest.java | 429 ++++++++++++++++++ .../bdd/suites/hip991/TopicCustomFeeTest.java | 399 ---------------- 9 files changed, 457 insertions(+), 427 deletions(-) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{ => handlers/customfee}/ConsensusAllowanceUpdater.java (97%) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{ => handlers/customfee}/ConsensusCustomFeeAssessor.java (93%) create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java index 137bdd62ef0b..1f63b1d9e303 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java @@ -22,7 +22,7 @@ import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; -import com.hedera.node.app.service.consensus.impl.ConsensusAllowanceUpdater; +import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusAllowanceUpdater; import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index e77aa2db599d..6f6b714397e3 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -45,8 +45,8 @@ import com.hedera.node.app.hapi.utils.CommonPbjConverters; import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusCustomFeeAssessor; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; -import com.hedera.node.app.service.consensus.impl.ConsensusCustomFeeAssessor; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java similarity index 97% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java index 24fed8ca2a33..f7b613d3584a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl; +package com.hedera.node.app.service.consensus.impl.handlers.customfee; import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; import static java.util.Objects.requireNonNull; @@ -26,12 +26,12 @@ import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import edu.umd.cs.findbugs.annotations.NonNull; - -import javax.inject.Inject; -import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; // todo update this class when allowances are done!!! @Singleton @@ -45,7 +45,6 @@ public ConsensusAllowanceUpdater() { // Needed for Dagger injection } - /** * Applies all changes needed for Crypto allowances from the transaction. * If the topic already has an allowance, the allowance value will be replaced with values @@ -237,8 +236,7 @@ private void updateTokenAllowance( * @param spenderNum spender account number * @return index of the allowance if it exists, otherwise -1 */ - private int lookupSpender( - final List topicCryptoAllowances, final AccountID spenderNum) { + private int lookupSpender(final List topicCryptoAllowances, final AccountID spenderNum) { for (int i = 0; i < topicCryptoAllowances.size(); i++) { final var allowance = topicCryptoAllowances.get(i); if (allowance.spenderIdOrThrow().equals(spenderNum)) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java similarity index 93% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 85a20ee4462f..81af6e85628a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl; +package com.hedera.node.app.service.consensus.impl.handlers.customfee; import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; @@ -32,13 +32,11 @@ import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; - -import javax.inject.Inject; -import javax.inject.Singleton; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -46,6 +44,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.inject.Singleton; @Singleton public class ConsensusCustomFeeAssessor { @@ -98,7 +98,7 @@ public List assessCustomFee(Topic topic, HandleCo // if there is a second layer it will be assessed in crypto transfer handler for (ConsensusCustomFee fee : topic.customFees()) { // check if payer is treasury or collector - if(context.payer().equals(fee.feeCollectorAccountId())) { + if (context.payer().equals(fee.feeCollectorAccountId())) { continue; } @@ -106,14 +106,15 @@ public List assessCustomFee(Topic topic, HandleCo if (fixedFee.hasDenominatingTokenId()) { final var tokenId = fixedFee.denominatingTokenIdOrThrow(); final var tokenTreasury = tokenStore.get(tokenId).treasuryAccountIdOrThrow(); - if(context.payer().equals(tokenTreasury)) { + if (context.payer().equals(tokenTreasury)) { continue; } validateTokenAllowance(tokenAllowanceMap, fixedFee); tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); // update allowance values - allowanceUpdater.applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); + allowanceUpdater.applyFungibleTokenAllowances( + topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); } else { validateHbarAllowance(hbarAllowance, fixedFee); hbarTransfers = mergeTransfers( @@ -138,7 +139,7 @@ public List assessCustomFee(Topic topic, HandleCo } } - if(tokenTransfers.isEmpty() && hbarTransfers.isEmpty()) { + if (tokenTransfers.isEmpty() && hbarTransfers.isEmpty()) { return transactionBodies; } @@ -169,8 +170,7 @@ public void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { // extract the code for updating the allowance amounts from ConsensusApproveAllowanceHandler and reuse it here } - private List buildCustomFeeHbarTransferList( - AccountID payer, AccountID collector, FixedFee fee) { + private List buildCustomFeeHbarTransferList(AccountID payer, AccountID collector, FixedFee fee) { return List.of( AccountAmount.newBuilder() .accountID(payer) @@ -182,8 +182,7 @@ private List buildCustomFeeHbarTransferList( .build()); } - private TokenTransferList buildCustomFeeTokenTransferList( - AccountID payer, AccountID collector, FixedFee fee) { + private TokenTransferList buildCustomFeeTokenTransferList(AccountID payer, AccountID collector, FixedFee fee) { return TokenTransferList.newBuilder() .token(fee.denominatingTokenId()) .transfers( @@ -198,8 +197,7 @@ private TokenTransferList buildCustomFeeTokenTransferList( .build(); } - private CryptoTransferTransactionBody.Builder tokenTransfers( - @NonNull TokenTransferList... tokenTransferLists) { + private CryptoTransferTransactionBody.Builder tokenTransfers(@NonNull TokenTransferList... tokenTransferLists) { if (repeatsTokenId(tokenTransferLists)) { final Map consolidatedTokenTransfers = new LinkedHashMap<>(); for (final var tokenTransferList : tokenTransferLists) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java index 5665b34dc129..40f635f1a72b 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java @@ -23,7 +23,10 @@ exports com.hedera.node.app.service.consensus.impl to com.hedera.node.app, com.hedera.node.test.clients; - exports com.hedera.node.app.service.consensus.impl.handlers; + exports com.hedera.node.app.service.consensus.impl.handlers to + com.hedera.node.app; + exports com.hedera.node.app.service.consensus.impl.handlers.customfee to + com.hedera.node.app; exports com.hedera.node.app.service.consensus.impl.records; exports com.hedera.node.app.service.consensus.impl.schemas; exports com.hedera.node.app.service.consensus.impl.validators; diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java index 76295710768a..d352538014d5 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java @@ -40,7 +40,7 @@ import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; -import com.hedera.node.app.service.consensus.impl.ConsensusAllowanceUpdater; +import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusAllowanceUpdater; import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -59,7 +59,8 @@ public class ConsensusApproveAllowanceTest extends ConsensusTestBase { @BeforeEach void setUp() { - subject = new ConsensusApproveAllowanceHandler(new ConsensusAllowancesValidator(), new ConsensusAllowanceUpdater()); + subject = new ConsensusApproveAllowanceHandler( + new ConsensusAllowancesValidator(), new ConsensusAllowanceUpdater()); refreshStoresWithCurrentTopicOnlyInReadable(); given(handleContext.savepointStack()).willReturn(stack); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index f0a94bcd03d4..74f25d0b051b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -92,7 +92,6 @@ protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int n return specOperations.toArray(new SpecOperation[0]); } - /** * * @@ -102,7 +101,8 @@ protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int n * @param createTreasury * @return */ - protected static List createTokenWith2LayerFee(String owner, String tokenName, boolean createTreasury) { + protected static List createTokenWith2LayerFee( + String owner, String tokenName, boolean createTreasury) { final var specOperations = new ArrayList(); final var collectorName = COLLECTOR_PREFIX + tokenName; final var denomToken = DENOM_TOKEN_PREFIX + tokenName; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java new file mode 100644 index 000000000000..ad4193f30a9a --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -0,0 +1,429 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.suites.hip991; + +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; +import static com.hedera.services.bdd.suites.HapiSuite.flattened; + +import com.hedera.services.bdd.junit.HapiTest; +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.SpecOperation; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; + +@HapiTestLifecycle +@DisplayName("Submit message") +public class TopicCustomFeeSubmitMessageTest extends TopicCustomFeeBase { + // @HapiTest + // @DisplayName("submit") + // final Stream submitMessage() { + // final var collector = "collector"; + // final var payer = "submitter"; + // final var treasury = "treasury"; + // final var token = "testToken"; + // final var secondToken = "secondToken"; + // final var denomToken = "denomToken"; + // final var simpleKey = "simpleKey"; + // final var simpleKey2 = "simpleKey2"; + // final var invalidKey = "invalidKey"; + // final var threshKey = "threshKey"; + // + // return hapiTest( + // // create keys + // newKeyNamed(invalidKey), + // newKeyNamed(simpleKey), + // newKeyNamed(simpleKey2), + // newKeyNamed(threshKey) + // .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) + // .signedWith(sigs(simpleKey2, simpleKey))), + // // create accounts and denomination token + // cryptoCreate(collector).balance(0L), + // cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), + // cryptoCreate(treasury), + // tokenCreate(denomToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, denomToken), + // tokenAssociate(payer, denomToken), + // tokenCreate(token) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .withCustom(fixedHtsFee(1, denomToken, collector)) + // .initialSupply(500), + // tokenCreate(secondToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, token, secondToken), + // tokenAssociate(payer, token, secondToken), + // cryptoTransfer( + // moving(2, token).between(treasury, payer), + // moving(1, secondToken).between(treasury, payer), + // moving(1, denomToken).between(treasury, payer)), + // + // // create topic with custom fees + // createTopic(TOPIC) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // token, + // // collector)) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // secondToken, + // // collector)) + // .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + // .feeExemptKeys(threshKey) + // .hasKnownStatus(SUCCESS), + // + // // add allowance + // approveTopicAllowance() + // .payingWith(payer) + // .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), + // + // // submit message + // submitMessageTo(TOPIC) + // .message("TEST") + // .signedBy(invalidKey, payer) + // .payingWith(payer) + // .via("submit"), + // + // // check records + // getTxnRecord("submit").andAllChildRecords().logged(), + // + // // assert balances + // getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + // // .hasTokenBalance(token, 2) + // // .hasTokenBalance(denomToken,1) + // // .hasTokenBalance(secondToken, 1), + // // getAccountBalance(payer) + // // .hasTokenBalance(token, 0) + // // .hasTokenBalance(secondToken, 0)); + // } + + @Nested + @DisplayName("Positive scenarios") + class SubmitMessagesPositiveScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(associateFeeTokensAndSubmitter()); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") + // TOPIC_FEE_104 + final Stream messageSubmitToPublicTopicWithFee1Hbar() { + final var collector = "collector"; + final var submitter = "submitter"; + return hapiTest( + cryptoCreate(collector).balance(0L), + cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(submitter), + submitMessageTo(TOPIC).message("TEST").payingWith(submitter), + getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") + // TOPIC_FEE_105 + final Stream messageSubmitToPublicTopicWithFee1token() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 3 layer fee") + // TOPIC_FEE_106 + final Stream messageSubmitToPublicTopicWith3layerFee() { + final var topicFeeCollector = "collector"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; + return hapiTest(flattened( + // create denomination token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + // submit message + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector) + .hasTokenBalance(denomToken, 1) + .hasTinyBars(ONE_HBAR))); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 10 different 3 layer fees") + // TOPIC_FEE_108 + final Stream messageSubmitToPublicTopicWith10different2layerFees() { + return hapiTest(flattened( + // create 9 denomination tokens and transfer them to the submitter + createMultipleTokensWith2LayerFees(SUBMITTER, 9), + // create 9 collectors and associate them with tokens + associateAllTokensToCollectors(), + // create topic with 10 multilayer fees - 9 HTS + 1 HBAR + createTopicWith10Different2layerFees(), + approveTopicAllowanceForAllFees(), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + // assert topic fee collector balance + assertAllCollectorsBalances())); + } + + // TOPIC_FEE_108 + private SpecOperation[] associateAllTokensToCollectors() { + final var collectorName = "collector_"; + final var associateTokensToCollectors = new ArrayList(); + for (int i = 0; i < 9; i++) { + associateTokensToCollectors.add(cryptoCreate(collectorName + i).balance(0L)); + associateTokensToCollectors.add(tokenAssociate(collectorName + i, TOKEN_PREFIX + i)); + } + return associateTokensToCollectors.toArray(SpecOperation[]::new); + } + // TOPIC_FEE_108 + private SpecOperation createTopicWith10Different2layerFees() { + final var collectorName = "collector_"; + final var topicCreateOp = createTopic(TOPIC); + for (int i = 0; i < 9; i++) { + topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, TOKEN_PREFIX + i, collectorName + i)); + } + // add one hbar custom fee + topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); + return topicCreateOp; + } + // TOPIC_FEE_108 + private SpecOperation approveTopicAllowanceForAllFees() { + final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); + + for (int i = 0; i < 9; i++) { + approveAllowance.addTokenAllowance(SUBMITTER, TOKEN_PREFIX + i, TOPIC, 100, 1); + } + approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); + return approveAllowance; + } + // TOPIC_FEE_108 + private SpecOperation[] assertAllCollectorsBalances() { + final var collectorName = "collector_"; + final var assertBalances = new ArrayList(); + // assert token balances + for (int i = 0; i < 9; i++) { + assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(TOKEN_PREFIX + i, 1)); + } + // add assert for hbar + assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); + return assertBalances.toArray(SpecOperation[]::new); + } + + @HapiTest + @DisplayName("Treasury submit to a public topic with 3 layer fees") + // TOPIC_FEE_109 + final Stream treasurySubmitToPublicTopicWith3layerFees() { + final var topicFeeCollector = "collector"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; + + return hapiTest(flattened( + // create denomination token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + // submit message + submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector) + .hasTokenBalance(denomToken, 0) + .hasTinyBars(0))); + } + + @HapiTest + @DisplayName("Treasury second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_110 + final Stream treasuryOfSecondLayerSubmitToPublic() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(DENOM_TREASURY, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, DENOM_TREASURY)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(DENOM_TREASURY, token, TOPIC, 100, 1) + .payingWith(DENOM_TREASURY), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), + + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(topicFeeCollector) + .hasTokenBalance(denomToken, 0) + .hasTinyBars(0))); + } + + @HapiTest + @DisplayName("Collector submit to a public topic with 3 layer fees") + // TOPIC_FEE_111 + final Stream collectorSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // transfer one token to the collector, to be able to pay the fee + cryptoCreate(topicFeeCollector).balance(ONE_HBAR), + tokenAssociate(topicFeeCollector, token), + + // create topic + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(topicFeeCollector, token, TOPIC, 100, 1) + .payingWith(topicFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), + + // assert balances + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + getAccountBalance(COLLECTOR_PREFIX + token).hasTokenBalance(denomToken, 0))); + } + + @HapiTest + @DisplayName("Collector of second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_112 + final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var secondLayerFeeCollector = COLLECTOR_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(secondLayerFeeCollector, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, secondLayerFeeCollector)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(secondLayerFeeCollector, token, TOPIC, 100, 1) + .payingWith(secondLayerFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), + + // assert topic fee collector balance - only first layer fee should be paid + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // token fee collector should have 1 token from the first transfer and 0 from msg submit + getAccountBalance(secondLayerFeeCollector).hasTokenBalance(denomToken, 1))); + } + + @HapiTest + @DisplayName("Another collector submit message to a topic with a fee") + // TOPIC_FEE_113 + final Stream anotherCollectorSubmitMessageToATopicWithAFee() { + final var collector = "collector"; + final var anotherToken = "anotherToken"; + final var anotherCollector = COLLECTOR_PREFIX + anotherToken; + return hapiTest(flattened( + // create another token with fixed fee + createTokenWith2LayerFee(SUBMITTER, anotherToken, true), + tokenAssociate(anotherCollector, BASE_TOKEN), + cryptoTransfer( + moving(100, BASE_TOKEN).between(SUBMITTER, anotherCollector), + TokenMovement.movingHbar(ONE_HBAR).between(SUBMITTER, anotherCollector)), + // create topic + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + // add allowance and submit with another collector + approveTopicAllowance() + .addTokenAllowance(anotherCollector, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(anotherCollector), + submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), + // the fee was paid + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1))); + } + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 453d3a0102ee..3a382e899577 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -17,26 +17,20 @@ package com.hedera.services.bdd.suites.hip991; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; -import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.logIt; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; -import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.flattened; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; @@ -46,11 +40,8 @@ import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; -import com.hedera.services.bdd.spec.SpecOperation; -import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hederahashgraph.api.proto.java.TokenType; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; @@ -265,396 +256,6 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { } } - @Nested - @DisplayName("Submit message") - class SubmitMessage { - - // @HapiTest - // @DisplayName("submit") - // final Stream submitMessage() { - // final var collector = "collector"; - // final var payer = "submitter"; - // final var treasury = "treasury"; - // final var token = "testToken"; - // final var secondToken = "secondToken"; - // final var denomToken = "denomToken"; - // final var simpleKey = "simpleKey"; - // final var simpleKey2 = "simpleKey2"; - // final var invalidKey = "invalidKey"; - // final var threshKey = "threshKey"; - // - // return hapiTest( - // // create keys - // newKeyNamed(invalidKey), - // newKeyNamed(simpleKey), - // newKeyNamed(simpleKey2), - // newKeyNamed(threshKey) - // .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) - // .signedWith(sigs(simpleKey2, simpleKey))), - // // create accounts and denomination token - // cryptoCreate(collector).balance(0L), - // cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), - // cryptoCreate(treasury), - // tokenCreate(denomToken) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .initialSupply(500), - // tokenAssociate(collector, denomToken), - // tokenAssociate(payer, denomToken), - // tokenCreate(token) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .withCustom(fixedHtsFee(1, denomToken, collector)) - // .initialSupply(500), - // tokenCreate(secondToken) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .initialSupply(500), - // tokenAssociate(collector, token, secondToken), - // tokenAssociate(payer, token, secondToken), - // cryptoTransfer( - // moving(2, token).between(treasury, payer), - // moving(1, secondToken).between(treasury, payer), - // moving(1, denomToken).between(treasury, payer)), - // - // // create topic with custom fees - // createTopic(TOPIC) - // // .withConsensusCustomFee(fixedConsensusHtsFee(1, - // token, - // // collector)) - // // .withConsensusCustomFee(fixedConsensusHtsFee(1, - // secondToken, - // // collector)) - // .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) - // .feeExemptKeys(threshKey) - // .hasKnownStatus(SUCCESS), - // - // // add allowance - // approveTopicAllowance() - // .payingWith(payer) - // .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), - // - // // submit message - // submitMessageTo(TOPIC) - // .message("TEST") - // .signedBy(invalidKey, payer) - // .payingWith(payer) - // .via("submit"), - // - // // check records - // getTxnRecord("submit").andAllChildRecords().logged(), - // - // // assert balances - // getAccountBalance(collector).hasTinyBars(ONE_HBAR)); - // // .hasTokenBalance(token, 2) - // // .hasTokenBalance(denomToken,1) - // // .hasTokenBalance(secondToken, 1), - // // getAccountBalance(payer) - // // .hasTokenBalance(token, 0) - // // .hasTokenBalance(secondToken, 0)); - // } - - @Nested - @DisplayName("Positive scenarios") - class SubmitMessagesPositiveScenarios { - - @BeforeAll - static void beforeAll(@NonNull final TestLifecycle lifecycle) { - lifecycle.doAdhoc(associateFeeTokensAndSubmitter()); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") - // TOPIC_FEE_104 - final Stream messageSubmitToPublicTopicWithFee1Hbar() { - final var collector = "collector"; - final var submitter = "submitter"; - return hapiTest( - cryptoCreate(collector).balance(0L), - cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - approveTopicAllowance() - .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(submitter), - submitMessageTo(TOPIC).message("TEST").payingWith(submitter), - getAccountBalance(collector).hasTinyBars(ONE_HBAR)); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") - // TOPIC_FEE_105 - final Stream messageSubmitToPublicTopicWithFee1token() { - final var collector = "collector"; - return hapiTest( - cryptoCreate(collector), - tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, BASE_TOKEN, TOPIC, 100, 1) - .payingWith(SUBMITTER), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), - getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with 3 layer fee") - // TOPIC_FEE_106 - final Stream messageSubmitToPublicTopicWith3layerFee() { - final var topicFeeCollector = "collector"; - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var tokenFeeCollector = COLLECTOR_PREFIX + token; - return hapiTest(flattened( - // create denomination token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - // create topic with multilayer fee - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) - .payingWith(SUBMITTER), - // submit message - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), - // assert topic fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), - // assert token fee collector balance - getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 1).hasTinyBars(ONE_HBAR))); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with 10 different 3 layer fees") - // TOPIC_FEE_108 - final Stream messageSubmitToPublicTopicWith10different2layerFees() { - return hapiTest(flattened( - // create 9 denomination tokens and transfer them to the submitter - createMultipleTokensWith2LayerFees(SUBMITTER, 9), - // create 9 collectors and associate them with tokens - associateAllTokensToCollectors(), - // create topic with 10 multilayer fees - 9 HTS + 1 HBAR - createTopicWith10Different2layerFees(), - approveTopicAllowanceForAllFees(), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), - // assert topic fee collector balance - assertAllCollectorsBalances())); - } - - // TOPIC_FEE_108 - private SpecOperation[] associateAllTokensToCollectors() { - final var collectorName = "collector_"; - final var associateTokensToCollectors = new ArrayList(); - for (int i = 0; i < 9; i++) { - associateTokensToCollectors.add( - cryptoCreate(collectorName + i).balance(0L)); - associateTokensToCollectors.add(tokenAssociate(collectorName + i, TOKEN_PREFIX + i)); - } - return associateTokensToCollectors.toArray(SpecOperation[]::new); - } - // TOPIC_FEE_108 - private SpecOperation createTopicWith10Different2layerFees() { - final var collectorName = "collector_"; - final var topicCreateOp = createTopic(TOPIC); - for (int i = 0; i < 9; i++) { - topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, TOKEN_PREFIX + i, collectorName + i)); - } - // add one hbar custom fee - topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); - return topicCreateOp; - } - // TOPIC_FEE_108 - private SpecOperation approveTopicAllowanceForAllFees() { - final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); - - for (int i = 0; i < 9; i++) { - approveAllowance.addTokenAllowance(SUBMITTER, TOKEN_PREFIX + i, TOPIC, 100, 1); - } - approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); - return approveAllowance; - } - // TOPIC_FEE_108 - private SpecOperation[] assertAllCollectorsBalances() { - final var collectorName = "collector_"; - final var assertBalances = new ArrayList(); - // assert token balances - for (int i = 0; i < 9; i++) { - assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(TOKEN_PREFIX + i, 1)); - } - // add assert for hbar - assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); - return assertBalances.toArray(SpecOperation[]::new); - } - - @HapiTest - @DisplayName("Treasury submit to a public topic with 3 layer fees") - // TOPIC_FEE_109 - final Stream treasurySubmitToPublicTopicWith3layerFees() { - final var topicFeeCollector = "collector"; - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var tokenFeeCollector = COLLECTOR_PREFIX + token; - - return hapiTest(flattened( - // create denomination token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - // create topic with multilayer fee - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) - .payingWith(SUBMITTER), - // submit message - submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), - // assert topic fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), - // assert token fee collector balance - getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); - } - - - @HapiTest - @DisplayName("Treasury second layer submit to a public topic with 3 layer fees") - // TOPIC_FEE_110 - final Stream treasuryOfSecondLayerSubmitToPublic() { - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var topicFeeCollector = "topicFeeCollector"; - - return hapiTest(flattened( - // create token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - - // give one token to denomToken treasury to be able to pay the fee - tokenAssociate(DENOM_TREASURY, token), - cryptoTransfer(moving(1, token).between(SUBMITTER, DENOM_TREASURY)), - - // create topic - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(DENOM_TREASURY, token, TOPIC, 100, 1) - .payingWith(DENOM_TREASURY), - - // submit - submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), - - // assert topic fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), - // assert token fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); - } - - - @HapiTest - @DisplayName("Collector submit to a public topic with 3 layer fees") - // TOPIC_FEE_111 - final Stream collectorSubmitToPublicTopicWith3layerFees() { - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var topicFeeCollector = "topicFeeCollector"; - - return hapiTest(flattened( - // create token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - - // transfer one token to the collector, to be able to pay the fee - cryptoCreate(topicFeeCollector).balance(ONE_HBAR), - tokenAssociate(topicFeeCollector, token), - - // create topic - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(topicFeeCollector, token, TOPIC, 100, 1) - .payingWith(topicFeeCollector), - - // submit - submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), - - // assert balances - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), - getAccountBalance(COLLECTOR_PREFIX+token).hasTokenBalance(denomToken, 0))); - } - - - @HapiTest - @DisplayName("Collector of second layer submit to a public topic with 3 layer fees") - // TOPIC_FEE_112 - final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFees() { - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var secondLayerFeeCollector = COLLECTOR_PREFIX + token; - final var topicFeeCollector = "topicFeeCollector"; - - return hapiTest(flattened( - // create token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - - // give one token to denomToken treasury to be able to pay the fee - tokenAssociate(secondLayerFeeCollector, token), - cryptoTransfer(moving(1, token).between(SUBMITTER, secondLayerFeeCollector)), - - // create topic - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(secondLayerFeeCollector, token, TOPIC, 100, 1) - .payingWith(secondLayerFeeCollector), - - // submit - submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), - - // assert topic fee collector balance - only first layer fee should be paid - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), - // token fee collector should have 1 token from the first transfer and 0 from msg submit - getAccountBalance(secondLayerFeeCollector).hasTokenBalance(denomToken, 1))); - } - - @HapiTest - @DisplayName("Another collector submit message to a topic with a fee") - // TOPIC_FEE_113 - final Stream anotherCollectorSubmitMessageToATopicWithAFee() { - final var collector = "collector"; - final var anotherToken = "anotherToken"; - final var anotherCollector = COLLECTOR_PREFIX + anotherToken; - return hapiTest(flattened( - // create another token with fixed fee - createTokenWith2LayerFee(SUBMITTER, anotherToken, true), - tokenAssociate(anotherCollector, BASE_TOKEN), - cryptoTransfer( - moving(100, BASE_TOKEN).between(SUBMITTER, anotherCollector), - TokenMovement.movingHbar(ONE_HBAR).between(SUBMITTER, anotherCollector) - ), - // create topic - cryptoCreate(collector), - tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - // add allowance and submit with another collector - approveTopicAllowance() - .addTokenAllowance(anotherCollector, BASE_TOKEN, TOPIC, 100, 1) - .payingWith(anotherCollector), - submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), - // the fee was paid - getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1) - )); - } - - - } - } - @Nested @DisplayName("Topic approve allowance") class TopicApproveAllowance { From 78c8748c27495a6ba9d55b313dc306f98b692e38 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 8 Oct 2024 17:17:36 +0300 Subject: [PATCH 41/94] Add hapi tests Signed-off-by: Zhivko Kelchev --- .../ConsensusAllowancesValidator.java | 4 +- .../bdd/suites/hip991/TopicCustomFeeBase.java | 10 +- .../TopicCustomFeeSubmitMessageTest.java | 121 +++++++++++++++++- 3 files changed, 126 insertions(+), 9 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java index 266a208de975..e947173d9456 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java @@ -156,7 +156,7 @@ private static void validateCryptoAllowances(List= 0, NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck(hbarAllowance.amountPerMessage() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck( - hbarAllowance.amount() > hbarAllowance.amountPerMessage(), + hbarAllowance.amount() >= hbarAllowance.amountPerMessage(), ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); // Add the unique (AccountID, TopicID) pair to the map uniqueMap.put(hbarAllowance.owner(), hbarAllowance.topicId()); @@ -192,7 +192,7 @@ private static void validateTokenAllowances(List= 0, NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); validateTruePreCheck( - tokenAllowance.amount() > tokenAllowance.amountPerMessage(), + tokenAllowance.amount() >= tokenAllowance.amountPerMessage(), ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 74f25d0b051b..362bd33fa244 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -47,6 +47,7 @@ public class TopicCustomFeeBase { protected static final String TOKEN_TREASURY = "tokenTreasury"; protected static final String DENOM_TREASURY = "denomTreasury"; protected static final String BASE_TOKEN = "baseToken"; + protected static final String SECOND_TOKEN = "secondToken"; /* tokens with multilayer fees */ protected static final String TOKEN_PREFIX = "token_"; @@ -69,8 +70,13 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { .tokenType(TokenType.FUNGIBLE_COMMON) .treasury(TOKEN_TREASURY) .initialSupply(500L), - tokenAssociate(SUBMITTER, BASE_TOKEN), - cryptoTransfer(moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER)) + tokenCreate(SECOND_TOKEN) + .treasury(TOKEN_TREASURY) + .initialSupply(500L), + tokenAssociate(SUBMITTER, BASE_TOKEN, SECOND_TOKEN), + cryptoTransfer( + moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER), + moving(500L, SECOND_TOKEN).between(TOKEN_TREASURY, SUBMITTER)), }; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index ad4193f30a9a..c483e2c20916 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -18,6 +18,8 @@ import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; @@ -27,6 +29,10 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHollow; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.flattened; @@ -146,15 +152,13 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { // TOPIC_FEE_104 final Stream messageSubmitToPublicTopicWithFee1Hbar() { final var collector = "collector"; - final var submitter = "submitter"; return hapiTest( cryptoCreate(collector).balance(0L), - cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), approveTopicAllowance() - .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(submitter), - submitMessageTo(TOPIC).message("TEST").payingWith(submitter), + .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTinyBars(ONE_HBAR)); } @@ -425,5 +429,112 @@ final Stream anotherCollectorSubmitMessageToATopicWithAFee() { // the fee was paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1))); } + + @HapiTest + @DisplayName("MessageSubmit to a topic with hollow account as fee collector") + // TOPIC_FEE_116 + final Stream messageTopicSubmitToHollowAccountAsFeeCollector() { + final var collector = "collector"; + return hapiTest( + // create hollow account with ONE_HUNDRED_HBARS + createHollow(1, i -> collector), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + + // collector should be still a hollow account + // and should have the initial balance + ONE_HBAR fee + getAccountInfo(collector).isHollow(), + getAccountBalance(collector).hasTinyBars(ONE_HUNDRED_HBARS + ONE_HBAR)); + } + + @HapiTest + @DisplayName("MessageSubmit and signs with the topic’s feeScheduleKey which is listed in the FEKL list") + // TOPIC_FEE_124 + final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { + final var collector = "collector"; + final var feeScheduleKey = "feeScheduleKey"; + return hapiTest( + newKeyNamed(feeScheduleKey), + cryptoCreate(collector).balance(0L), + createTopic(TOPIC) + .feeScheduleKeyName(feeScheduleKey) + .feeExemptKeys(feeScheduleKey) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").signedByPayerAnd(feeScheduleKey), + getAccountBalance(collector).hasTinyBars(0L)); + + } + + @HapiTest + @DisplayName("Collector submits a message to a topic with fee of 1 FT.") + final Stream collectorSubmitMessageToTopicWithFTFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + approveTopicAllowance() + .addTokenAllowance(collector, BASE_TOKEN,TOPIC, 1, 1) + .payingWith(collector), + submitMessageTo(TOPIC).message("TEST").payingWith(collector), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); + } + + @HapiTest + @DisplayName("Collector submits a message to a topic with fee of 1 HBAR.") + final Stream collectorSubmitMessageToTopicWithHbarFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector).balance(ONE_HBAR), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(collector, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(collector), + submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit"), + // assert collector's tinyBars balance + withOpContext((spec, log) -> { + final var transactionRecord = getTxnRecord("submit"); + allRunFor(spec, transactionRecord); + final var transactionFee = transactionRecord.getResponseRecord().getTransactionFee(); + // todo When we add fee for approveAllowance transaction we must take it into account here!! + getAccountBalance(collector).hasTinyBars(ONE_HBAR - transactionFee); + })); + } + + @HapiTest + @DisplayName("Collector submits a message to a topic with 2 different FT fees.") + final Stream collectorSubmitMessageToTopicWith2differentFees() { + final var collector = "collector"; + final var secondCollector = "secondCollector"; + return hapiTest( + // todo create and associate collector in beforeAll() + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN, SECOND_TOKEN), + // create second collector and send second token + cryptoCreate(secondCollector).balance(ONE_HBAR), + tokenAssociate(secondCollector, SECOND_TOKEN), + cryptoTransfer(moving(1, SECOND_TOKEN).between(SUBMITTER, collector)), + // create topic with two fees + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)) + .withConsensusCustomFee(fixedConsensusHtsFee(1, SECOND_TOKEN, secondCollector)), + approveTopicAllowance() + .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) + .addTokenAllowance(collector, SECOND_TOKEN, TOPIC, 1, 1) + .payingWith(collector), + submitMessageTo(TOPIC).message("TEST").payingWith(collector), + // only second fee should be paid + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), + getAccountBalance(secondCollector).hasTokenBalance(SECOND_TOKEN, 1L)); + } + } } From dea8af1f63943ae51084100327a922330f5aedc4 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 10 Oct 2024 09:41:03 +0300 Subject: [PATCH 42/94] Fix tests Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 4 +-- .../TopicCustomFeeSubmitMessageTest.java | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 362bd33fa244..fefed6728a62 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -70,9 +70,7 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { .tokenType(TokenType.FUNGIBLE_COMMON) .treasury(TOKEN_TREASURY) .initialSupply(500L), - tokenCreate(SECOND_TOKEN) - .treasury(TOKEN_TREASURY) - .initialSupply(500L), + tokenCreate(SECOND_TOKEN).treasury(TOKEN_TREASURY).initialSupply(500L), tokenAssociate(SUBMITTER, BASE_TOKEN, SECOND_TOKEN), cryptoTransfer( moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index c483e2c20916..ed0e095076c1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -468,20 +468,19 @@ final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { .payingWith(SUBMITTER), submitMessageTo(TOPIC).message("TEST").signedByPayerAnd(feeScheduleKey), getAccountBalance(collector).hasTinyBars(0L)); - } @HapiTest @DisplayName("Collector submits a message to a topic with fee of 1 FT.") + // TOPIC_FEE_125 final Stream collectorSubmitMessageToTopicWithFTFee() { final var collector = "collector"; return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), approveTopicAllowance() - .addTokenAllowance(collector, BASE_TOKEN,TOPIC, 1, 1) + .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) .payingWith(collector), submitMessageTo(TOPIC).message("TEST").payingWith(collector), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); @@ -489,23 +488,28 @@ final Stream collectorSubmitMessageToTopicWithFTFee() { @HapiTest @DisplayName("Collector submits a message to a topic with fee of 1 HBAR.") + // TOPIC_FEE_126 final Stream collectorSubmitMessageToTopicWithHbarFee() { final var collector = "collector"; return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), approveTopicAllowance() .addCryptoAllowance(collector, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(collector), + .payingWith(collector) + .via("approveAllowance"), submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit"), // assert collector's tinyBars balance withOpContext((spec, log) -> { - final var transactionRecord = getTxnRecord("submit"); - allRunFor(spec, transactionRecord); - final var transactionFee = transactionRecord.getResponseRecord().getTransactionFee(); - // todo When we add fee for approveAllowance transaction we must take it into account here!! - getAccountBalance(collector).hasTinyBars(ONE_HBAR - transactionFee); + final var submitTxnRecord = getTxnRecord("submit"); + final var allowanceTxnRecord = getTxnRecord("approveAllowance"); + allRunFor(spec, submitTxnRecord, allowanceTxnRecord); + final var transactionTxnFee = + submitTxnRecord.getResponseRecord().getTransactionFee(); + final var allowanceTxnFee = + allowanceTxnRecord.getResponseRecord().getTransactionFee(); + getAccountBalance(collector) + .hasTinyBars(ONE_HUNDRED_HBARS - transactionTxnFee - allowanceTxnFee); })); } @@ -535,6 +539,5 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), getAccountBalance(secondCollector).hasTokenBalance(SECOND_TOKEN, 1L)); } - } } From ab6ce397bed34377cf8f4b3857450124e9f05341 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 27 Sep 2024 17:06:41 +0300 Subject: [PATCH 43/94] submit msg Signed-off-by: Zhivko Kelchev --- .../node/app/services/ServiceScopeLookup.java | 1 + .../ConsensusSubmitMessageHandler.java | 37 ++ .../util/ConsensusApproveAllowanceHelper.java | 258 ++++++++++++ .../impl/util/ConsensusCustomFeeHelper.java | 195 +++++++++ .../services/bdd/spec/keys/KeyFactory.java | 30 +- .../bdd/suites/hip991/TopicCustomFeeTest.java | 389 ++++++++++++++++++ 6 files changed, 905 insertions(+), 5 deletions(-) create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java create mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java index 46b9ae01faaf..8ce11d31ea91 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java @@ -60,6 +60,7 @@ public String getServiceName(@NonNull final TransactionBody txBody) { case CONSENSUS_CREATE_TOPIC, CONSENSUS_UPDATE_TOPIC, CONSENSUS_DELETE_TOPIC, + CONSENSUS_APPROVE_ALLOWANCE, CONSENSUS_SUBMIT_MESSAGE -> ConsensusService.NAME; case CONTRACT_CREATE_INSTANCE, diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 5e2cc16d4324..8c306907b265 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -23,11 +23,13 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_MESSAGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION; import static com.hedera.hapi.node.base.ResponseCodeEnum.MESSAGE_SIZE_TOO_LARGE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.BASIC_ENTITY_ID_SIZE; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.LONG_SIZE; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.RECEIPT_STORAGE_TIME_SEC; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.TX_HASH_SIZE; import static com.hedera.node.app.spi.validation.Validations.mustExist; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; @@ -44,6 +46,7 @@ import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; +import com.hedera.node.app.service.consensus.impl.util.ConsensusCustomFeeHelper; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; @@ -61,6 +64,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.util.HashSet; import javax.inject.Inject; import javax.inject.Singleton; @@ -99,6 +103,12 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx if (topic.hasSubmitKey()) { context.requireKeyOrThrow(topic.submitKeyOrThrow(), INVALID_SUBMIT_KEY); } + // add optional fee exempt keys in to the key verifieer + // later it will be used to validate if transaction was signed by + // any of these keys and based on that, custom fees will be charged or not + if (!topic.feeExemptKeyList().isEmpty()) { + context.optionalKeys(new HashSet<>(topic.feeExemptKeyList())); + } } /** @@ -122,6 +132,33 @@ public void handle(@NonNull final HandleContext handleContext) { final var config = handleContext.configuration().getConfigData(ConsensusConfig.class); validateTransaction(txn, config, topic); + /* handle custom fees */ + // check if payer is fee exempt + var payerIsFeeExempted = false; + if (!topic.feeExemptKeyList().isEmpty()) { + for (final var key : topic.feeExemptKeyList()) { + final var keyVerificationResult = handleContext.keyVerifier().verificationFor(key); + if (keyVerificationResult.passed()) { + payerIsFeeExempted = true; + } + } + } + if (!topic.customFees().isEmpty() && !payerIsFeeExempted) { + // validate and create synthetic body + final var syntheticBody = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); + // dispatch crypto transfer + var record = handleContext.dispatchChildTransaction( + TransactionBody.newBuilder().cryptoTransfer(syntheticBody).build(), + ConsensusSubmitMessageStreamBuilder.class, + null, + handleContext.payer(), + HandleContext.TransactionCategory.CHILD, + HandleContext.ConsensusThrottling.OFF); + validateTrue(record.status().equals(SUCCESS), record.status()); + // update total allowances + ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + } + try { final var updatedTopic = updateRunningHashAndSequenceNumber(txn, topic, handleContext.consensusNow()); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java new file mode 100644 index 000000000000..c95a17a78dd3 --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.util; + +import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; +import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TopicID; +import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; +import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.List; + +public class ConsensusApproveAllowanceHelper { + /** + * Applies all changes needed for Crypto allowances from the transaction. + * If the topic already has an allowance, the allowance value will be replaced with values + * from transaction. If the amount specified is 0, the allowance will be removed. + * @param topicCryptoAllowances the list of crypto allowances + * @param topicStore the topic store + */ + public static void applyCryptoAllowances( + @NonNull final List topicCryptoAllowances, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(topicCryptoAllowances); + requireNonNull(topicStore); + + for (final var allowance : topicCryptoAllowances) { + final var ownerId = allowance.owner(); + final var topicId = allowance.topicIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); + + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + + updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); + final var copy = + topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); + + topicStore.put(copy); + } + } + + public static void applyCryptoAllowances( + @NonNull final TopicID topicId, + @NonNull final TopicCryptoAllowance allowance, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(allowance); + requireNonNull(topicStore); + + final var ownerId = allowance.spenderId(); + final var topic = getIfUsable(topicId, topicStore); + final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); + + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + + updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); + final var copy = topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); + + topicStore.put(copy); + } + + /** + * Updates the crypto allowance amount if the allowance exists, otherwise adds a new allowance. + * If the amount is zero removes the allowance if it exists in the list. + * @param mutableAllowances the list of mutable allowances of owner + * @param amount the amount + * @param spenderId the spender id + */ + private static void updateCryptoAllowance( + final List mutableAllowances, + final long amount, + final long amountPerMessage, + final AccountID spenderId) { + final var newAllowanceBuilder = TopicCryptoAllowance.newBuilder().spenderId(spenderId); + // get the index of the allowance with same spender in existing list + final var index = lookupSpender(mutableAllowances, spenderId); + // If given amount is zero, if the element exists remove it, otherwise do nothing + if (amount == 0) { + if (index != -1) { + // If amount is 0, remove the allowance + mutableAllowances.remove(index); + } + return; + } + if (index != -1) { + mutableAllowances.set( + index, + newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } else { + mutableAllowances.add(newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } + } + + /** + * Applies all changes needed for fungible token allowances from the transaction. If the key + * {token, spender} already has an allowance, the allowance value will be replaced with values + * from transaction. + * @param tokenAllowances the list of token allowances + * @param topicStore the topic store + */ + public static void applyFungibleTokenAllowances( + @NonNull final List tokenAllowances, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(tokenAllowances); + requireNonNull(topicStore); + + for (final var allowance : tokenAllowances) { + final var ownerId = allowance.owner(); + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + final var tokenId = allowance.tokenIdOrThrow(); + final var topicId = allowance.topicIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + + final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); + + updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); + final var copy = + topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); + + topicStore.put(copy); + } + } + + /* + * + */ + public static void applyFungibleTokenAllowances( + @NonNull final TopicID topicId, + @NonNull final TopicFungibleTokenAllowance allowance, + @NonNull final WritableTopicStore topicStore) { + requireNonNull(allowance); + requireNonNull(topicStore); + + final var ownerId = allowance.spenderIdOrThrow(); + final var amount = allowance.amount(); + final var amountPerMessage = allowance.amountPerMessage(); + final var tokenId = allowance.tokenIdOrThrow(); + final var topic = getIfUsable(topicId, topicStore); + + final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); + + updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); + final var copy = + topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); + + topicStore.put(copy); + } + + /** + * Updates the token allowance amount if the allowance for given tokenNuma dn spenderNum exists, + * otherwise adds a new allowance. + * If the amount is zero removes the allowance if it exists in the list + * @param mutableAllowances the list of mutable allowances of owner + * @param amount the amount + * @param spenderId the spender number + * @param tokenId the token number + */ + private static void updateTokenAllowance( + final List mutableAllowances, + final long amount, + final long amountPerMessage, + final AccountID spenderId, + final TokenID tokenId) { + final var newAllowanceBuilder = + TopicFungibleTokenAllowance.newBuilder().spenderId(spenderId).tokenId(tokenId); + final var index = lookupSpenderAndToken(mutableAllowances, spenderId, tokenId); + // If given amount is zero, if the element exists remove it + if (amount == 0) { + if (index != -1) { + mutableAllowances.remove(index); + } + return; + } + if (index != -1) { + mutableAllowances.set( + index, + newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } else { + mutableAllowances.add(newAllowanceBuilder + .amount(amount) + .amountPerMessage(amountPerMessage) + .build()); + } + } + + /** + * Returns the index of the allowance with the given spender in the list if it exists, + * otherwise returns -1. + * @param topicCryptoAllowances list of allowances + * @param spenderNum spender account number + * @return index of the allowance if it exists, otherwise -1 + */ + private static int lookupSpender( + final List topicCryptoAllowances, final AccountID spenderNum) { + for (int i = 0; i < topicCryptoAllowances.size(); i++) { + final var allowance = topicCryptoAllowances.get(i); + if (allowance.spenderIdOrThrow().equals(spenderNum)) { + return i; + } + } + return -1; + } + + /** + * Returns the index of the allowance with the given spender and token in the list if it exists, + * otherwise returns -1. + * @param topicTokenAllowances list of allowances + * @param spenderId spender account number + * @param tokenId token number + * @return index of the allowance if it exists, otherwise -1 + */ + private static int lookupSpenderAndToken( + final List topicTokenAllowances, + final AccountID spenderId, + final TokenID tokenId) { + for (int i = 0; i < topicTokenAllowances.size(); i++) { + final var allowance = topicTokenAllowances.get(i); + if (allowance.spenderIdOrThrow().equals(spenderId) + && allowance.tokenIdOrThrow().equals(tokenId)) { + return i; + } + } + return -1; + } +} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java new file mode 100644 index 000000000000..c04ac7e2a98a --- /dev/null +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.service.consensus.impl.util; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyCryptoAllowances; +import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyFungibleTokenAllowances; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.AccountAmount; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenTransferList; +import com.hedera.hapi.node.base.TransferList; +import com.hedera.hapi.node.state.consensus.Topic; +import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; +import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; +import com.hedera.hapi.node.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.ConsensusCustomFee; +import com.hedera.hapi.node.transaction.FixedFee; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.spi.workflows.HandleContext; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class ConsensusCustomFeeHelper { + + public static CryptoTransferTransactionBody assessCustomFee(Topic topic, HandleContext context) { + final var payer = context.payer(); + final var topicStore = context.storeFactory().writableStore(WritableTopicStore.class); + // lookup for hbar allowance + TopicCryptoAllowance hbarAllowance = null; + for (final var allowance : topic.cryptoAllowances()) { + if (payer.equals(allowance.spenderId())) { + hbarAllowance = allowance; + } + } + // lookup for fungible token allowance + Map tokenAllowanceMap = new HashMap<>(); + for (final var allowance : topic.tokenAllowances()) { + if (payer.equals(allowance.spenderId())) { + tokenAllowanceMap.put(allowance.tokenId(), allowance); + } + } + + final var tokenTransfers = new ArrayList(); + List hbarTransfers = new ArrayList<>(); + for (ConsensusCustomFee fee : topic.customFees()) { + final var fixedFee = fee.fixedFeeOrThrow(); + // build crypto transfer body for the first layer of custom fees, + // if there is a second layer it will be assessed in crypto transfer handler + if (fixedFee.hasDenominatingTokenId()) { + final var tokenId = fixedFee.denominatingTokenId(); + validateTokenAllowance(tokenAllowanceMap, fixedFee); + tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); + } else { + validateHbarAllowance(hbarAllowance, fixedFee); + hbarTransfers = mergeTransfers( + hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); + } + } + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); + + return syntheticBodyBuilder + .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) + .build(); + } + + private static void validateTokenAllowance( + Map tokenAllowanceMap, FixedFee fixedFee) { + final var allowance = tokenAllowanceMap.get(fixedFee.denominatingTokenId()); + validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); + validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + } + + private static void validateHbarAllowance(TopicCryptoAllowance allowance, FixedFee fixedFee) { + validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); + validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); + } + + public static void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { + // todo adjust allowance + // extract the code for updating the allowance amounts from ConsensusApproveAllowanceHandler and reuse it here + } + + private static List buildCustomFeeHbarTransferList( + AccountID payer, AccountID collector, FixedFee fee) { + return List.of( + AccountAmount.newBuilder() + .accountID(payer) + .amount(-fee.amount()) + .build(), + AccountAmount.newBuilder() + .accountID(collector) + .amount(fee.amount()) + .build()); + } + + private static TokenTransferList buildCustomFeeTokenTransferList( + AccountID payer, AccountID collector, FixedFee fee) { + return TokenTransferList.newBuilder() + .token(fee.denominatingTokenId()) + .transfers( + AccountAmount.newBuilder() + .accountID(payer) + .amount(-fee.amount()) + .build(), + AccountAmount.newBuilder() + .accountID(collector) + .amount(fee.amount()) + .build()) + .build(); + } + + private static CryptoTransferTransactionBody.Builder tokenTransfers( + @NonNull TokenTransferList... tokenTransferLists) { + if (repeatsTokenId(tokenTransferLists)) { + final Map consolidatedTokenTransfers = new LinkedHashMap<>(); + for (final var tokenTransferList : tokenTransferLists) { + consolidatedTokenTransfers.merge( + tokenTransferList.tokenOrThrow(), + tokenTransferList, + ConsensusCustomFeeHelper::mergeTokenTransferLists); + } + tokenTransferLists = consolidatedTokenTransfers.values().toArray(TokenTransferList[]::new); + } + return CryptoTransferTransactionBody.newBuilder().tokenTransfers(tokenTransferLists); + } + + private static TokenTransferList mergeTokenTransferLists( + @NonNull final TokenTransferList from, @NonNull final TokenTransferList to) { + return from.copyBuilder() + .transfers(mergeTransfers(from.transfers(), to.transfers())) + .build(); + } + + private static List mergeTransfers( + @NonNull final List from, @NonNull final List to) { + requireNonNull(from); + requireNonNull(to); + final Map consolidated = new LinkedHashMap<>(); + consolidateInto(consolidated, from); + consolidateInto(consolidated, to); + return consolidated.values().stream().toList(); + } + + private static void consolidateInto( + @NonNull final Map consolidated, @NonNull final List transfers) { + for (final var transfer : transfers) { + consolidated.merge(transfer.accountID(), transfer, ConsensusCustomFeeHelper::mergeAdjusts); + } + } + + private static AccountAmount mergeAdjusts(@NonNull final AccountAmount from, @NonNull final AccountAmount to) { + return from.copyBuilder() + .amount(from.amount() + to.amount()) + .isApproval(from.isApproval() || to.isApproval()) + .build(); + } + + private static boolean repeatsTokenId(@NonNull final TokenTransferList[] tokenTransferList) { + return tokenTransferList.length > 1 + && Arrays.stream(tokenTransferList) + .map(TokenTransferList::token) + .collect(Collectors.toSet()) + .size() + < tokenTransferList.length; + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java index b841e7fe0149..32fb0849ca67 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java @@ -665,12 +665,32 @@ private void signRecursively(final Key key, final SigControl controller) throws case SIG_ON: signIfNecessary(key); break; + case PREDEFINED: + signPredefinedNatureKey(key, controller); + break; default: - final KeyList composite = TxnUtils.getCompositeList(key); - final SigControl[] childControls = controller.getChildControls(); - for (int i = 0; i < childControls.length; i++) { - signRecursively(composite.getKeys(i), childControls[i]); - } + signCompositeKeys(key, controller); + } + } + + private void signPredefinedNatureKey(Key key, SigControl control) throws GeneralSecurityException { + // if key is composite sign recursively + if (key.hasKeyList() || key.hasThresholdKey()) { + signCompositeKeys(key, control); + return; + } + + // skip contract id keys + if (!(key.hasContractID() || key.hasDelegatableContractId())) { + signIfNecessary(key); + } + } + + private void signCompositeKeys(Key key, SigControl control) throws GeneralSecurityException { + final KeyList composite = TxnUtils.getCompositeList(key); + final SigControl[] childControls = control.getChildControls(); + for (int i = 0; i < childControls.length; i++) { + signRecursively(composite.getKeys(i), childControls[i]); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java new file mode 100644 index 000000000000..4a3d24cfa979 --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.suites.hip991; + +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.keys.KeyShape.PREDEFINED_SHAPE; +import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; +import static com.hedera.services.bdd.spec.keys.KeyShape.threshOf; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; +import static com.hedera.services.bdd.suites.HapiSuite.flattened; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; + +import com.hedera.services.bdd.junit.HapiTest; +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hederahashgraph.api.proto.java.TokenType; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; + +@HapiTestLifecycle +@DisplayName("Topic custom fees") +public class TopicCustomFeeTest extends TopicCustomFeeBase { + + @Nested + @DisplayName("Topic create") + class TopicCreate { + + @Nested + @DisplayName("Positive scenarios") + class TopicCreatePositiveScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } + + @HapiTest + @DisplayName("Create topic with all keys") + // TOPIC_FEE_001 + final Stream createTopicWithAllKeys() { + return hapiTest(flattened( + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY), + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY))); + } + + @HapiTest + @DisplayName("Create topic with submitKey and feeScheduleKey") + // TOPIC_FEE_002 + final Stream createTopicWithSubmitKeyAndFeeScheduleKey() { + return hapiTest( + createTopic(TOPIC).submitKeyName(SUBMIT_KEY).feeScheduleKeyName(FEE_SCHEDULE_KEY), + getTopicInfo(TOPIC).hasSubmitKey(SUBMIT_KEY).hasFeeScheduleKey(FEE_SCHEDULE_KEY)); + } + + @HapiTest + @DisplayName("Create topic with only feeScheduleKey") + // TOPIC_FEE_003 + final Stream createTopicWithOnlyFeeScheduleKey() { + return hapiTest( + createTopic(TOPIC).feeScheduleKeyName(FEE_SCHEDULE_KEY), + getTopicInfo(TOPIC).hasFeeScheduleKey(FEE_SCHEDULE_KEY)); + } + + @HapiTest + @DisplayName("Create topic with 1 Hbar fixed fee") + // TOPIC_FEE_004 + final Stream createTopicWithOneHbarFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY) + .hasCustom(expectedConsensusFixedHbarFee(ONE_HBAR, collector))); + } + + @HapiTest + @DisplayName("Create topic with 1 HTS fixed fee") + // TOPIC_FEE_005 + final Stream createTopicWithOneHTSFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenCreate("testToken") + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, "testToken"), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHtsFee(1, "testToken", collector)), + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY) + .hasCustom(expectedConsensusFixedHTSFee(1, "testToken", collector))); + } + + @HapiTest + @DisplayName("Create topic with 10 keys in FEKL") + // TOPIC_FEE_020 + final Stream createTopicWithFEKL() { + final var collector = "collector"; + return hapiTest(flattened( + // create 10 keys + newNamedKeysForFEKL(10), + cryptoCreate(collector), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(5, collector)) + // set list of 10 keys + .feeExemptKeys(feeExemptKeyNames(10)), + getTopicInfo(TOPIC) + .hasAdminKey(ADMIN_KEY) + .hasSubmitKey(SUBMIT_KEY) + .hasFeeScheduleKey(FEE_SCHEDULE_KEY) + // assert the list + .hasFeeExemptKeys(List.of(feeExemptKeyNames(10))))); + } + } + + @Nested + @DisplayName("Negative scenarios") + class TopicCreateNegativeScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } + + @HapiTest + @DisplayName("Create topic with duplicated signatures in FEKL") + // TOPIC_FEE_023 + final Stream createTopicWithDuplicateSignatures() { + final var testKey = "testKey"; + return hapiTest(flattened( + newKeyNamed(testKey), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .feeExemptKeys(testKey, testKey) + .hasPrecheck(FEKL_CONTAINS_DUPLICATED_KEYS))); + } + + @HapiTest + @DisplayName("Create topic with 0 Hbar fixed fee") + // TOPIC_FEE_024 + final Stream createTopicWithZeroHbarFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(0, collector)) + .hasKnownStatus(CUSTOM_FEE_MUST_BE_POSITIVE)); + } + + @HapiTest + @DisplayName("Create topic with 0 HTS fixed fee") + // TOPIC_FEE_025 + final Stream createTopicWithZeroHTSFixedFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenCreate("testToken") + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, "testToken"), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHtsFee(0, "testToken", collector)) + .hasKnownStatus(CUSTOM_FEE_MUST_BE_POSITIVE)); + } + + @HapiTest + @DisplayName("Create topic with invalid fee schedule key") + // TOPIC_FEE_026 + final Stream createTopicWithInvalidFeeScheduleKey() { + final var invalidKey = "invalidKey"; + return hapiTest( + withOpContext((spec, opLog) -> spec.registry().saveKey(invalidKey, STRUCTURALLY_INVALID_KEY)), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(invalidKey) + .hasKnownStatus(INVALID_CUSTOM_FEE_SCHEDULE_KEY)); + } + + @HapiTest + @DisplayName("Create topic with custom fee and deleted collector") + // TOPIC_FEE_028 + final Stream createTopicWithCustomFeeAndDeletedCollector() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + cryptoDelete(collector), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + .hasKnownStatus(ACCOUNT_DELETED)); + } + } + } + + @Nested + @DisplayName("Submit message") + class SubmitMessage { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } + + @HapiTest + @DisplayName("submit") + final Stream submitMessage() { + final var collector = "collector"; + final var payer = "submitter"; + final var treasury = "treasury"; + final var token = "testToken"; + final var secondToken = "secondToken"; + final var denomToken = "denomToken"; + final var simpleKey = "simpleKey"; + final var simpleKey2 = "simpleKey2"; + final var invalidKey = "invalidKey"; + final var threshKey = "threshKey"; + + return hapiTest( + // create keys + newKeyNamed(invalidKey), + newKeyNamed(simpleKey), + newKeyNamed(simpleKey2), + newKeyNamed(threshKey) + .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) + .signedWith(sigs(simpleKey2, simpleKey))), + // create accounts and denomination token + cryptoCreate(collector).balance(0L), + cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), + cryptoCreate(treasury), + tokenCreate(denomToken) + .treasury(treasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, denomToken), + tokenAssociate(payer, denomToken), + tokenCreate(token) + .treasury(treasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .withCustom(fixedHtsFee(1, denomToken, collector)) + .initialSupply(500), + tokenCreate(secondToken) + .treasury(treasury) + .tokenType(TokenType.FUNGIBLE_COMMON) + .initialSupply(500), + tokenAssociate(collector, token, secondToken), + tokenAssociate(payer, token, secondToken), + cryptoTransfer( + moving(2, token).between(treasury, payer), + moving(1, secondToken).between(treasury, payer), + moving(1, denomToken).between(treasury, payer)), + + // create topic with custom fees + createTopic(TOPIC) + // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, + // collector)) + // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, + // collector)) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + .feeExemptKeys(threshKey) + .hasKnownStatus(SUCCESS), + + // add allowance + approveTopicAllowance() + .payingWith(payer) + .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), + + // submit message + submitMessageTo(TOPIC) + .message("TEST") + .signedBy(invalidKey, payer) + .payingWith(payer) + .via("submit"), + + // check records + getTxnRecord("submit").andAllChildRecords().logged(), + + // assert balances + getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + // .hasTokenBalance(token, 2) + // .hasTokenBalance(denomToken,1) + // .hasTokenBalance(secondToken, 1), + // getAccountBalance(payer) + // .hasTokenBalance(token, 0) + // .hasTokenBalance(secondToken, 0)); + } + } + + @Nested + @DisplayName("Topic approve allowance") + class TopicApproveAllowance { + + @Nested + @DisplayName("Positive scenarios") + class ApproveAllowancePositiveScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(setupBaseKeys()); + } + + @HapiTest + @DisplayName("Approve crypto allowance for topic") + final Stream createTopicWithAllKeys() { + return hapiTest( + cryptoCreate(OWNER), + createTopic(TOPIC) + .adminKeyName(ADMIN_KEY) + .submitKeyName(SUBMIT_KEY) + .feeScheduleKeyName(FEE_SCHEDULE_KEY), + approveTopicAllowance().payingWith(OWNER).addCryptoAllowance(OWNER, TOPIC, 100, 10)); + } + } + } +} From b1f07bc0815f75694fec7f29a84b0cbb1c5188e1 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 30 Sep 2024 17:16:22 +0300 Subject: [PATCH 44/94] Add hapi tests Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 50 +++ .../bdd/suites/hip991/TopicCustomFeeTest.java | 304 +++++++++++++----- 2 files changed, 269 insertions(+), 85 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index b10f7dc49c91..f37d5d70838f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -17,9 +17,14 @@ package com.hedera.services.bdd.suites.hip991; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_MILLION_HBARS; import com.google.protobuf.ByteString; import com.hedera.services.bdd.spec.SpecOperation; @@ -30,6 +35,8 @@ public class TopicCustomFeeBase { protected static final String TOPIC = "topic"; + protected static final String OWNER = "owner"; + protected static final String FUNGIBLE_TOKEN = "fungibleToken"; protected static final String ADMIN_KEY = "adminKey"; protected static final String SUBMIT_KEY = "submitKey"; protected static final String FEE_SCHEDULE_KEY = "feeScheduleKey"; @@ -39,6 +46,11 @@ public class TopicCustomFeeBase { protected static final String TOKEN = "TOKEN"; protected static final String COLLECTOR = "COLLECTOR"; + /* Submit message entities */ + protected static final String SUBMITTER = "submitter"; + protected static final String TOKEN_TREASURY = "tokenTreasury"; + protected static final String BASE_TOKEN = "baseToken"; + protected static final String MULTI_LAYER_FEE_PREFIX = "multiLayerFeePrefix_"; // This key is truly invalid, as all Ed25519 public keys must be 32 bytes long protected static final Key STRUCTURALLY_INVALID_KEY = Key.newBuilder().setEd25519(ByteString.fromHex("ff")).build(); @@ -64,6 +76,44 @@ protected static SpecOperation[] setupBaseForUpdate() { }; } + protected static SpecOperation[] associateFeeTokensAndSubmitter() { + return new SpecOperation[] { + cryptoCreate(SUBMITTER).balance(ONE_MILLION_HBARS), + cryptoCreate(TOKEN_TREASURY), + tokenCreate(BASE_TOKEN) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .initialSupply(500L), + tokenAssociate(SUBMITTER, BASE_TOKEN), + cryptoTransfer(moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER)) + }; + } + + /** + * Create and transfer multiple tokens with fixed hbar custom fee to account. + * @param account account to transfer tokens + * @param numberOfTokens the count of tokens to be transferred + * @return array of spec operations + */ + protected SpecOperation[] transferMultiLayerFeeTokensTo(String account, int numberOfTokens) { + final var treasury = MULTI_LAYER_FEE_PREFIX + TOKEN_TREASURY; + final var list = new ArrayList(); + list.add(cryptoCreate(treasury)); + for (int i = 0; i < numberOfTokens; i++) { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_" + i; + final var collectorName = MULTI_LAYER_FEE_PREFIX + "collector_" + i; + list.add(cryptoCreate(collectorName).balance(0L)); + list.add(tokenCreate(tokenName) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(treasury) + .initialSupply(500L) + .withCustom(fixedHbarFee(ONE_HBAR, collectorName))); + list.add(tokenAssociate(account, tokenName)); + list.add(cryptoTransfer(moving(500L, tokenName).between(treasury, account))); + } + return list.toArray(new SpecOperation[0]); + } + protected static SpecOperation[] newNamedKeysForFEKL(int count) { final var list = new ArrayList(); for (int i = 0; i < count; i++) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 4a3d24cfa979..1cb6aee5bcf9 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -47,12 +47,15 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.SpecOperation; import com.hederahashgraph.api.proto.java.TokenType; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; @@ -271,92 +274,223 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { @DisplayName("Submit message") class SubmitMessage { - @BeforeAll - static void beforeAll(@NonNull final TestLifecycle lifecycle) { - lifecycle.doAdhoc(setupBaseKeys()); - } +// @HapiTest +// @DisplayName("submit") +// final Stream submitMessage() { +// final var collector = "collector"; +// final var payer = "submitter"; +// final var treasury = "treasury"; +// final var token = "testToken"; +// final var secondToken = "secondToken"; +// final var denomToken = "denomToken"; +// final var simpleKey = "simpleKey"; +// final var simpleKey2 = "simpleKey2"; +// final var invalidKey = "invalidKey"; +// final var threshKey = "threshKey"; +// +// return hapiTest( +// // create keys +// newKeyNamed(invalidKey), +// newKeyNamed(simpleKey), +// newKeyNamed(simpleKey2), +// newKeyNamed(threshKey) +// .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) +// .signedWith(sigs(simpleKey2, simpleKey))), +// // create accounts and denomination token +// cryptoCreate(collector).balance(0L), +// cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), +// cryptoCreate(treasury), +// tokenCreate(denomToken) +// .treasury(treasury) +// .tokenType(TokenType.FUNGIBLE_COMMON) +// .initialSupply(500), +// tokenAssociate(collector, denomToken), +// tokenAssociate(payer, denomToken), +// tokenCreate(token) +// .treasury(treasury) +// .tokenType(TokenType.FUNGIBLE_COMMON) +// .withCustom(fixedHtsFee(1, denomToken, collector)) +// .initialSupply(500), +// tokenCreate(secondToken) +// .treasury(treasury) +// .tokenType(TokenType.FUNGIBLE_COMMON) +// .initialSupply(500), +// tokenAssociate(collector, token, secondToken), +// tokenAssociate(payer, token, secondToken), +// cryptoTransfer( +// moving(2, token).between(treasury, payer), +// moving(1, secondToken).between(treasury, payer), +// moving(1, denomToken).between(treasury, payer)), +// +// // create topic with custom fees +// createTopic(TOPIC) +// // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, +// // collector)) +// // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, +// // collector)) +// .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) +// .feeExemptKeys(threshKey) +// .hasKnownStatus(SUCCESS), +// +// // add allowance +// approveTopicAllowance() +// .payingWith(payer) +// .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), +// +// // submit message +// submitMessageTo(TOPIC) +// .message("TEST") +// .signedBy(invalidKey, payer) +// .payingWith(payer) +// .via("submit"), +// +// // check records +// getTxnRecord("submit").andAllChildRecords().logged(), +// +// // assert balances +// getAccountBalance(collector).hasTinyBars(ONE_HBAR)); +// // .hasTokenBalance(token, 2) +// // .hasTokenBalance(denomToken,1) +// // .hasTokenBalance(secondToken, 1), +// // getAccountBalance(payer) +// // .hasTokenBalance(token, 0) +// // .hasTokenBalance(secondToken, 0)); +// } + + @Nested + @DisplayName("Positive scenarios") + class SubmitMessagesPositiveScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(associateFeeTokensAndSubmitter()); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") + final Stream messageSubmitToPublicTopicWithFee1Hbar() { + final var collector = "collector"; + final var submitter = "submitter"; + return hapiTest( + cryptoCreate(collector).balance(0L), + cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(submitter), + submitMessageTo(TOPIC).message("TEST").payingWith(submitter), + getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") + final Stream messageSubmitToPublicTopicWithFee1token() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 2 layer fee") + final Stream messageSubmitToPublicTopicWith2layerFee() { + final var topicFeeCollector = "collector"; + final var token = MULTI_LAYER_FEE_PREFIX + "token_0"; + final var tokenFeeCollector = MULTI_LAYER_FEE_PREFIX + "collector_0"; + return hapiTest(flattened( + cryptoCreate(topicFeeCollector).balance(0L), + // create denomination token and transfer it to the submitter + transferMultiLayerFeeTokensTo(SUBMITTER, 1), + tokenAssociate(topicFeeCollector, token), + // create topic with multilayer fee + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector).hasTinyBars(ONE_HBAR))); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 10 different 2 layer fees") + final Stream messageSubmitToPublicTopicWith10different2layerFees() { + return hapiTest(flattened( + // create 9 denomination tokens and transfer them to the submitter + transferMultiLayerFeeTokensTo(SUBMITTER, 9), + // create 9 collectors and associate them with tokens + associateAllTokensToCollectors(), + // create topic with 10 multilayer fees - 9 HTS + 1 HBAR + createTopicWith10Different2layerFees(), + approveTopicAllowanceForAllFees(), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER) + // todo for now custom fee will fail, because of limitation in cryptoTransfer + .hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED))); + // assert topic fee collector balance +// assertAllCollectorsBalances())); + } + + + + + - @HapiTest - @DisplayName("submit") - final Stream submitMessage() { - final var collector = "collector"; - final var payer = "submitter"; - final var treasury = "treasury"; - final var token = "testToken"; - final var secondToken = "secondToken"; - final var denomToken = "denomToken"; - final var simpleKey = "simpleKey"; - final var simpleKey2 = "simpleKey2"; - final var invalidKey = "invalidKey"; - final var threshKey = "threshKey"; - - return hapiTest( - // create keys - newKeyNamed(invalidKey), - newKeyNamed(simpleKey), - newKeyNamed(simpleKey2), - newKeyNamed(threshKey) - .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) - .signedWith(sigs(simpleKey2, simpleKey))), - // create accounts and denomination token - cryptoCreate(collector).balance(0L), - cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), - cryptoCreate(treasury), - tokenCreate(denomToken) - .treasury(treasury) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(500), - tokenAssociate(collector, denomToken), - tokenAssociate(payer, denomToken), - tokenCreate(token) - .treasury(treasury) - .tokenType(TokenType.FUNGIBLE_COMMON) - .withCustom(fixedHtsFee(1, denomToken, collector)) - .initialSupply(500), - tokenCreate(secondToken) - .treasury(treasury) - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(500), - tokenAssociate(collector, token, secondToken), - tokenAssociate(payer, token, secondToken), - cryptoTransfer( - moving(2, token).between(treasury, payer), - moving(1, secondToken).between(treasury, payer), - moving(1, denomToken).between(treasury, payer)), - - // create topic with custom fees - createTopic(TOPIC) - // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, - // collector)) - // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, - // collector)) - .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) - .feeExemptKeys(threshKey) - .hasKnownStatus(SUCCESS), - - // add allowance - approveTopicAllowance() - .payingWith(payer) - .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), - - // submit message - submitMessageTo(TOPIC) - .message("TEST") - .signedBy(invalidKey, payer) - .payingWith(payer) - .via("submit"), - - // check records - getTxnRecord("submit").andAllChildRecords().logged(), - - // assert balances - getAccountBalance(collector).hasTinyBars(ONE_HBAR)); - // .hasTokenBalance(token, 2) - // .hasTokenBalance(denomToken,1) - // .hasTokenBalance(secondToken, 1), - // getAccountBalance(payer) - // .hasTokenBalance(token, 0) - // .hasTokenBalance(secondToken, 0)); + private SpecOperation[] associateAllTokensToCollectors() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var collectorName = "collector_"; + final var associateTokensToCollectors = new ArrayList(); + for (int i = 0; i < 9; i++) { + associateTokensToCollectors.add( + cryptoCreate(collectorName + i).balance(0L)); + associateTokensToCollectors.add(tokenAssociate(collectorName + i, tokenName + i)); + } + return associateTokensToCollectors.toArray(SpecOperation[]::new); + } + + private SpecOperation createTopicWith10Different2layerFees() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var collectorName = "collector_"; + final var topicCreateOp = createTopic(TOPIC); + for (int i = 0; i < 9; i++) { + topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, tokenName + i, collectorName + i)); + } + // add one hbar custom fee + topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); + return topicCreateOp; + } + + private SpecOperation approveTopicAllowanceForAllFees() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); + + for (int i = 0; i < 9; i++) { + approveAllowance.addTokenAllowance(SUBMITTER, tokenName + i, TOPIC, 100, 1); + } + approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); + return approveAllowance; + } + + private SpecOperation[] assertAllCollectorsBalances() { + final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; + final var collectorName = "collector_"; + + final var assertBalances = new ArrayList(); + + for (int i = 0; i < 9; i++) { + assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(tokenName + i, 1)); + } + // add assert for hbar fee + assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); + return assertBalances.toArray(SpecOperation[]::new); + } } } From 83f37e2017bc5177de1e7588ec0a8341fe80a216 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 1 Oct 2024 17:48:48 +0300 Subject: [PATCH 45/94] Split crypto dispatches if we have to many transfers Signed-off-by: Zhivko Kelchev --- .../ConsensusSubmitMessageHandler.java | 28 +-- .../impl/util/ConsensusCustomFeeHelper.java | 46 ++++- .../bdd/suites/hip991/TopicCustomFeeTest.java | 186 ++++++++---------- 3 files changed, 141 insertions(+), 119 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 8c306907b265..c5e3be9d7aca 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -145,18 +145,22 @@ public void handle(@NonNull final HandleContext handleContext) { } if (!topic.customFees().isEmpty() && !payerIsFeeExempted) { // validate and create synthetic body - final var syntheticBody = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); - // dispatch crypto transfer - var record = handleContext.dispatchChildTransaction( - TransactionBody.newBuilder().cryptoTransfer(syntheticBody).build(), - ConsensusSubmitMessageStreamBuilder.class, - null, - handleContext.payer(), - HandleContext.TransactionCategory.CHILD, - HandleContext.ConsensusThrottling.OFF); - validateTrue(record.status().equals(SUCCESS), record.status()); - // update total allowances - ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + final var syntheticBodies = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); + for (final var syntheticBody : syntheticBodies) { + // dispatch crypto transfer + var record = handleContext.dispatchChildTransaction( + TransactionBody.newBuilder() + .cryptoTransfer(syntheticBody) + .build(), + ConsensusSubmitMessageStreamBuilder.class, + null, + handleContext.payer(), + HandleContext.TransactionCategory.CHILD, + HandleContext.ConsensusThrottling.OFF); + validateTrue(record.status().equals(SUCCESS), record.status()); + // update total allowances + ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + } } try { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java index c04ac7e2a98a..acab8571799e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java @@ -36,6 +36,7 @@ import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.Arrays; @@ -47,9 +48,15 @@ public class ConsensusCustomFeeHelper { - public static CryptoTransferTransactionBody assessCustomFee(Topic topic, HandleContext context) { + public static List assessCustomFee(Topic topic, HandleContext context) { + final List transactionBodies = new ArrayList<>(); + final var payer = context.payer(); final var topicStore = context.storeFactory().writableStore(WritableTopicStore.class); + final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); + + // todo: allowance validation will be changed, when the storage situation is clear. + // lookup for hbar allowance TopicCryptoAllowance hbarAllowance = null; for (final var allowance : topic.cryptoAllowances()) { @@ -67,27 +74,52 @@ public static CryptoTransferTransactionBody assessCustomFee(Topic topic, HandleC final var tokenTransfers = new ArrayList(); List hbarTransfers = new ArrayList<>(); + // we need to count the number of balance adjustments, + // and if needed to split custom fee transfers in to two separate dispatches + final var maxTransfers = ledgerConfig.transfersMaxLen() / 2; + var transferCounts = 0; + + // build crypto transfer body for the first layer of custom fees, + // if there is a second layer it will be assessed in crypto transfer handler for (ConsensusCustomFee fee : topic.customFees()) { final var fixedFee = fee.fixedFeeOrThrow(); - // build crypto transfer body for the first layer of custom fees, - // if there is a second layer it will be assessed in crypto transfer handler if (fixedFee.hasDenominatingTokenId()) { final var tokenId = fixedFee.denominatingTokenId(); validateTokenAllowance(tokenAllowanceMap, fixedFee); + tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + // update allowance values applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); } else { validateHbarAllowance(hbarAllowance, fixedFee); hbarTransfers = mergeTransfers( hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + // update allowance values applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); } + transferCounts++; + + if (transferCounts == maxTransfers) { + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); + + transactionBodies.add(syntheticBodyBuilder + .transfers( + TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) + .build()); + + // reset lists and counter + transferCounts = 0; + tokenTransfers.clear(); + hbarTransfers.clear(); + } } - final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); - return syntheticBodyBuilder + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); + transactionBodies.add(syntheticBodyBuilder .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) - .build(); + .build()); + + return transactionBodies; } private static void validateTokenAllowance( @@ -167,7 +199,7 @@ private static List mergeTransfers( final Map consolidated = new LinkedHashMap<>(); consolidateInto(consolidated, from); consolidateInto(consolidated, to); - return consolidated.values().stream().toList(); + return new ArrayList<>(consolidated.values()); } private static void consolidateInto( diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 1cb6aee5bcf9..af59032f917d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -17,26 +17,19 @@ package com.hedera.services.bdd.suites.hip991; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; -import static com.hedera.services.bdd.spec.keys.KeyShape.PREDEFINED_SHAPE; -import static com.hedera.services.bdd.spec.keys.KeyShape.sigs; -import static com.hedera.services.bdd.spec.keys.KeyShape.threshOf; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; -import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; @@ -46,8 +39,6 @@ import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; @@ -274,88 +265,90 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { @DisplayName("Submit message") class SubmitMessage { -// @HapiTest -// @DisplayName("submit") -// final Stream submitMessage() { -// final var collector = "collector"; -// final var payer = "submitter"; -// final var treasury = "treasury"; -// final var token = "testToken"; -// final var secondToken = "secondToken"; -// final var denomToken = "denomToken"; -// final var simpleKey = "simpleKey"; -// final var simpleKey2 = "simpleKey2"; -// final var invalidKey = "invalidKey"; -// final var threshKey = "threshKey"; -// -// return hapiTest( -// // create keys -// newKeyNamed(invalidKey), -// newKeyNamed(simpleKey), -// newKeyNamed(simpleKey2), -// newKeyNamed(threshKey) -// .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) -// .signedWith(sigs(simpleKey2, simpleKey))), -// // create accounts and denomination token -// cryptoCreate(collector).balance(0L), -// cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), -// cryptoCreate(treasury), -// tokenCreate(denomToken) -// .treasury(treasury) -// .tokenType(TokenType.FUNGIBLE_COMMON) -// .initialSupply(500), -// tokenAssociate(collector, denomToken), -// tokenAssociate(payer, denomToken), -// tokenCreate(token) -// .treasury(treasury) -// .tokenType(TokenType.FUNGIBLE_COMMON) -// .withCustom(fixedHtsFee(1, denomToken, collector)) -// .initialSupply(500), -// tokenCreate(secondToken) -// .treasury(treasury) -// .tokenType(TokenType.FUNGIBLE_COMMON) -// .initialSupply(500), -// tokenAssociate(collector, token, secondToken), -// tokenAssociate(payer, token, secondToken), -// cryptoTransfer( -// moving(2, token).between(treasury, payer), -// moving(1, secondToken).between(treasury, payer), -// moving(1, denomToken).between(treasury, payer)), -// -// // create topic with custom fees -// createTopic(TOPIC) -// // .withConsensusCustomFee(fixedConsensusHtsFee(1, token, -// // collector)) -// // .withConsensusCustomFee(fixedConsensusHtsFee(1, secondToken, -// // collector)) -// .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) -// .feeExemptKeys(threshKey) -// .hasKnownStatus(SUCCESS), -// -// // add allowance -// approveTopicAllowance() -// .payingWith(payer) -// .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), -// -// // submit message -// submitMessageTo(TOPIC) -// .message("TEST") -// .signedBy(invalidKey, payer) -// .payingWith(payer) -// .via("submit"), -// -// // check records -// getTxnRecord("submit").andAllChildRecords().logged(), -// -// // assert balances -// getAccountBalance(collector).hasTinyBars(ONE_HBAR)); -// // .hasTokenBalance(token, 2) -// // .hasTokenBalance(denomToken,1) -// // .hasTokenBalance(secondToken, 1), -// // getAccountBalance(payer) -// // .hasTokenBalance(token, 0) -// // .hasTokenBalance(secondToken, 0)); -// } + // @HapiTest + // @DisplayName("submit") + // final Stream submitMessage() { + // final var collector = "collector"; + // final var payer = "submitter"; + // final var treasury = "treasury"; + // final var token = "testToken"; + // final var secondToken = "secondToken"; + // final var denomToken = "denomToken"; + // final var simpleKey = "simpleKey"; + // final var simpleKey2 = "simpleKey2"; + // final var invalidKey = "invalidKey"; + // final var threshKey = "threshKey"; + // + // return hapiTest( + // // create keys + // newKeyNamed(invalidKey), + // newKeyNamed(simpleKey), + // newKeyNamed(simpleKey2), + // newKeyNamed(threshKey) + // .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) + // .signedWith(sigs(simpleKey2, simpleKey))), + // // create accounts and denomination token + // cryptoCreate(collector).balance(0L), + // cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), + // cryptoCreate(treasury), + // tokenCreate(denomToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, denomToken), + // tokenAssociate(payer, denomToken), + // tokenCreate(token) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .withCustom(fixedHtsFee(1, denomToken, collector)) + // .initialSupply(500), + // tokenCreate(secondToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, token, secondToken), + // tokenAssociate(payer, token, secondToken), + // cryptoTransfer( + // moving(2, token).between(treasury, payer), + // moving(1, secondToken).between(treasury, payer), + // moving(1, denomToken).between(treasury, payer)), + // + // // create topic with custom fees + // createTopic(TOPIC) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // token, + // // collector)) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // secondToken, + // // collector)) + // .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + // .feeExemptKeys(threshKey) + // .hasKnownStatus(SUCCESS), + // + // // add allowance + // approveTopicAllowance() + // .payingWith(payer) + // .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), + // + // // submit message + // submitMessageTo(TOPIC) + // .message("TEST") + // .signedBy(invalidKey, payer) + // .payingWith(payer) + // .via("submit"), + // + // // check records + // getTxnRecord("submit").andAllChildRecords().logged(), + // + // // assert balances + // getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + // // .hasTokenBalance(token, 2) + // // .hasTokenBalance(denomToken,1) + // // .hasTokenBalance(secondToken, 1), + // // getAccountBalance(payer) + // // .hasTokenBalance(token, 0) + // // .hasTokenBalance(secondToken, 0)); + // } @Nested @DisplayName("Positive scenarios") @@ -431,18 +424,11 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() // create topic with 10 multilayer fees - 9 HTS + 1 HBAR createTopicWith10Different2layerFees(), approveTopicAllowanceForAllFees(), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER) - // todo for now custom fee will fail, because of limitation in cryptoTransfer - .hasKnownStatus(TOKEN_TRANSFER_LIST_SIZE_LIMIT_EXCEEDED))); + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance -// assertAllCollectorsBalances())); + assertAllCollectorsBalances())); } - - - - - private SpecOperation[] associateAllTokensToCollectors() { final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; From 5e6d71fad651b4bbf9cf3a44d27e83dd7ef604ea Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 8 Oct 2024 11:13:13 +0300 Subject: [PATCH 46/94] Hapi tests, custom fee assessor refactoring Signed-off-by: Zhivko Kelchev --- ...er.java => ConsensusAllowanceUpdater.java} | 36 ++- ...r.java => ConsensusCustomFeeAssessor.java} | 68 ++++-- .../ConsensusSubmitMessageHandler.java | 11 +- .../bdd/suites/hip991/TopicCustomFeeBase.java | 79 +++++-- .../bdd/suites/hip991/TopicCustomFeeTest.java | 223 ++++++++++++++++-- 5 files changed, 339 insertions(+), 78 deletions(-) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{util/ConsensusApproveAllowanceHelper.java => ConsensusAllowanceUpdater.java} (93%) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{util/ConsensusCustomFeeHelper.java => ConsensusCustomFeeAssessor.java} (81%) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java similarity index 93% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java index c95a17a78dd3..24fed8ca2a33 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusApproveAllowanceHelper.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl.util; +package com.hedera.node.app.service.consensus.impl; import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; import static java.util.Objects.requireNonNull; @@ -26,12 +26,26 @@ import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import edu.umd.cs.findbugs.annotations.NonNull; + +import javax.inject.Inject; +import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; -public class ConsensusApproveAllowanceHelper { +// todo update this class when allowances are done!!! +@Singleton +public class ConsensusAllowanceUpdater { + + /** + * Constructs a {@link ConsensusAllowanceUpdater} instance. + */ + @Inject + public ConsensusAllowanceUpdater() { + // Needed for Dagger injection + } + + /** * Applies all changes needed for Crypto allowances from the transaction. * If the topic already has an allowance, the allowance value will be replaced with values @@ -39,7 +53,7 @@ public class ConsensusApproveAllowanceHelper { * @param topicCryptoAllowances the list of crypto allowances * @param topicStore the topic store */ - public static void applyCryptoAllowances( + public void applyCryptoAllowances( @NonNull final List topicCryptoAllowances, @NonNull final WritableTopicStore topicStore) { requireNonNull(topicCryptoAllowances); @@ -62,7 +76,7 @@ public static void applyCryptoAllowances( } } - public static void applyCryptoAllowances( + public void applyCryptoAllowances( @NonNull final TopicID topicId, @NonNull final TopicCryptoAllowance allowance, @NonNull final WritableTopicStore topicStore) { @@ -89,7 +103,7 @@ public static void applyCryptoAllowances( * @param amount the amount * @param spenderId the spender id */ - private static void updateCryptoAllowance( + private void updateCryptoAllowance( final List mutableAllowances, final long amount, final long amountPerMessage, @@ -127,7 +141,7 @@ private static void updateCryptoAllowance( * @param tokenAllowances the list of token allowances * @param topicStore the topic store */ - public static void applyFungibleTokenAllowances( + public void applyFungibleTokenAllowances( @NonNull final List tokenAllowances, @NonNull final WritableTopicStore topicStore) { requireNonNull(tokenAllowances); @@ -154,7 +168,7 @@ public static void applyFungibleTokenAllowances( /* * */ - public static void applyFungibleTokenAllowances( + public void applyFungibleTokenAllowances( @NonNull final TopicID topicId, @NonNull final TopicFungibleTokenAllowance allowance, @NonNull final WritableTopicStore topicStore) { @@ -185,7 +199,7 @@ public static void applyFungibleTokenAllowances( * @param spenderId the spender number * @param tokenId the token number */ - private static void updateTokenAllowance( + private void updateTokenAllowance( final List mutableAllowances, final long amount, final long amountPerMessage, @@ -223,7 +237,7 @@ private static void updateTokenAllowance( * @param spenderNum spender account number * @return index of the allowance if it exists, otherwise -1 */ - private static int lookupSpender( + private int lookupSpender( final List topicCryptoAllowances, final AccountID spenderNum) { for (int i = 0; i < topicCryptoAllowances.size(); i++) { final var allowance = topicCryptoAllowances.get(i); @@ -242,7 +256,7 @@ private static int lookupSpender( * @param tokenId token number * @return index of the allowance if it exists, otherwise -1 */ - private static int lookupSpenderAndToken( + private int lookupSpenderAndToken( final List topicTokenAllowances, final AccountID spenderId, final TokenID tokenId) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java similarity index 81% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java index acab8571799e..85a20ee4462f 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusCustomFeeHelper.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java @@ -14,12 +14,10 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl.util; +package com.hedera.node.app.service.consensus.impl; import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyCryptoAllowances; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusApproveAllowanceHelper.applyFungibleTokenAllowances; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -34,10 +32,13 @@ import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; + +import javax.inject.Inject; +import javax.inject.Singleton; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -46,13 +47,26 @@ import java.util.Map; import java.util.stream.Collectors; -public class ConsensusCustomFeeHelper { +@Singleton +public class ConsensusCustomFeeAssessor { + + private final ConsensusAllowanceUpdater allowanceUpdater; - public static List assessCustomFee(Topic topic, HandleContext context) { + /** + * Constructs a {@link ConsensusCustomFeeAssessor} instance. + */ + @Inject + public ConsensusCustomFeeAssessor(@NonNull final ConsensusAllowanceUpdater allowanceUpdater) { + // Needed for Dagger injection + this.allowanceUpdater = requireNonNull(allowanceUpdater); + } + + public List assessCustomFee(Topic topic, HandleContext context) { final List transactionBodies = new ArrayList<>(); final var payer = context.payer(); final var topicStore = context.storeFactory().writableStore(WritableTopicStore.class); + final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); // todo: allowance validation will be changed, when the storage situation is clear. @@ -76,26 +90,36 @@ public static List assessCustomFee(Topic topic, H List hbarTransfers = new ArrayList<>(); // we need to count the number of balance adjustments, // and if needed to split custom fee transfers in to two separate dispatches - final var maxTransfers = ledgerConfig.transfersMaxLen() / 2; + // todo: add explanation for maxTransfers + final var maxTransfers = ledgerConfig.transfersMaxLen() / 3; var transferCounts = 0; // build crypto transfer body for the first layer of custom fees, // if there is a second layer it will be assessed in crypto transfer handler for (ConsensusCustomFee fee : topic.customFees()) { + // check if payer is treasury or collector + if(context.payer().equals(fee.feeCollectorAccountId())) { + continue; + } + final var fixedFee = fee.fixedFeeOrThrow(); if (fixedFee.hasDenominatingTokenId()) { - final var tokenId = fixedFee.denominatingTokenId(); - validateTokenAllowance(tokenAllowanceMap, fixedFee); + final var tokenId = fixedFee.denominatingTokenIdOrThrow(); + final var tokenTreasury = tokenStore.get(tokenId).treasuryAccountIdOrThrow(); + if(context.payer().equals(tokenTreasury)) { + continue; + } + validateTokenAllowance(tokenAllowanceMap, fixedFee); tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); // update allowance values - applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); + allowanceUpdater.applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); } else { validateHbarAllowance(hbarAllowance, fixedFee); hbarTransfers = mergeTransfers( hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); // update allowance values - applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); + allowanceUpdater.applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); } transferCounts++; @@ -114,6 +138,10 @@ public static List assessCustomFee(Topic topic, H } } + if(tokenTransfers.isEmpty() && hbarTransfers.isEmpty()) { + return transactionBodies; + } + final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); transactionBodies.add(syntheticBodyBuilder .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) @@ -122,7 +150,7 @@ public static List assessCustomFee(Topic topic, H return transactionBodies; } - private static void validateTokenAllowance( + private void validateTokenAllowance( Map tokenAllowanceMap, FixedFee fixedFee) { final var allowance = tokenAllowanceMap.get(fixedFee.denominatingTokenId()); validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); @@ -130,18 +158,18 @@ private static void validateTokenAllowance( validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); } - private static void validateHbarAllowance(TopicCryptoAllowance allowance, FixedFee fixedFee) { + private void validateHbarAllowance(TopicCryptoAllowance allowance, FixedFee fixedFee) { validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); } - public static void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { + public void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { // todo adjust allowance // extract the code for updating the allowance amounts from ConsensusApproveAllowanceHandler and reuse it here } - private static List buildCustomFeeHbarTransferList( + private List buildCustomFeeHbarTransferList( AccountID payer, AccountID collector, FixedFee fee) { return List.of( AccountAmount.newBuilder() @@ -154,7 +182,7 @@ private static List buildCustomFeeHbarTransferList( .build()); } - private static TokenTransferList buildCustomFeeTokenTransferList( + private TokenTransferList buildCustomFeeTokenTransferList( AccountID payer, AccountID collector, FixedFee fee) { return TokenTransferList.newBuilder() .token(fee.denominatingTokenId()) @@ -170,7 +198,7 @@ private static TokenTransferList buildCustomFeeTokenTransferList( .build(); } - private static CryptoTransferTransactionBody.Builder tokenTransfers( + private CryptoTransferTransactionBody.Builder tokenTransfers( @NonNull TokenTransferList... tokenTransferLists) { if (repeatsTokenId(tokenTransferLists)) { final Map consolidatedTokenTransfers = new LinkedHashMap<>(); @@ -178,7 +206,7 @@ private static CryptoTransferTransactionBody.Builder tokenTransfers( consolidatedTokenTransfers.merge( tokenTransferList.tokenOrThrow(), tokenTransferList, - ConsensusCustomFeeHelper::mergeTokenTransferLists); + ConsensusCustomFeeAssessor::mergeTokenTransferLists); } tokenTransferLists = consolidatedTokenTransfers.values().toArray(TokenTransferList[]::new); } @@ -205,7 +233,7 @@ private static List mergeTransfers( private static void consolidateInto( @NonNull final Map consolidated, @NonNull final List transfers) { for (final var transfer : transfers) { - consolidated.merge(transfer.accountID(), transfer, ConsensusCustomFeeHelper::mergeAdjusts); + consolidated.merge(transfer.accountID(), transfer, ConsensusCustomFeeAssessor::mergeAdjusts); } } @@ -216,7 +244,7 @@ private static AccountAmount mergeAdjusts(@NonNull final AccountAmount from, @No .build(); } - private static boolean repeatsTokenId(@NonNull final TokenTransferList[] tokenTransferList) { + private boolean repeatsTokenId(@NonNull final TokenTransferList[] tokenTransferList) { return tokenTransferList.length > 1 && Arrays.stream(tokenTransferList) .map(TokenTransferList::token) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index c5e3be9d7aca..e77aa2db599d 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -46,7 +46,7 @@ import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; -import com.hedera.node.app.service.consensus.impl.util.ConsensusCustomFeeHelper; +import com.hedera.node.app.service.consensus.impl.ConsensusCustomFeeAssessor; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; @@ -75,10 +75,11 @@ @Singleton public class ConsensusSubmitMessageHandler implements TransactionHandler { public static final long RUNNING_HASH_VERSION = 3L; + private final ConsensusCustomFeeAssessor customFeeAssessor; @Inject - public ConsensusSubmitMessageHandler() { - // Exists for injection + public ConsensusSubmitMessageHandler(@NonNull ConsensusCustomFeeAssessor customFeeAssessor) { + this.customFeeAssessor = requireNonNull(customFeeAssessor); } @Override @@ -145,7 +146,7 @@ public void handle(@NonNull final HandleContext handleContext) { } if (!topic.customFees().isEmpty() && !payerIsFeeExempted) { // validate and create synthetic body - final var syntheticBodies = ConsensusCustomFeeHelper.assessCustomFee(topic, handleContext); + final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); for (final var syntheticBody : syntheticBodies) { // dispatch crypto transfer var record = handleContext.dispatchChildTransaction( @@ -159,7 +160,7 @@ var record = handleContext.dispatchChildTransaction( HandleContext.ConsensusThrottling.OFF); validateTrue(record.status().equals(SUCCESS), record.status()); // update total allowances - ConsensusCustomFeeHelper.adjustAllowance(syntheticBody); + customFeeAssessor.adjustAllowance(syntheticBody); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index f37d5d70838f..d77585e0acd0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -21,6 +21,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; @@ -32,6 +33,7 @@ import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.TokenType; import java.util.ArrayList; +import java.util.List; public class TopicCustomFeeBase { protected static final String TOPIC = "topic"; @@ -49,8 +51,14 @@ public class TopicCustomFeeBase { /* Submit message entities */ protected static final String SUBMITTER = "submitter"; protected static final String TOKEN_TREASURY = "tokenTreasury"; + protected static final String DENOM_TREASURY = "denomTreasury"; protected static final String BASE_TOKEN = "baseToken"; - protected static final String MULTI_LAYER_FEE_PREFIX = "multiLayerFeePrefix_"; + + /* tokens with multilayer fees */ + protected static final String TOKEN_PREFIX = "token_"; + protected static final String COLLECTOR_PREFIX = "collector_"; + protected static final String DENOM_TOKEN_PREFIX = "denomToken_"; + // This key is truly invalid, as all Ed25519 public keys must be 32 bytes long protected static final Key STRUCTURALLY_INVALID_KEY = Key.newBuilder().setEd25519(ByteString.fromHex("ff")).build(); @@ -90,28 +98,63 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { } /** - * Create and transfer multiple tokens with fixed hbar custom fee to account. - * @param account account to transfer tokens + * Create and transfer multiple tokens with 2 layer custom fees to given account. + * + * @param owner account to transfer tokens * @param numberOfTokens the count of tokens to be transferred * @return array of spec operations */ - protected SpecOperation[] transferMultiLayerFeeTokensTo(String account, int numberOfTokens) { - final var treasury = MULTI_LAYER_FEE_PREFIX + TOKEN_TREASURY; - final var list = new ArrayList(); - list.add(cryptoCreate(treasury)); + protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int numberOfTokens) { + final var specOperations = new ArrayList(); + specOperations.add(cryptoCreate(DENOM_TREASURY)); + specOperations.add(cryptoCreate(TOKEN_TREASURY)); for (int i = 0; i < numberOfTokens; i++) { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_" + i; - final var collectorName = MULTI_LAYER_FEE_PREFIX + "collector_" + i; - list.add(cryptoCreate(collectorName).balance(0L)); - list.add(tokenCreate(tokenName) - .tokenType(TokenType.FUNGIBLE_COMMON) - .treasury(treasury) - .initialSupply(500L) - .withCustom(fixedHbarFee(ONE_HBAR, collectorName))); - list.add(tokenAssociate(account, tokenName)); - list.add(cryptoTransfer(moving(500L, tokenName).between(treasury, account))); + final var tokenName = TOKEN_PREFIX + i; + specOperations.addAll(createTokenWith2LayerFee(owner, tokenName, false)); } - return list.toArray(new SpecOperation[0]); + return specOperations.toArray(new SpecOperation[0]); + } + + + /** + * + * + * + * @param owner + * @param tokenName + * @param createTreasury + * @return + */ + protected static List createTokenWith2LayerFee(String owner, String tokenName, boolean createTreasury) { + final var specOperations = new ArrayList(); + final var collectorName = COLLECTOR_PREFIX + tokenName; + final var denomToken = DENOM_TOKEN_PREFIX + tokenName; + // if we generate multiple tokens, there will be no need to create treasury every time we create new token + if (createTreasury) { + specOperations.add(cryptoCreate(DENOM_TREASURY)); + specOperations.add(cryptoCreate(TOKEN_TREASURY)); + } + // create first common collector + specOperations.add(cryptoCreate(collectorName).balance(0L)); + // create denomination token with hbar fee + specOperations.add(tokenCreate(denomToken) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(DENOM_TREASURY) + .withCustom(fixedHbarFee(ONE_HBAR, collectorName))); + // associate the denomination token with the collector + specOperations.add(tokenAssociate(collectorName, denomToken)); + // create the token with fixed HTS fee + specOperations.add(tokenCreate(tokenName) + .tokenType(TokenType.FUNGIBLE_COMMON) + .treasury(TOKEN_TREASURY) + .withCustom(fixedHtsFee(1, denomToken, collectorName))); + // associate the owner with the two new tokens + specOperations.add(tokenAssociate(owner, tokenName)); + specOperations.add(tokenAssociate(owner, denomToken)); + // transfer the tokens to the owner + specOperations.add(cryptoTransfer(moving(100L, tokenName).between(TOKEN_TREASURY, owner))); + specOperations.add(cryptoTransfer(moving(100L, denomToken).between(DENOM_TREASURY, owner))); + return specOperations; } protected static SpecOperation[] newNamedKeysForFEKL(int count) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index af59032f917d..e28f9074b2dd 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -23,6 +23,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; @@ -30,6 +31,8 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.logIt; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; @@ -44,6 +47,7 @@ import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; import com.hedera.services.bdd.spec.SpecOperation; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hederahashgraph.api.proto.java.TokenType; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; @@ -361,6 +365,7 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { @HapiTest @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") + // TOPIC_FEE_104 final Stream messageSubmitToPublicTopicWithFee1Hbar() { final var collector = "collector"; final var submitter = "submitter"; @@ -377,6 +382,7 @@ final Stream messageSubmitToPublicTopicWithFee1Hbar() { @HapiTest @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") + // TOPIC_FEE_105 final Stream messageSubmitToPublicTopicWithFee1token() { final var collector = "collector"; return hapiTest( @@ -391,34 +397,39 @@ final Stream messageSubmitToPublicTopicWithFee1token() { } @HapiTest - @DisplayName("MessageSubmit to a public topic with 2 layer fee") - final Stream messageSubmitToPublicTopicWith2layerFee() { + @DisplayName("MessageSubmit to a public topic with 3 layer fee") + // TOPIC_FEE_106 + final Stream messageSubmitToPublicTopicWith3layerFee() { final var topicFeeCollector = "collector"; - final var token = MULTI_LAYER_FEE_PREFIX + "token_0"; - final var tokenFeeCollector = MULTI_LAYER_FEE_PREFIX + "collector_0"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; return hapiTest(flattened( - cryptoCreate(topicFeeCollector).balance(0L), // create denomination token and transfer it to the submitter - transferMultiLayerFeeTokensTo(SUBMITTER, 1), - tokenAssociate(topicFeeCollector, token), + createTokenWith2LayerFee(SUBMITTER, token, true), // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance approveTopicAllowance() .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) .payingWith(SUBMITTER), + // submit message submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), // assert token fee collector balance - getAccountBalance(tokenFeeCollector).hasTinyBars(ONE_HBAR))); + getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 1).hasTinyBars(ONE_HBAR))); } @HapiTest - @DisplayName("MessageSubmit to a public topic with 10 different 2 layer fees") + @DisplayName("MessageSubmit to a public topic with 10 different 3 layer fees") + // TOPIC_FEE_108 final Stream messageSubmitToPublicTopicWith10different2layerFees() { return hapiTest(flattened( // create 9 denomination tokens and transfer them to the submitter - transferMultiLayerFeeTokensTo(SUBMITTER, 9), + createMultipleTokensWith2LayerFees(SUBMITTER, 9), // create 9 collectors and associate them with tokens associateAllTokensToCollectors(), // create topic with 10 multilayer fees - 9 HTS + 1 HBAR @@ -429,54 +440,218 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() assertAllCollectorsBalances())); } + // TOPIC_FEE_108 private SpecOperation[] associateAllTokensToCollectors() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; final var associateTokensToCollectors = new ArrayList(); for (int i = 0; i < 9; i++) { associateTokensToCollectors.add( cryptoCreate(collectorName + i).balance(0L)); - associateTokensToCollectors.add(tokenAssociate(collectorName + i, tokenName + i)); + associateTokensToCollectors.add(tokenAssociate(collectorName + i, TOKEN_PREFIX + i)); } return associateTokensToCollectors.toArray(SpecOperation[]::new); } - + // TOPIC_FEE_108 private SpecOperation createTopicWith10Different2layerFees() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; final var topicCreateOp = createTopic(TOPIC); for (int i = 0; i < 9; i++) { - topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, tokenName + i, collectorName + i)); + topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, TOKEN_PREFIX + i, collectorName + i)); } // add one hbar custom fee topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); return topicCreateOp; } - + // TOPIC_FEE_108 private SpecOperation approveTopicAllowanceForAllFees() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); for (int i = 0; i < 9; i++) { - approveAllowance.addTokenAllowance(SUBMITTER, tokenName + i, TOPIC, 100, 1); + approveAllowance.addTokenAllowance(SUBMITTER, TOKEN_PREFIX + i, TOPIC, 100, 1); } approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); return approveAllowance; } - + // TOPIC_FEE_108 private SpecOperation[] assertAllCollectorsBalances() { - final var tokenName = MULTI_LAYER_FEE_PREFIX + "token_"; final var collectorName = "collector_"; - final var assertBalances = new ArrayList(); - + // assert token balances for (int i = 0; i < 9; i++) { - assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(tokenName + i, 1)); + assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(TOKEN_PREFIX + i, 1)); } - // add assert for hbar fee + // add assert for hbar assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); return assertBalances.toArray(SpecOperation[]::new); } + + @HapiTest + @DisplayName("Treasury submit to a public topic with 3 layer fees") + // TOPIC_FEE_109 + final Stream treasurySubmitToPublicTopicWith3layerFees() { + final var topicFeeCollector = "collector"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; + + return hapiTest(flattened( + // create denomination token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + // submit message + submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); + } + + + @HapiTest + @DisplayName("Treasury second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_110 + final Stream treasuryOfSecondLayerSubmitToPublic() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(DENOM_TREASURY, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, DENOM_TREASURY)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(DENOM_TREASURY, token, TOPIC, 100, 1) + .payingWith(DENOM_TREASURY), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), + + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); + } + + + @HapiTest + @DisplayName("Collector submit to a public topic with 3 layer fees") + // TOPIC_FEE_111 + final Stream collectorSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // transfer one token to the collector, to be able to pay the fee + cryptoCreate(topicFeeCollector).balance(ONE_HBAR), + tokenAssociate(topicFeeCollector, token), + + // create topic + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(topicFeeCollector, token, TOPIC, 100, 1) + .payingWith(topicFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), + + // assert balances + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + getAccountBalance(COLLECTOR_PREFIX+token).hasTokenBalance(denomToken, 0))); + } + + + @HapiTest + @DisplayName("Collector of second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_112 + final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var secondLayerFeeCollector = COLLECTOR_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(secondLayerFeeCollector, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, secondLayerFeeCollector)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(secondLayerFeeCollector, token, TOPIC, 100, 1) + .payingWith(secondLayerFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), + + // assert topic fee collector balance - only first layer fee should be paid + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // token fee collector should have 1 token from the first transfer and 0 from msg submit + getAccountBalance(secondLayerFeeCollector).hasTokenBalance(denomToken, 1))); + } + + @HapiTest + @DisplayName("Another collector submit message to a topic with a fee") + // TOPIC_FEE_113 + final Stream anotherCollectorSubmitMessageToATopicWithAFee() { + final var collector = "collector"; + final var anotherToken = "anotherToken"; + final var anotherCollector = COLLECTOR_PREFIX + anotherToken; + return hapiTest(flattened( + // create another token with fixed fee + createTokenWith2LayerFee(SUBMITTER, anotherToken, true), + tokenAssociate(anotherCollector, BASE_TOKEN), + cryptoTransfer( + moving(100, BASE_TOKEN).between(SUBMITTER, anotherCollector), + TokenMovement.movingHbar(ONE_HBAR).between(SUBMITTER, anotherCollector) + ), + // create topic + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + // add allowance and submit with another collector + approveTopicAllowance() + .addTokenAllowance(anotherCollector, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(anotherCollector), + submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), + // the fee was paid + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1) + )); + } + + } } From 2cca7388b80e659b614934cfc8383593963b9884 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 8 Oct 2024 14:12:07 +0300 Subject: [PATCH 47/94] Refactor Signed-off-by: Zhivko Kelchev --- .../ConsensusSubmitMessageHandler.java | 2 +- .../customfee}/ConsensusAllowanceUpdater.java | 12 +- .../ConsensusCustomFeeAssessor.java | 26 +- .../src/main/java/module-info.java | 5 +- .../bdd/suites/hip991/TopicCustomFeeBase.java | 4 +- .../TopicCustomFeeSubmitMessageTest.java | 429 ++++++++++++++++++ .../bdd/suites/hip991/TopicCustomFeeTest.java | 399 ---------------- 7 files changed, 453 insertions(+), 424 deletions(-) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{ => handlers/customfee}/ConsensusAllowanceUpdater.java (97%) rename hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/{ => handlers/customfee}/ConsensusCustomFeeAssessor.java (93%) create mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index e77aa2db599d..6f6b714397e3 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -45,8 +45,8 @@ import com.hedera.node.app.hapi.utils.CommonPbjConverters; import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusCustomFeeAssessor; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; -import com.hedera.node.app.service.consensus.impl.ConsensusCustomFeeAssessor; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java similarity index 97% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java index 24fed8ca2a33..f7b613d3584a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusAllowanceUpdater.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl; +package com.hedera.node.app.service.consensus.impl.handlers.customfee; import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; import static java.util.Objects.requireNonNull; @@ -26,12 +26,12 @@ import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import edu.umd.cs.findbugs.annotations.NonNull; - -import javax.inject.Inject; -import javax.inject.Singleton; import java.util.ArrayList; import java.util.List; +import javax.inject.Inject; +import javax.inject.Singleton; // todo update this class when allowances are done!!! @Singleton @@ -45,7 +45,6 @@ public ConsensusAllowanceUpdater() { // Needed for Dagger injection } - /** * Applies all changes needed for Crypto allowances from the transaction. * If the topic already has an allowance, the allowance value will be replaced with values @@ -237,8 +236,7 @@ private void updateTokenAllowance( * @param spenderNum spender account number * @return index of the allowance if it exists, otherwise -1 */ - private int lookupSpender( - final List topicCryptoAllowances, final AccountID spenderNum) { + private int lookupSpender(final List topicCryptoAllowances, final AccountID spenderNum) { for (int i = 0; i < topicCryptoAllowances.size(); i++) { final var allowance = topicCryptoAllowances.get(i); if (allowance.spenderIdOrThrow().equals(spenderNum)) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java similarity index 93% rename from hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java rename to hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 85a20ee4462f..81af6e85628a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.hedera.node.app.service.consensus.impl; +package com.hedera.node.app.service.consensus.impl.handlers.customfee; import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; @@ -32,13 +32,11 @@ import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; +import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; - -import javax.inject.Inject; -import javax.inject.Singleton; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -46,6 +44,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import javax.inject.Inject; +import javax.inject.Singleton; @Singleton public class ConsensusCustomFeeAssessor { @@ -98,7 +98,7 @@ public List assessCustomFee(Topic topic, HandleCo // if there is a second layer it will be assessed in crypto transfer handler for (ConsensusCustomFee fee : topic.customFees()) { // check if payer is treasury or collector - if(context.payer().equals(fee.feeCollectorAccountId())) { + if (context.payer().equals(fee.feeCollectorAccountId())) { continue; } @@ -106,14 +106,15 @@ public List assessCustomFee(Topic topic, HandleCo if (fixedFee.hasDenominatingTokenId()) { final var tokenId = fixedFee.denominatingTokenIdOrThrow(); final var tokenTreasury = tokenStore.get(tokenId).treasuryAccountIdOrThrow(); - if(context.payer().equals(tokenTreasury)) { + if (context.payer().equals(tokenTreasury)) { continue; } validateTokenAllowance(tokenAllowanceMap, fixedFee); tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); // update allowance values - allowanceUpdater.applyFungibleTokenAllowances(topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); + allowanceUpdater.applyFungibleTokenAllowances( + topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); } else { validateHbarAllowance(hbarAllowance, fixedFee); hbarTransfers = mergeTransfers( @@ -138,7 +139,7 @@ public List assessCustomFee(Topic topic, HandleCo } } - if(tokenTransfers.isEmpty() && hbarTransfers.isEmpty()) { + if (tokenTransfers.isEmpty() && hbarTransfers.isEmpty()) { return transactionBodies; } @@ -169,8 +170,7 @@ public void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { // extract the code for updating the allowance amounts from ConsensusApproveAllowanceHandler and reuse it here } - private List buildCustomFeeHbarTransferList( - AccountID payer, AccountID collector, FixedFee fee) { + private List buildCustomFeeHbarTransferList(AccountID payer, AccountID collector, FixedFee fee) { return List.of( AccountAmount.newBuilder() .accountID(payer) @@ -182,8 +182,7 @@ private List buildCustomFeeHbarTransferList( .build()); } - private TokenTransferList buildCustomFeeTokenTransferList( - AccountID payer, AccountID collector, FixedFee fee) { + private TokenTransferList buildCustomFeeTokenTransferList(AccountID payer, AccountID collector, FixedFee fee) { return TokenTransferList.newBuilder() .token(fee.denominatingTokenId()) .transfers( @@ -198,8 +197,7 @@ private TokenTransferList buildCustomFeeTokenTransferList( .build(); } - private CryptoTransferTransactionBody.Builder tokenTransfers( - @NonNull TokenTransferList... tokenTransferLists) { + private CryptoTransferTransactionBody.Builder tokenTransfers(@NonNull TokenTransferList... tokenTransferLists) { if (repeatsTokenId(tokenTransferLists)) { final Map consolidatedTokenTransfers = new LinkedHashMap<>(); for (final var tokenTransferList : tokenTransferLists) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java index 5665b34dc129..40f635f1a72b 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java @@ -23,7 +23,10 @@ exports com.hedera.node.app.service.consensus.impl to com.hedera.node.app, com.hedera.node.test.clients; - exports com.hedera.node.app.service.consensus.impl.handlers; + exports com.hedera.node.app.service.consensus.impl.handlers to + com.hedera.node.app; + exports com.hedera.node.app.service.consensus.impl.handlers.customfee to + com.hedera.node.app; exports com.hedera.node.app.service.consensus.impl.records; exports com.hedera.node.app.service.consensus.impl.schemas; exports com.hedera.node.app.service.consensus.impl.validators; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index d77585e0acd0..780429656b26 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -115,7 +115,6 @@ protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int n return specOperations.toArray(new SpecOperation[0]); } - /** * * @@ -125,7 +124,8 @@ protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int n * @param createTreasury * @return */ - protected static List createTokenWith2LayerFee(String owner, String tokenName, boolean createTreasury) { + protected static List createTokenWith2LayerFee( + String owner, String tokenName, boolean createTreasury) { final var specOperations = new ArrayList(); final var collectorName = COLLECTOR_PREFIX + tokenName; final var denomToken = DENOM_TOKEN_PREFIX + tokenName; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java new file mode 100644 index 000000000000..ad4193f30a9a --- /dev/null +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -0,0 +1,429 @@ +/* + * Copyright (C) 2024 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.services.bdd.suites.hip991; + +import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; +import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; +import static com.hedera.services.bdd.suites.HapiSuite.flattened; + +import com.hedera.services.bdd.junit.HapiTest; +import com.hedera.services.bdd.junit.HapiTestLifecycle; +import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.SpecOperation; +import com.hedera.services.bdd.spec.transactions.token.TokenMovement; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.ArrayList; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; + +@HapiTestLifecycle +@DisplayName("Submit message") +public class TopicCustomFeeSubmitMessageTest extends TopicCustomFeeBase { + // @HapiTest + // @DisplayName("submit") + // final Stream submitMessage() { + // final var collector = "collector"; + // final var payer = "submitter"; + // final var treasury = "treasury"; + // final var token = "testToken"; + // final var secondToken = "secondToken"; + // final var denomToken = "denomToken"; + // final var simpleKey = "simpleKey"; + // final var simpleKey2 = "simpleKey2"; + // final var invalidKey = "invalidKey"; + // final var threshKey = "threshKey"; + // + // return hapiTest( + // // create keys + // newKeyNamed(invalidKey), + // newKeyNamed(simpleKey), + // newKeyNamed(simpleKey2), + // newKeyNamed(threshKey) + // .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) + // .signedWith(sigs(simpleKey2, simpleKey))), + // // create accounts and denomination token + // cryptoCreate(collector).balance(0L), + // cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), + // cryptoCreate(treasury), + // tokenCreate(denomToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, denomToken), + // tokenAssociate(payer, denomToken), + // tokenCreate(token) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .withCustom(fixedHtsFee(1, denomToken, collector)) + // .initialSupply(500), + // tokenCreate(secondToken) + // .treasury(treasury) + // .tokenType(TokenType.FUNGIBLE_COMMON) + // .initialSupply(500), + // tokenAssociate(collector, token, secondToken), + // tokenAssociate(payer, token, secondToken), + // cryptoTransfer( + // moving(2, token).between(treasury, payer), + // moving(1, secondToken).between(treasury, payer), + // moving(1, denomToken).between(treasury, payer)), + // + // // create topic with custom fees + // createTopic(TOPIC) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // token, + // // collector)) + // // .withConsensusCustomFee(fixedConsensusHtsFee(1, + // secondToken, + // // collector)) + // .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) + // .feeExemptKeys(threshKey) + // .hasKnownStatus(SUCCESS), + // + // // add allowance + // approveTopicAllowance() + // .payingWith(payer) + // .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), + // + // // submit message + // submitMessageTo(TOPIC) + // .message("TEST") + // .signedBy(invalidKey, payer) + // .payingWith(payer) + // .via("submit"), + // + // // check records + // getTxnRecord("submit").andAllChildRecords().logged(), + // + // // assert balances + // getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + // // .hasTokenBalance(token, 2) + // // .hasTokenBalance(denomToken,1) + // // .hasTokenBalance(secondToken, 1), + // // getAccountBalance(payer) + // // .hasTokenBalance(token, 0) + // // .hasTokenBalance(secondToken, 0)); + // } + + @Nested + @DisplayName("Positive scenarios") + class SubmitMessagesPositiveScenarios { + + @BeforeAll + static void beforeAll(@NonNull final TestLifecycle lifecycle) { + lifecycle.doAdhoc(associateFeeTokensAndSubmitter()); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") + // TOPIC_FEE_104 + final Stream messageSubmitToPublicTopicWithFee1Hbar() { + final var collector = "collector"; + final var submitter = "submitter"; + return hapiTest( + cryptoCreate(collector).balance(0L), + cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(submitter), + submitMessageTo(TOPIC).message("TEST").payingWith(submitter), + getAccountBalance(collector).hasTinyBars(ONE_HBAR)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") + // TOPIC_FEE_105 + final Stream messageSubmitToPublicTopicWithFee1token() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 3 layer fee") + // TOPIC_FEE_106 + final Stream messageSubmitToPublicTopicWith3layerFee() { + final var topicFeeCollector = "collector"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; + return hapiTest(flattened( + // create denomination token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + // submit message + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector) + .hasTokenBalance(denomToken, 1) + .hasTinyBars(ONE_HBAR))); + } + + @HapiTest + @DisplayName("MessageSubmit to a public topic with 10 different 3 layer fees") + // TOPIC_FEE_108 + final Stream messageSubmitToPublicTopicWith10different2layerFees() { + return hapiTest(flattened( + // create 9 denomination tokens and transfer them to the submitter + createMultipleTokensWith2LayerFees(SUBMITTER, 9), + // create 9 collectors and associate them with tokens + associateAllTokensToCollectors(), + // create topic with 10 multilayer fees - 9 HTS + 1 HBAR + createTopicWith10Different2layerFees(), + approveTopicAllowanceForAllFees(), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + // assert topic fee collector balance + assertAllCollectorsBalances())); + } + + // TOPIC_FEE_108 + private SpecOperation[] associateAllTokensToCollectors() { + final var collectorName = "collector_"; + final var associateTokensToCollectors = new ArrayList(); + for (int i = 0; i < 9; i++) { + associateTokensToCollectors.add(cryptoCreate(collectorName + i).balance(0L)); + associateTokensToCollectors.add(tokenAssociate(collectorName + i, TOKEN_PREFIX + i)); + } + return associateTokensToCollectors.toArray(SpecOperation[]::new); + } + // TOPIC_FEE_108 + private SpecOperation createTopicWith10Different2layerFees() { + final var collectorName = "collector_"; + final var topicCreateOp = createTopic(TOPIC); + for (int i = 0; i < 9; i++) { + topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, TOKEN_PREFIX + i, collectorName + i)); + } + // add one hbar custom fee + topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); + return topicCreateOp; + } + // TOPIC_FEE_108 + private SpecOperation approveTopicAllowanceForAllFees() { + final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); + + for (int i = 0; i < 9; i++) { + approveAllowance.addTokenAllowance(SUBMITTER, TOKEN_PREFIX + i, TOPIC, 100, 1); + } + approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); + return approveAllowance; + } + // TOPIC_FEE_108 + private SpecOperation[] assertAllCollectorsBalances() { + final var collectorName = "collector_"; + final var assertBalances = new ArrayList(); + // assert token balances + for (int i = 0; i < 9; i++) { + assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(TOKEN_PREFIX + i, 1)); + } + // add assert for hbar + assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); + return assertBalances.toArray(SpecOperation[]::new); + } + + @HapiTest + @DisplayName("Treasury submit to a public topic with 3 layer fees") + // TOPIC_FEE_109 + final Stream treasurySubmitToPublicTopicWith3layerFees() { + final var topicFeeCollector = "collector"; + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var tokenFeeCollector = COLLECTOR_PREFIX + token; + + return hapiTest(flattened( + // create denomination token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + // create topic with multilayer fee + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + // add allowance + approveTopicAllowance() + .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) + .payingWith(SUBMITTER), + // submit message + submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + // assert token fee collector balance + getAccountBalance(tokenFeeCollector) + .hasTokenBalance(denomToken, 0) + .hasTinyBars(0))); + } + + @HapiTest + @DisplayName("Treasury second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_110 + final Stream treasuryOfSecondLayerSubmitToPublic() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(DENOM_TREASURY, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, DENOM_TREASURY)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(DENOM_TREASURY, token, TOPIC, 100, 1) + .payingWith(DENOM_TREASURY), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), + + // assert topic fee collector balance + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // assert token fee collector balance + getAccountBalance(topicFeeCollector) + .hasTokenBalance(denomToken, 0) + .hasTinyBars(0))); + } + + @HapiTest + @DisplayName("Collector submit to a public topic with 3 layer fees") + // TOPIC_FEE_111 + final Stream collectorSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // transfer one token to the collector, to be able to pay the fee + cryptoCreate(topicFeeCollector).balance(ONE_HBAR), + tokenAssociate(topicFeeCollector, token), + + // create topic + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(topicFeeCollector, token, TOPIC, 100, 1) + .payingWith(topicFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), + + // assert balances + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), + getAccountBalance(COLLECTOR_PREFIX + token).hasTokenBalance(denomToken, 0))); + } + + @HapiTest + @DisplayName("Collector of second layer submit to a public topic with 3 layer fees") + // TOPIC_FEE_112 + final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFees() { + final var token = "token"; + final var denomToken = DENOM_TOKEN_PREFIX + token; + final var secondLayerFeeCollector = COLLECTOR_PREFIX + token; + final var topicFeeCollector = "topicFeeCollector"; + + return hapiTest(flattened( + // create token and transfer it to the submitter + createTokenWith2LayerFee(SUBMITTER, token, true), + + // give one token to denomToken treasury to be able to pay the fee + tokenAssociate(secondLayerFeeCollector, token), + cryptoTransfer(moving(1, token).between(SUBMITTER, secondLayerFeeCollector)), + + // create topic + cryptoCreate(topicFeeCollector).balance(0L), + tokenAssociate(topicFeeCollector, token), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + + // add allowance for denomination token treasury + approveTopicAllowance() + .addTokenAllowance(secondLayerFeeCollector, token, TOPIC, 100, 1) + .payingWith(secondLayerFeeCollector), + + // submit + submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), + + // assert topic fee collector balance - only first layer fee should be paid + getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), + // token fee collector should have 1 token from the first transfer and 0 from msg submit + getAccountBalance(secondLayerFeeCollector).hasTokenBalance(denomToken, 1))); + } + + @HapiTest + @DisplayName("Another collector submit message to a topic with a fee") + // TOPIC_FEE_113 + final Stream anotherCollectorSubmitMessageToATopicWithAFee() { + final var collector = "collector"; + final var anotherToken = "anotherToken"; + final var anotherCollector = COLLECTOR_PREFIX + anotherToken; + return hapiTest(flattened( + // create another token with fixed fee + createTokenWith2LayerFee(SUBMITTER, anotherToken, true), + tokenAssociate(anotherCollector, BASE_TOKEN), + cryptoTransfer( + moving(100, BASE_TOKEN).between(SUBMITTER, anotherCollector), + TokenMovement.movingHbar(ONE_HBAR).between(SUBMITTER, anotherCollector)), + // create topic + cryptoCreate(collector), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + // add allowance and submit with another collector + approveTopicAllowance() + .addTokenAllowance(anotherCollector, BASE_TOKEN, TOPIC, 100, 1) + .payingWith(anotherCollector), + submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), + // the fee was paid + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1))); + } + } +} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index e28f9074b2dd..18e6803be8d4 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -17,26 +17,20 @@ package com.hedera.services.bdd.suites.hip991; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; -import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.logIt; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; -import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.flattened; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; @@ -46,11 +40,8 @@ import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; -import com.hedera.services.bdd.spec.SpecOperation; -import com.hedera.services.bdd.spec.transactions.token.TokenMovement; import com.hederahashgraph.api.proto.java.TokenType; import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; import org.junit.jupiter.api.BeforeAll; @@ -265,396 +256,6 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { } } - @Nested - @DisplayName("Submit message") - class SubmitMessage { - - // @HapiTest - // @DisplayName("submit") - // final Stream submitMessage() { - // final var collector = "collector"; - // final var payer = "submitter"; - // final var treasury = "treasury"; - // final var token = "testToken"; - // final var secondToken = "secondToken"; - // final var denomToken = "denomToken"; - // final var simpleKey = "simpleKey"; - // final var simpleKey2 = "simpleKey2"; - // final var invalidKey = "invalidKey"; - // final var threshKey = "threshKey"; - // - // return hapiTest( - // // create keys - // newKeyNamed(invalidKey), - // newKeyNamed(simpleKey), - // newKeyNamed(simpleKey2), - // newKeyNamed(threshKey) - // .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) - // .signedWith(sigs(simpleKey2, simpleKey))), - // // create accounts and denomination token - // cryptoCreate(collector).balance(0L), - // cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), - // cryptoCreate(treasury), - // tokenCreate(denomToken) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .initialSupply(500), - // tokenAssociate(collector, denomToken), - // tokenAssociate(payer, denomToken), - // tokenCreate(token) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .withCustom(fixedHtsFee(1, denomToken, collector)) - // .initialSupply(500), - // tokenCreate(secondToken) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .initialSupply(500), - // tokenAssociate(collector, token, secondToken), - // tokenAssociate(payer, token, secondToken), - // cryptoTransfer( - // moving(2, token).between(treasury, payer), - // moving(1, secondToken).between(treasury, payer), - // moving(1, denomToken).between(treasury, payer)), - // - // // create topic with custom fees - // createTopic(TOPIC) - // // .withConsensusCustomFee(fixedConsensusHtsFee(1, - // token, - // // collector)) - // // .withConsensusCustomFee(fixedConsensusHtsFee(1, - // secondToken, - // // collector)) - // .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) - // .feeExemptKeys(threshKey) - // .hasKnownStatus(SUCCESS), - // - // // add allowance - // approveTopicAllowance() - // .payingWith(payer) - // .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), - // - // // submit message - // submitMessageTo(TOPIC) - // .message("TEST") - // .signedBy(invalidKey, payer) - // .payingWith(payer) - // .via("submit"), - // - // // check records - // getTxnRecord("submit").andAllChildRecords().logged(), - // - // // assert balances - // getAccountBalance(collector).hasTinyBars(ONE_HBAR)); - // // .hasTokenBalance(token, 2) - // // .hasTokenBalance(denomToken,1) - // // .hasTokenBalance(secondToken, 1), - // // getAccountBalance(payer) - // // .hasTokenBalance(token, 0) - // // .hasTokenBalance(secondToken, 0)); - // } - - @Nested - @DisplayName("Positive scenarios") - class SubmitMessagesPositiveScenarios { - - @BeforeAll - static void beforeAll(@NonNull final TestLifecycle lifecycle) { - lifecycle.doAdhoc(associateFeeTokensAndSubmitter()); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with a fee of 1 HBAR") - // TOPIC_FEE_104 - final Stream messageSubmitToPublicTopicWithFee1Hbar() { - final var collector = "collector"; - final var submitter = "submitter"; - return hapiTest( - cryptoCreate(collector).balance(0L), - cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - approveTopicAllowance() - .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(submitter), - submitMessageTo(TOPIC).message("TEST").payingWith(submitter), - getAccountBalance(collector).hasTinyBars(ONE_HBAR)); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with a fee of 1 FT") - // TOPIC_FEE_105 - final Stream messageSubmitToPublicTopicWithFee1token() { - final var collector = "collector"; - return hapiTest( - cryptoCreate(collector), - tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, BASE_TOKEN, TOPIC, 100, 1) - .payingWith(SUBMITTER), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), - getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with 3 layer fee") - // TOPIC_FEE_106 - final Stream messageSubmitToPublicTopicWith3layerFee() { - final var topicFeeCollector = "collector"; - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var tokenFeeCollector = COLLECTOR_PREFIX + token; - return hapiTest(flattened( - // create denomination token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - // create topic with multilayer fee - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) - .payingWith(SUBMITTER), - // submit message - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), - // assert topic fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), - // assert token fee collector balance - getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 1).hasTinyBars(ONE_HBAR))); - } - - @HapiTest - @DisplayName("MessageSubmit to a public topic with 10 different 3 layer fees") - // TOPIC_FEE_108 - final Stream messageSubmitToPublicTopicWith10different2layerFees() { - return hapiTest(flattened( - // create 9 denomination tokens and transfer them to the submitter - createMultipleTokensWith2LayerFees(SUBMITTER, 9), - // create 9 collectors and associate them with tokens - associateAllTokensToCollectors(), - // create topic with 10 multilayer fees - 9 HTS + 1 HBAR - createTopicWith10Different2layerFees(), - approveTopicAllowanceForAllFees(), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), - // assert topic fee collector balance - assertAllCollectorsBalances())); - } - - // TOPIC_FEE_108 - private SpecOperation[] associateAllTokensToCollectors() { - final var collectorName = "collector_"; - final var associateTokensToCollectors = new ArrayList(); - for (int i = 0; i < 9; i++) { - associateTokensToCollectors.add( - cryptoCreate(collectorName + i).balance(0L)); - associateTokensToCollectors.add(tokenAssociate(collectorName + i, TOKEN_PREFIX + i)); - } - return associateTokensToCollectors.toArray(SpecOperation[]::new); - } - // TOPIC_FEE_108 - private SpecOperation createTopicWith10Different2layerFees() { - final var collectorName = "collector_"; - final var topicCreateOp = createTopic(TOPIC); - for (int i = 0; i < 9; i++) { - topicCreateOp.withConsensusCustomFee(fixedConsensusHtsFee(1, TOKEN_PREFIX + i, collectorName + i)); - } - // add one hbar custom fee - topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); - return topicCreateOp; - } - // TOPIC_FEE_108 - private SpecOperation approveTopicAllowanceForAllFees() { - final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); - - for (int i = 0; i < 9; i++) { - approveAllowance.addTokenAllowance(SUBMITTER, TOKEN_PREFIX + i, TOPIC, 100, 1); - } - approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); - return approveAllowance; - } - // TOPIC_FEE_108 - private SpecOperation[] assertAllCollectorsBalances() { - final var collectorName = "collector_"; - final var assertBalances = new ArrayList(); - // assert token balances - for (int i = 0; i < 9; i++) { - assertBalances.add(getAccountBalance(collectorName + i).hasTokenBalance(TOKEN_PREFIX + i, 1)); - } - // add assert for hbar - assertBalances.add(getAccountBalance(collectorName + 0).hasTinyBars(ONE_HBAR)); - return assertBalances.toArray(SpecOperation[]::new); - } - - @HapiTest - @DisplayName("Treasury submit to a public topic with 3 layer fees") - // TOPIC_FEE_109 - final Stream treasurySubmitToPublicTopicWith3layerFees() { - final var topicFeeCollector = "collector"; - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var tokenFeeCollector = COLLECTOR_PREFIX + token; - - return hapiTest(flattened( - // create denomination token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - // create topic with multilayer fee - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) - .payingWith(SUBMITTER), - // submit message - submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), - // assert topic fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), - // assert token fee collector balance - getAccountBalance(tokenFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); - } - - - @HapiTest - @DisplayName("Treasury second layer submit to a public topic with 3 layer fees") - // TOPIC_FEE_110 - final Stream treasuryOfSecondLayerSubmitToPublic() { - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var topicFeeCollector = "topicFeeCollector"; - - return hapiTest(flattened( - // create token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - - // give one token to denomToken treasury to be able to pay the fee - tokenAssociate(DENOM_TREASURY, token), - cryptoTransfer(moving(1, token).between(SUBMITTER, DENOM_TREASURY)), - - // create topic - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(DENOM_TREASURY, token, TOPIC, 100, 1) - .payingWith(DENOM_TREASURY), - - // submit - submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), - - // assert topic fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), - // assert token fee collector balance - getAccountBalance(topicFeeCollector).hasTokenBalance(denomToken, 0).hasTinyBars(0))); - } - - - @HapiTest - @DisplayName("Collector submit to a public topic with 3 layer fees") - // TOPIC_FEE_111 - final Stream collectorSubmitToPublicTopicWith3layerFees() { - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var topicFeeCollector = "topicFeeCollector"; - - return hapiTest(flattened( - // create token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - - // transfer one token to the collector, to be able to pay the fee - cryptoCreate(topicFeeCollector).balance(ONE_HBAR), - tokenAssociate(topicFeeCollector, token), - - // create topic - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(topicFeeCollector, token, TOPIC, 100, 1) - .payingWith(topicFeeCollector), - - // submit - submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), - - // assert balances - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), - getAccountBalance(COLLECTOR_PREFIX+token).hasTokenBalance(denomToken, 0))); - } - - - @HapiTest - @DisplayName("Collector of second layer submit to a public topic with 3 layer fees") - // TOPIC_FEE_112 - final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFees() { - final var token = "token"; - final var denomToken = DENOM_TOKEN_PREFIX + token; - final var secondLayerFeeCollector = COLLECTOR_PREFIX + token; - final var topicFeeCollector = "topicFeeCollector"; - - return hapiTest(flattened( - // create token and transfer it to the submitter - createTokenWith2LayerFee(SUBMITTER, token, true), - - // give one token to denomToken treasury to be able to pay the fee - tokenAssociate(secondLayerFeeCollector, token), - cryptoTransfer(moving(1, token).between(SUBMITTER, secondLayerFeeCollector)), - - // create topic - cryptoCreate(topicFeeCollector).balance(0L), - tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(secondLayerFeeCollector, token, TOPIC, 100, 1) - .payingWith(secondLayerFeeCollector), - - // submit - submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), - - // assert topic fee collector balance - only first layer fee should be paid - getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), - // token fee collector should have 1 token from the first transfer and 0 from msg submit - getAccountBalance(secondLayerFeeCollector).hasTokenBalance(denomToken, 1))); - } - - @HapiTest - @DisplayName("Another collector submit message to a topic with a fee") - // TOPIC_FEE_113 - final Stream anotherCollectorSubmitMessageToATopicWithAFee() { - final var collector = "collector"; - final var anotherToken = "anotherToken"; - final var anotherCollector = COLLECTOR_PREFIX + anotherToken; - return hapiTest(flattened( - // create another token with fixed fee - createTokenWith2LayerFee(SUBMITTER, anotherToken, true), - tokenAssociate(anotherCollector, BASE_TOKEN), - cryptoTransfer( - moving(100, BASE_TOKEN).between(SUBMITTER, anotherCollector), - TokenMovement.movingHbar(ONE_HBAR).between(SUBMITTER, anotherCollector) - ), - // create topic - cryptoCreate(collector), - tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - // add allowance and submit with another collector - approveTopicAllowance() - .addTokenAllowance(anotherCollector, BASE_TOKEN, TOPIC, 100, 1) - .payingWith(anotherCollector), - submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), - // the fee was paid - getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1) - )); - } - - - } - } - @Nested @DisplayName("Topic approve allowance") class TopicApproveAllowance { From 44ff6227576ee4b9d8254518d345777b9d5a3e8a Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 8 Oct 2024 17:17:36 +0300 Subject: [PATCH 48/94] Add hapi tests Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 10 +- .../TopicCustomFeeSubmitMessageTest.java | 121 +++++++++++++++++- 2 files changed, 124 insertions(+), 7 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 780429656b26..2f04d96af33c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -53,6 +53,7 @@ public class TopicCustomFeeBase { protected static final String TOKEN_TREASURY = "tokenTreasury"; protected static final String DENOM_TREASURY = "denomTreasury"; protected static final String BASE_TOKEN = "baseToken"; + protected static final String SECOND_TOKEN = "secondToken"; /* tokens with multilayer fees */ protected static final String TOKEN_PREFIX = "token_"; @@ -92,8 +93,13 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { .tokenType(TokenType.FUNGIBLE_COMMON) .treasury(TOKEN_TREASURY) .initialSupply(500L), - tokenAssociate(SUBMITTER, BASE_TOKEN), - cryptoTransfer(moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER)) + tokenCreate(SECOND_TOKEN) + .treasury(TOKEN_TREASURY) + .initialSupply(500L), + tokenAssociate(SUBMITTER, BASE_TOKEN, SECOND_TOKEN), + cryptoTransfer( + moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER), + moving(500L, SECOND_TOKEN).between(TOKEN_TREASURY, SUBMITTER)), }; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index ad4193f30a9a..c483e2c20916 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -18,6 +18,8 @@ import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; @@ -27,6 +29,10 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHollow; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.flattened; @@ -146,15 +152,13 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { // TOPIC_FEE_104 final Stream messageSubmitToPublicTopicWithFee1Hbar() { final var collector = "collector"; - final var submitter = "submitter"; return hapiTest( cryptoCreate(collector).balance(0L), - cryptoCreate(submitter).balance(ONE_HUNDRED_HBARS), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), approveTopicAllowance() - .addCryptoAllowance(submitter, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(submitter), - submitMessageTo(TOPIC).message("TEST").payingWith(submitter), + .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTinyBars(ONE_HBAR)); } @@ -425,5 +429,112 @@ final Stream anotherCollectorSubmitMessageToATopicWithAFee() { // the fee was paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1))); } + + @HapiTest + @DisplayName("MessageSubmit to a topic with hollow account as fee collector") + // TOPIC_FEE_116 + final Stream messageTopicSubmitToHollowAccountAsFeeCollector() { + final var collector = "collector"; + return hapiTest( + // create hollow account with ONE_HUNDRED_HBARS + createHollow(1, i -> collector), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + + // collector should be still a hollow account + // and should have the initial balance + ONE_HBAR fee + getAccountInfo(collector).isHollow(), + getAccountBalance(collector).hasTinyBars(ONE_HUNDRED_HBARS + ONE_HBAR)); + } + + @HapiTest + @DisplayName("MessageSubmit and signs with the topic’s feeScheduleKey which is listed in the FEKL list") + // TOPIC_FEE_124 + final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { + final var collector = "collector"; + final var feeScheduleKey = "feeScheduleKey"; + return hapiTest( + newKeyNamed(feeScheduleKey), + cryptoCreate(collector).balance(0L), + createTopic(TOPIC) + .feeScheduleKeyName(feeScheduleKey) + .feeExemptKeys(feeScheduleKey) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").signedByPayerAnd(feeScheduleKey), + getAccountBalance(collector).hasTinyBars(0L)); + + } + + @HapiTest + @DisplayName("Collector submits a message to a topic with fee of 1 FT.") + final Stream collectorSubmitMessageToTopicWithFTFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + approveTopicAllowance() + .addTokenAllowance(collector, BASE_TOKEN,TOPIC, 1, 1) + .payingWith(collector), + submitMessageTo(TOPIC).message("TEST").payingWith(collector), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); + } + + @HapiTest + @DisplayName("Collector submits a message to a topic with fee of 1 HBAR.") + final Stream collectorSubmitMessageToTopicWithHbarFee() { + final var collector = "collector"; + return hapiTest( + cryptoCreate(collector).balance(ONE_HBAR), + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + approveTopicAllowance() + .addCryptoAllowance(collector, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + .payingWith(collector), + submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit"), + // assert collector's tinyBars balance + withOpContext((spec, log) -> { + final var transactionRecord = getTxnRecord("submit"); + allRunFor(spec, transactionRecord); + final var transactionFee = transactionRecord.getResponseRecord().getTransactionFee(); + // todo When we add fee for approveAllowance transaction we must take it into account here!! + getAccountBalance(collector).hasTinyBars(ONE_HBAR - transactionFee); + })); + } + + @HapiTest + @DisplayName("Collector submits a message to a topic with 2 different FT fees.") + final Stream collectorSubmitMessageToTopicWith2differentFees() { + final var collector = "collector"; + final var secondCollector = "secondCollector"; + return hapiTest( + // todo create and associate collector in beforeAll() + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN, SECOND_TOKEN), + // create second collector and send second token + cryptoCreate(secondCollector).balance(ONE_HBAR), + tokenAssociate(secondCollector, SECOND_TOKEN), + cryptoTransfer(moving(1, SECOND_TOKEN).between(SUBMITTER, collector)), + // create topic with two fees + createTopic(TOPIC) + .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)) + .withConsensusCustomFee(fixedConsensusHtsFee(1, SECOND_TOKEN, secondCollector)), + approveTopicAllowance() + .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) + .addTokenAllowance(collector, SECOND_TOKEN, TOPIC, 1, 1) + .payingWith(collector), + submitMessageTo(TOPIC).message("TEST").payingWith(collector), + // only second fee should be paid + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), + getAccountBalance(secondCollector).hasTokenBalance(SECOND_TOKEN, 1L)); + } + } } From 3ef8877779787ec2575cfe3dac3127af1b1ac7b3 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 10 Oct 2024 09:41:03 +0300 Subject: [PATCH 49/94] Fix tests Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 4 +-- .../TopicCustomFeeSubmitMessageTest.java | 29 ++++++++++--------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 2f04d96af33c..8bf8e805ccfb 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -93,9 +93,7 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { .tokenType(TokenType.FUNGIBLE_COMMON) .treasury(TOKEN_TREASURY) .initialSupply(500L), - tokenCreate(SECOND_TOKEN) - .treasury(TOKEN_TREASURY) - .initialSupply(500L), + tokenCreate(SECOND_TOKEN).treasury(TOKEN_TREASURY).initialSupply(500L), tokenAssociate(SUBMITTER, BASE_TOKEN, SECOND_TOKEN), cryptoTransfer( moving(500L, BASE_TOKEN).between(TOKEN_TREASURY, SUBMITTER), diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index c483e2c20916..ed0e095076c1 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -468,20 +468,19 @@ final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { .payingWith(SUBMITTER), submitMessageTo(TOPIC).message("TEST").signedByPayerAnd(feeScheduleKey), getAccountBalance(collector).hasTinyBars(0L)); - } @HapiTest @DisplayName("Collector submits a message to a topic with fee of 1 FT.") + // TOPIC_FEE_125 final Stream collectorSubmitMessageToTopicWithFTFee() { final var collector = "collector"; return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), approveTopicAllowance() - .addTokenAllowance(collector, BASE_TOKEN,TOPIC, 1, 1) + .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) .payingWith(collector), submitMessageTo(TOPIC).message("TEST").payingWith(collector), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); @@ -489,23 +488,28 @@ final Stream collectorSubmitMessageToTopicWithFTFee() { @HapiTest @DisplayName("Collector submits a message to a topic with fee of 1 HBAR.") + // TOPIC_FEE_126 final Stream collectorSubmitMessageToTopicWithHbarFee() { final var collector = "collector"; return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), + createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), approveTopicAllowance() .addCryptoAllowance(collector, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(collector), + .payingWith(collector) + .via("approveAllowance"), submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit"), // assert collector's tinyBars balance withOpContext((spec, log) -> { - final var transactionRecord = getTxnRecord("submit"); - allRunFor(spec, transactionRecord); - final var transactionFee = transactionRecord.getResponseRecord().getTransactionFee(); - // todo When we add fee for approveAllowance transaction we must take it into account here!! - getAccountBalance(collector).hasTinyBars(ONE_HBAR - transactionFee); + final var submitTxnRecord = getTxnRecord("submit"); + final var allowanceTxnRecord = getTxnRecord("approveAllowance"); + allRunFor(spec, submitTxnRecord, allowanceTxnRecord); + final var transactionTxnFee = + submitTxnRecord.getResponseRecord().getTransactionFee(); + final var allowanceTxnFee = + allowanceTxnRecord.getResponseRecord().getTransactionFee(); + getAccountBalance(collector) + .hasTinyBars(ONE_HUNDRED_HBARS - transactionTxnFee - allowanceTxnFee); })); } @@ -535,6 +539,5 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), getAccountBalance(secondCollector).hasTokenBalance(SECOND_TOKEN, 1L)); } - } } From f1de34a7dbcb83cc9713e46542e9508894145450 Mon Sep 17 00:00:00 2001 From: JivkoKelchev Date: Thu, 3 Oct 2024 09:07:09 +0300 Subject: [PATCH 50/94] feat: Add custom fees to ConsensusCreateTopicHandler (#15402) Signed-off-by: ibankov Signed-off-by: Kim Rader Signed-off-by: Zhivko Kelchev Co-authored-by: ibankov Co-authored-by: Kim Rader --- .../services/response_code.proto | 4 +- .../bdd/suites/hip991/TopicCustomFeeTest.java | 41 ++++++++++++++----- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 0b8890bcf1b0..3ca2ad54ce3a 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1608,7 +1608,7 @@ enum ResponseCodeEnum { * The provided fee exempt key list contains an invalid key. */ INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST = 372; - + /** * The provided fee schedule key contains an invalid key. */ @@ -1619,5 +1619,5 @@ enum ResponseCodeEnum { * we cannot add it on update. */ FEE_SCHEDULE_KEY_CANNOT_BE_UPDATED = 375; - + } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 18e6803be8d4..0ae54c690d58 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -34,12 +34,13 @@ import static com.hedera.services.bdd.suites.HapiSuite.flattened; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import com.hedera.services.bdd.junit.HapiTest; import com.hedera.services.bdd.junit.HapiTestLifecycle; import com.hedera.services.bdd.junit.support.TestLifecycle; +import com.hedera.services.bdd.spec.keys.KeyShape; import com.hederahashgraph.api.proto.java.TokenType; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.List; @@ -99,9 +100,29 @@ final Stream createTopicWithOnlyFeeScheduleKey() { getTopicInfo(TOPIC).hasFeeScheduleKey(FEE_SCHEDULE_KEY)); } + @HapiTest + @DisplayName("Create topic with ECDSA feeScheduleKey") + // TOPIC_FEE_005 + final Stream createTopicWithECDSAFeeScheduleKey() { + return hapiTest( + createTopic(TOPIC).feeScheduleKeyName(FEE_SCHEDULE_KEY_ECDSA), + getTopicInfo(TOPIC).hasFeeScheduleKey(FEE_SCHEDULE_KEY_ECDSA)); + } + + @HapiTest + @DisplayName("Create topic with threshold feeScheduleKey") + // TOPIC_FEE_006 + final Stream createTopicWithThresholdFeeScheduleKey() { + final var threshKey = "threshKey"; + return hapiTest( + newKeyNamed(threshKey).shape(KeyShape.threshOf(1, KeyShape.SIMPLE, KeyShape.SIMPLE)), + createTopic(TOPIC).feeScheduleKeyName(threshKey), + getTopicInfo(TOPIC).hasFeeScheduleKey(threshKey)); + } + @HapiTest @DisplayName("Create topic with 1 Hbar fixed fee") - // TOPIC_FEE_004 + // TOPIC_FEE_008 final Stream createTopicWithOneHbarFixedFee() { final var collector = "collector"; return hapiTest( @@ -120,7 +141,7 @@ final Stream createTopicWithOneHbarFixedFee() { @HapiTest @DisplayName("Create topic with 1 HTS fixed fee") - // TOPIC_FEE_005 + // TOPIC_FEE_009 final Stream createTopicWithOneHTSFixedFee() { final var collector = "collector"; return hapiTest( @@ -143,7 +164,7 @@ final Stream createTopicWithOneHTSFixedFee() { @HapiTest @DisplayName("Create topic with 10 keys in FEKL") - // TOPIC_FEE_020 + // TOPIC_FEE_022 final Stream createTopicWithFEKL() { final var collector = "collector"; return hapiTest(flattened( @@ -177,7 +198,7 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { @HapiTest @DisplayName("Create topic with duplicated signatures in FEKL") - // TOPIC_FEE_023 + // TOPIC_FEE_029 final Stream createTopicWithDuplicateSignatures() { final var testKey = "testKey"; return hapiTest(flattened( @@ -187,12 +208,12 @@ final Stream createTopicWithDuplicateSignatures() { .submitKeyName(SUBMIT_KEY) .feeScheduleKeyName(FEE_SCHEDULE_KEY) .feeExemptKeys(testKey, testKey) - .hasPrecheck(FEKL_CONTAINS_DUPLICATED_KEYS))); + .hasPrecheck(FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS))); } @HapiTest @DisplayName("Create topic with 0 Hbar fixed fee") - // TOPIC_FEE_024 + // TOPIC_FEE_030 final Stream createTopicWithZeroHbarFixedFee() { final var collector = "collector"; return hapiTest( @@ -207,7 +228,7 @@ final Stream createTopicWithZeroHbarFixedFee() { @HapiTest @DisplayName("Create topic with 0 HTS fixed fee") - // TOPIC_FEE_025 + // TOPIC_FEE_031 final Stream createTopicWithZeroHTSFixedFee() { final var collector = "collector"; return hapiTest( @@ -226,7 +247,7 @@ final Stream createTopicWithZeroHTSFixedFee() { @HapiTest @DisplayName("Create topic with invalid fee schedule key") - // TOPIC_FEE_026 + // TOPIC_FEE_032 final Stream createTopicWithInvalidFeeScheduleKey() { final var invalidKey = "invalidKey"; return hapiTest( @@ -240,7 +261,7 @@ final Stream createTopicWithInvalidFeeScheduleKey() { @HapiTest @DisplayName("Create topic with custom fee and deleted collector") - // TOPIC_FEE_028 + // TOPIC_FEE_033 final Stream createTopicWithCustomFeeAndDeletedCollector() { final var collector = "collector"; return hapiTest( From becfa7e6184a6750cb6e4ad55f8b1db496d7f84b Mon Sep 17 00:00:00 2001 From: Valentin Tronkov Date: Thu, 3 Oct 2024 10:44:47 +0300 Subject: [PATCH 51/94] feat: Update ConsensusUpdateTopicHandler with new custom fee functionality (#15539) Signed-off-by: ibankov Signed-off-by: Kim Rader Signed-off-by: Zhivko Kelchev Signed-off-by: Valentin Tronkov <99957253+vtronkov@users.noreply.github.com> Co-authored-by: ibankov Co-authored-by: Kim Rader Co-authored-by: Zhivko Kelchev --- .../hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 0ae54c690d58..80e33f8dfc12 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -136,7 +136,7 @@ final Stream createTopicWithOneHbarFixedFee() { .hasAdminKey(ADMIN_KEY) .hasSubmitKey(SUBMIT_KEY) .hasFeeScheduleKey(FEE_SCHEDULE_KEY) - .hasCustom(expectedConsensusFixedHbarFee(ONE_HBAR, collector))); + .hasCustomFee(expectedConsensusFixedHbarFee(ONE_HBAR, collector))); } @HapiTest @@ -159,7 +159,7 @@ final Stream createTopicWithOneHTSFixedFee() { .hasAdminKey(ADMIN_KEY) .hasSubmitKey(SUBMIT_KEY) .hasFeeScheduleKey(FEE_SCHEDULE_KEY) - .hasCustom(expectedConsensusFixedHTSFee(1, "testToken", collector))); + .hasCustomFee(expectedConsensusFixedHTSFee(1, "testToken", collector))); } @HapiTest From cc1d011d1fc9acc5b809c086b003b0b0b332bd16 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 5 Dec 2024 13:31:08 +0200 Subject: [PATCH 52/94] fix merge conflict Signed-off-by: Zhivko Kelchev --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 8bf8e805ccfb..e33d7c63f197 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -101,6 +101,18 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { }; } + protected static SpecOperation[] setupBaseForUpdate() { + return new SpecOperation[]{ + newKeyNamed(ADMIN_KEY), + newKeyNamed(SUBMIT_KEY), + newKeyNamed(FEE_SCHEDULE_KEY), + newKeyNamed(FEE_SCHEDULE_KEY2), + cryptoCreate(COLLECTOR), + tokenCreate(TOKEN).tokenType(TokenType.FUNGIBLE_COMMON).initialSupply(500), + tokenAssociate(COLLECTOR, TOKEN) + }; + } + /** * Create and transfer multiple tokens with 2 layer custom fees to given account. * @@ -128,8 +140,7 @@ protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int n * @param createTreasury * @return */ - protected static List createTokenWith2LayerFee( - String owner, String tokenName, boolean createTreasury) { + protected static List createTokenWith2LayerFee(String owner, String tokenName, boolean createTreasury) { final var specOperations = new ArrayList(); final var collectorName = COLLECTOR_PREFIX + tokenName; final var denomToken = DENOM_TOKEN_PREFIX + tokenName; From 7483da7f067a289302421fcc9fa0bb2633916279 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 5 Dec 2024 14:56:59 +0200 Subject: [PATCH 53/94] Remove allowances Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 26 +- .../dispatcher/TransactionDispatcher.java | 1 - .../dispatcher/TransactionHandlers.java | 2 - .../handle/HandleWorkflowModule.java | 1 - .../handle/HandleWorkflowModuleTest.java | 5 - .../hedera/node/config/data/TopicsConfig.java | 2 +- .../ConsensusApproveAllowanceHandler.java | 128 --------- .../handlers/ConsensusCreateTopicHandler.java | 18 +- .../impl/handlers/ConsensusHandlers.java | 16 +- .../ConsensusSubmitMessageHandler.java | 2 - .../customfee/ConsensusAllowanceUpdater.java | 270 ------------------ .../customfee/ConsensusCustomFeeAssessor.java | 37 +-- .../ConsensusAllowancesValidator.java | 199 ------------- .../ConsensusApproveAllowanceTest.java | 220 -------------- .../test/handlers/ConsensusHandlersTest.java | 14 +- .../ConsensusSubmitMessageHandlerTest.java | 5 +- .../ConsensusAllowancesValidatorTest.java | 239 ---------------- .../bdd/suites/hip991/TopicCustomFeeBase.java | 12 - .../TopicCustomFeeSubmitMessageTest.java | 97 ++----- 19 files changed, 50 insertions(+), 1244 deletions(-) delete mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java delete mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java delete mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java delete mode 100644 hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java delete mode 100644 hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 4ee45930b463..874363630958 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1597,17 +1597,15 @@ enum ResponseCodeEnum { /** * The provided fee exempt key list size exceeded the limit. */ - MAX_ENTRIES_FOR_FEKL_EXCEEDED = 370; - + MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED = 370; /** * The provided fee exempt key list contains duplicated keys. */ - FEKL_CONTAINS_DUPLICATED_KEYS = 371; - + FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS = 371; /** * The provided fee exempt key list contains an invalid key. */ - INVALID_KEY_IN_FEKL = 372; + INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST = 372; /** * Custom fee list is missing. @@ -1617,22 +1615,18 @@ enum ResponseCodeEnum { MISSING_CUSTOM_FEES = 373; /** - * Allowance per message is higher than total allowance. - */ - ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE = 374; + * The provided fee schedule key contains an invalid key. + */ + INVALID_FEE_SCHEDULE_KEY = 374; /** - * Repeated AccountId/TopicId pair in the transaction body. - */ - REPEATED_ALLOWANCE_IN_TRANSACTION_BODY = 375; + * If a fee schedule key is not set when we create a topic + * we cannot add it on update. + */ + FEE_SCHEDULE_KEY_CANNOT_BE_UPDATED = 375; /** * The topic has been marked as deleted. */ TOPIC_DELETED = 376; - - /** - * The account receiving allowance is a smart contract. - */ - ACCOUNT_IS_CONTRACT = 377; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java index d7b3f06200ff..98fc4abe168d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java @@ -155,7 +155,6 @@ private TransactionHandler getHandler(@NonNull final TransactionBody txBody) { case CONSENSUS_UPDATE_TOPIC -> handlers.consensusUpdateTopicHandler(); case CONSENSUS_DELETE_TOPIC -> handlers.consensusDeleteTopicHandler(); case CONSENSUS_SUBMIT_MESSAGE -> handlers.consensusSubmitMessageHandler(); - case CONSENSUS_APPROVE_ALLOWANCE -> handlers.consensusApproveAllowanceHandler(); case CONTRACT_CREATE_INSTANCE -> handlers.contractCreateHandler(); case CONTRACT_UPDATE_INSTANCE -> handlers.contractUpdateHandler(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java index b0ac8efa8100..d361c5838d8c 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionHandlers.java @@ -19,7 +19,6 @@ import com.hedera.node.app.service.addressbook.impl.handlers.NodeCreateHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeDeleteHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeUpdateHandler; -import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusDeleteTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusSubmitMessageHandler; @@ -82,7 +81,6 @@ public record TransactionHandlers( @NonNull ConsensusUpdateTopicHandler consensusUpdateTopicHandler, @NonNull ConsensusDeleteTopicHandler consensusDeleteTopicHandler, @NonNull ConsensusSubmitMessageHandler consensusSubmitMessageHandler, - @NonNull ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler, @NonNull ContractCreateHandler contractCreateHandler, @NonNull ContractUpdateHandler contractUpdateHandler, @NonNull ContractCallHandler contractCallHandler, diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java index d430b02a29f6..a1b3cbad6c06 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/HandleWorkflowModule.java @@ -95,7 +95,6 @@ static TransactionHandlers provideTransactionHandlers( consensusHandlers.consensusUpdateTopicHandler(), consensusHandlers.consensusDeleteTopicHandler(), consensusHandlers.consensusSubmitMessageHandler(), - consensusHandlers.consensusApproveAllowanceHandler(), contractHandlers.get().contractCreateHandler(), contractHandlers.get().contractUpdateHandler(), contractHandlers.get().contractCallHandler(), diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java index 410337918c72..1b2191338f68 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/HandleWorkflowModuleTest.java @@ -23,7 +23,6 @@ import com.hedera.node.app.service.addressbook.impl.handlers.NodeCreateHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeDeleteHandler; import com.hedera.node.app.service.addressbook.impl.handlers.NodeUpdateHandler; -import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusDeleteTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusHandlers; @@ -121,9 +120,6 @@ class HandleWorkflowModuleTest { @Mock private ConsensusSubmitMessageHandler consensusSubmitMessageHandler; - @Mock - private ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler; - @Mock private ContractCreateHandler contractCreateHandler; @@ -265,7 +261,6 @@ void usesComponentsToGetHandlers() { given(consensusHandlers.consensusUpdateTopicHandler()).willReturn(consensusUpdateTopicHandler); given(consensusHandlers.consensusDeleteTopicHandler()).willReturn(consensusDeleteTopicHandler); given(consensusHandlers.consensusSubmitMessageHandler()).willReturn(consensusSubmitMessageHandler); - given(consensusHandlers.consensusApproveAllowanceHandler()).willReturn(consensusApproveAllowanceHandler); given(contractHandlers.contractCreateHandler()).willReturn(contractCreateHandler); given(contractHandlers.contractUpdateHandler()).willReturn(contractUpdateHandler); given(contractHandlers.contractCallHandler()).willReturn(contractCallHandler); diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java index f5dd1e8875c7..7d303119801e 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java @@ -23,5 +23,5 @@ @ConfigData("topics") public record TopicsConfig( @ConfigProperty(defaultValue = "1000000") @NetworkProperty long maxNumber, - @ConfigProperty(defaultValue = "10") @NetworkProperty int maxCustoFeeEntriesForTopics, + @ConfigProperty(defaultValue = "10") @NetworkProperty int maxCustomFeeEntriesForTopics, @ConfigProperty(defaultValue = "10") @NetworkProperty int maxEntriesForFeeExemptKeyList) {} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java deleted file mode 100644 index 1f63b1d9e303..000000000000 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusApproveAllowanceHandler.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.consensus.impl.handlers; - -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID; -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.transaction.TransactionBody; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; -import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusAllowanceUpdater; -import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; -import com.hedera.node.app.spi.fees.FeeContext; -import com.hedera.node.app.spi.fees.Fees; -import com.hedera.node.app.spi.workflows.HandleContext; -import com.hedera.node.app.spi.workflows.HandleException; -import com.hedera.node.app.spi.workflows.PreCheckException; -import com.hedera.node.app.spi.workflows.PreHandleContext; -import com.hedera.node.app.spi.workflows.TransactionHandler; -import edu.umd.cs.findbugs.annotations.NonNull; -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * This class contains all workflow-related functionality regarding {@link HederaFunctionality#CONSENSUS_APPROVE_ALLOWANCE}. - */ -@Singleton -public class ConsensusApproveAllowanceHandler implements TransactionHandler { - private final ConsensusAllowancesValidator validator; - private final ConsensusAllowanceUpdater updater; - - /** - * Default constructor for injection. - * @param allowancesValidator allowances validator - */ - @Inject - public ConsensusApproveAllowanceHandler( - @NonNull final ConsensusAllowancesValidator allowancesValidator, - @NonNull final ConsensusAllowanceUpdater allowanceUpdater) { - requireNonNull(allowancesValidator); - this.validator = allowancesValidator; - this.updater = allowanceUpdater; - } - - @Override - public void preHandle(@NonNull PreHandleContext context) throws PreCheckException { - requireNonNull(context); - final var txn = context.body(); - final var payerId = context.payer(); - final var op = txn.consensusApproveAllowanceOrThrow(); - - for (final var allowance : op.consensusCryptoFeeScheduleAllowances()) { - final var owner = allowance.owner(); - if (owner != null && !owner.equals(payerId)) { - context.requireKeyOrThrow(owner, INVALID_ALLOWANCE_OWNER_ID); - } - } - - for (final var allowance : op.consensusTokenFeeScheduleAllowances()) { - final var owner = allowance.owner(); - if (owner != null && !owner.equals(payerId)) { - context.requireKeyOrThrow(owner, INVALID_ALLOWANCE_OWNER_ID); - } - } - } - - @Override - public void pureChecks(@NonNull TransactionBody txn) throws PreCheckException { - requireNonNull(txn); - final var op = txn.consensusApproveAllowanceOrThrow(); - validator.pureChecks(op); - } - - @Override - public void handle(@NonNull HandleContext handleContext) throws HandleException { - requireNonNull(handleContext, "The argument 'context' must not be null"); - - final var topicStore = handleContext.storeFactory().writableStore(WritableTopicStore.class); - final var op = handleContext.body().consensusApproveAllowanceOrThrow(); - - validator.validateSemantics(handleContext, op, topicStore); - - // Apply all changes to the state modifications. We need to look up payer for each modification, since payer - // would have been modified by a previous allowance change - approveAllowance(handleContext, topicStore); - } - - @NonNull - @Override - public Fees calculateFees(@NonNull final FeeContext feeContext) { - requireNonNull(feeContext); - // TODO: Implement this method - return Fees.FREE; - } - - /** - * Apply all changes to the state modifications for crypto and token allowances. - * @param context the handle context - * @param topicStore the topic store - * @throws HandleException if there is an error applying the changes - */ - private void approveAllowance(@NonNull final HandleContext context, @NonNull final WritableTopicStore topicStore) { - requireNonNull(context); - requireNonNull(topicStore); - - final var op = context.body().consensusApproveAllowanceOrThrow(); - final var cryptoAllowances = op.consensusCryptoFeeScheduleAllowances(); - final var tokenAllowances = op.consensusTokenFeeScheduleAllowances(); - - /* --- Apply changes to state --- */ - updater.applyCryptoAllowances(cryptoAllowances, topicStore); - updater.applyFungibleTokenAllowances(tokenAllowances, topicStore); - } -} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index a6ef057d7971..4a1482e0e134 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -20,13 +20,13 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTORENEW_DURATION_NOT_IN_RANGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.BAD_ENCODING; import static com.hedera.hapi.node.base.ResponseCodeEnum.CUSTOM_FEES_LIST_TOO_LONG; -import static com.hedera.hapi.node.base.ResponseCodeEnum.FEKL_CONTAINS_DUPLICATED_KEYS; +import static com.hedera.hapi.node.base.ResponseCodeEnum.FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_EXPIRATION_TIME; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FEKL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; -import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FEKL_EXCEEDED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED; import static com.hedera.hapi.node.base.ResponseCodeEnum.MISSING_CUSTOM_FEES; import static com.hedera.node.app.hapi.utils.fee.ConsensusServiceFeeBuilder.getConsensusCreateTopicFee; import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.RUNNING_HASH_BYTE_ARRAY_SIZE; @@ -87,7 +87,8 @@ public ConsensusCreateTopicHandler(@NonNull final ConsensusCustomFeesValidator c public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { final var op = txn.consensusCreateTopicOrThrow(); final var uniqueKeysCount = op.feeExemptKeyList().stream().distinct().count(); - validateTruePreCheck(uniqueKeysCount == op.feeExemptKeyList().size(), FEKL_CONTAINS_DUPLICATED_KEYS); + validateTruePreCheck( + uniqueKeysCount == op.feeExemptKeyList().size(), FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS); } @Override @@ -205,18 +206,19 @@ private void validateSemantics( if (!op.feeExemptKeyList().isEmpty()) { validateTrue( op.feeExemptKeyList().size() <= topicConfig.maxEntriesForFeeExemptKeyList(), - MAX_ENTRIES_FOR_FEKL_EXCEEDED); + MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED); validateTrue(!op.customFees().isEmpty(), MISSING_CUSTOM_FEES); op.feeExemptKeyList() - .forEach(key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEKL)); + .forEach(key -> + handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST)); builder.feeExemptKeyList(op.feeExemptKeyList()); } // validate custom fees if (!op.customFees().isEmpty()) { validateTrue( - op.customFees().size() <= topicConfig.maxCustoFeeEntriesForTopics(), CUSTOM_FEES_LIST_TOO_LONG); - customFeesValidator.validateForCreation( + op.customFees().size() <= topicConfig.maxCustomFeeEntriesForTopics(), CUSTOM_FEES_LIST_TOO_LONG); + customFeesValidator.validate( accountStore, tokenRelStore, tokenStore, op.customFees(), handleContext.expiryValidator()); builder.customFees(op.customFees()); } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java index 87b130a70ea6..84cc13ab8897 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusHandlers.java @@ -37,8 +37,6 @@ public class ConsensusHandlers { private final ConsensusUpdateTopicHandler consensusUpdateTopicHandler; - private final ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler; - /** * Constructor for ConsensusHandlers. */ @@ -48,8 +46,7 @@ public ConsensusHandlers( @NonNull final ConsensusDeleteTopicHandler consensusDeleteTopicHandler, @NonNull final ConsensusGetTopicInfoHandler consensusGetTopicInfoHandler, @NonNull final ConsensusSubmitMessageHandler consensusSubmitMessageHandler, - @NonNull final ConsensusUpdateTopicHandler consensusUpdateTopicHandler, - @NonNull final ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler) { + @NonNull final ConsensusUpdateTopicHandler consensusUpdateTopicHandler) { this.consensusCreateTopicHandler = Objects.requireNonNull(consensusCreateTopicHandler, "consensusCreateTopicHandler must not be null"); this.consensusDeleteTopicHandler = @@ -60,8 +57,6 @@ public ConsensusHandlers( Objects.requireNonNull(consensusSubmitMessageHandler, "consensusSubmitMessageHandler must not be null"); this.consensusUpdateTopicHandler = Objects.requireNonNull(consensusUpdateTopicHandler, "consensusUpdateTopicHandler must not be null"); - this.consensusApproveAllowanceHandler = Objects.requireNonNull( - consensusApproveAllowanceHandler, "consensusApproveAllowanceHandler must not be null"); } /** @@ -108,13 +103,4 @@ public ConsensusSubmitMessageHandler consensusSubmitMessageHandler() { public ConsensusUpdateTopicHandler consensusUpdateTopicHandler() { return consensusUpdateTopicHandler; } - - /** - * Get the consensusApproveAllowanceHandler. - * - * @return the consensusApproveAllowanceHandler - */ - public ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler() { - return consensusApproveAllowanceHandler; - } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 6f6b714397e3..de11353b82ad 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -159,8 +159,6 @@ var record = handleContext.dispatchChildTransaction( HandleContext.TransactionCategory.CHILD, HandleContext.ConsensusThrottling.OFF); validateTrue(record.status().equals(SUCCESS), record.status()); - // update total allowances - customFeeAssessor.adjustAllowance(syntheticBody); } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java deleted file mode 100644 index f7b613d3584a..000000000000 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusAllowanceUpdater.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.consensus.impl.handlers.customfee; - -import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; -import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; -import com.hedera.hapi.node.base.TokenID; -import com.hedera.hapi.node.base.TopicID; -import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; -import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Singleton; - -// todo update this class when allowances are done!!! -@Singleton -public class ConsensusAllowanceUpdater { - - /** - * Constructs a {@link ConsensusAllowanceUpdater} instance. - */ - @Inject - public ConsensusAllowanceUpdater() { - // Needed for Dagger injection - } - - /** - * Applies all changes needed for Crypto allowances from the transaction. - * If the topic already has an allowance, the allowance value will be replaced with values - * from transaction. If the amount specified is 0, the allowance will be removed. - * @param topicCryptoAllowances the list of crypto allowances - * @param topicStore the topic store - */ - public void applyCryptoAllowances( - @NonNull final List topicCryptoAllowances, - @NonNull final WritableTopicStore topicStore) { - requireNonNull(topicCryptoAllowances); - requireNonNull(topicStore); - - for (final var allowance : topicCryptoAllowances) { - final var ownerId = allowance.owner(); - final var topicId = allowance.topicIdOrThrow(); - final var topic = getIfUsable(topicId, topicStore); - final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); - - final var amount = allowance.amount(); - final var amountPerMessage = allowance.amountPerMessage(); - - updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); - final var copy = - topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); - - topicStore.put(copy); - } - } - - public void applyCryptoAllowances( - @NonNull final TopicID topicId, - @NonNull final TopicCryptoAllowance allowance, - @NonNull final WritableTopicStore topicStore) { - requireNonNull(allowance); - requireNonNull(topicStore); - - final var ownerId = allowance.spenderId(); - final var topic = getIfUsable(topicId, topicStore); - final var mutableAllowances = new ArrayList<>(topic.cryptoAllowances()); - - final var amount = allowance.amount(); - final var amountPerMessage = allowance.amountPerMessage(); - - updateCryptoAllowance(mutableAllowances, amount, amountPerMessage, ownerId); - final var copy = topic.copyBuilder().cryptoAllowances(mutableAllowances).build(); - - topicStore.put(copy); - } - - /** - * Updates the crypto allowance amount if the allowance exists, otherwise adds a new allowance. - * If the amount is zero removes the allowance if it exists in the list. - * @param mutableAllowances the list of mutable allowances of owner - * @param amount the amount - * @param spenderId the spender id - */ - private void updateCryptoAllowance( - final List mutableAllowances, - final long amount, - final long amountPerMessage, - final AccountID spenderId) { - final var newAllowanceBuilder = TopicCryptoAllowance.newBuilder().spenderId(spenderId); - // get the index of the allowance with same spender in existing list - final var index = lookupSpender(mutableAllowances, spenderId); - // If given amount is zero, if the element exists remove it, otherwise do nothing - if (amount == 0) { - if (index != -1) { - // If amount is 0, remove the allowance - mutableAllowances.remove(index); - } - return; - } - if (index != -1) { - mutableAllowances.set( - index, - newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } else { - mutableAllowances.add(newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } - } - - /** - * Applies all changes needed for fungible token allowances from the transaction. If the key - * {token, spender} already has an allowance, the allowance value will be replaced with values - * from transaction. - * @param tokenAllowances the list of token allowances - * @param topicStore the topic store - */ - public void applyFungibleTokenAllowances( - @NonNull final List tokenAllowances, - @NonNull final WritableTopicStore topicStore) { - requireNonNull(tokenAllowances); - requireNonNull(topicStore); - - for (final var allowance : tokenAllowances) { - final var ownerId = allowance.owner(); - final var amount = allowance.amount(); - final var amountPerMessage = allowance.amountPerMessage(); - final var tokenId = allowance.tokenIdOrThrow(); - final var topicId = allowance.topicIdOrThrow(); - final var topic = getIfUsable(topicId, topicStore); - - final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); - - updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); - final var copy = - topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); - - topicStore.put(copy); - } - } - - /* - * - */ - public void applyFungibleTokenAllowances( - @NonNull final TopicID topicId, - @NonNull final TopicFungibleTokenAllowance allowance, - @NonNull final WritableTopicStore topicStore) { - requireNonNull(allowance); - requireNonNull(topicStore); - - final var ownerId = allowance.spenderIdOrThrow(); - final var amount = allowance.amount(); - final var amountPerMessage = allowance.amountPerMessage(); - final var tokenId = allowance.tokenIdOrThrow(); - final var topic = getIfUsable(topicId, topicStore); - - final var mutableTokenAllowances = new ArrayList<>(topic.tokenAllowances()); - - updateTokenAllowance(mutableTokenAllowances, amount, amountPerMessage, ownerId, tokenId); - final var copy = - topic.copyBuilder().tokenAllowances(mutableTokenAllowances).build(); - - topicStore.put(copy); - } - - /** - * Updates the token allowance amount if the allowance for given tokenNuma dn spenderNum exists, - * otherwise adds a new allowance. - * If the amount is zero removes the allowance if it exists in the list - * @param mutableAllowances the list of mutable allowances of owner - * @param amount the amount - * @param spenderId the spender number - * @param tokenId the token number - */ - private void updateTokenAllowance( - final List mutableAllowances, - final long amount, - final long amountPerMessage, - final AccountID spenderId, - final TokenID tokenId) { - final var newAllowanceBuilder = - TopicFungibleTokenAllowance.newBuilder().spenderId(spenderId).tokenId(tokenId); - final var index = lookupSpenderAndToken(mutableAllowances, spenderId, tokenId); - // If given amount is zero, if the element exists remove it - if (amount == 0) { - if (index != -1) { - mutableAllowances.remove(index); - } - return; - } - if (index != -1) { - mutableAllowances.set( - index, - newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } else { - mutableAllowances.add(newAllowanceBuilder - .amount(amount) - .amountPerMessage(amountPerMessage) - .build()); - } - } - - /** - * Returns the index of the allowance with the given spender in the list if it exists, - * otherwise returns -1. - * @param topicCryptoAllowances list of allowances - * @param spenderNum spender account number - * @return index of the allowance if it exists, otherwise -1 - */ - private int lookupSpender(final List topicCryptoAllowances, final AccountID spenderNum) { - for (int i = 0; i < topicCryptoAllowances.size(); i++) { - final var allowance = topicCryptoAllowances.get(i); - if (allowance.spenderIdOrThrow().equals(spenderNum)) { - return i; - } - } - return -1; - } - - /** - * Returns the index of the allowance with the given spender and token in the list if it exists, - * otherwise returns -1. - * @param topicTokenAllowances list of allowances - * @param spenderId spender account number - * @param tokenId token number - * @return index of the allowance if it exists, otherwise -1 - */ - private int lookupSpenderAndToken( - final List topicTokenAllowances, - final AccountID spenderId, - final TokenID tokenId) { - for (int i = 0; i < topicTokenAllowances.size(); i++) { - final var allowance = topicTokenAllowances.get(i); - if (allowance.spenderIdOrThrow().equals(spenderId) - && allowance.tokenIdOrThrow().equals(tokenId)) { - return i; - } - } - return -1; - } -} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 81af6e85628a..ac394d80b737 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -16,9 +16,6 @@ package com.hedera.node.app.service.consensus.impl.handlers.customfee; -import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE; -import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; -import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountAmount; @@ -50,15 +47,12 @@ @Singleton public class ConsensusCustomFeeAssessor { - private final ConsensusAllowanceUpdater allowanceUpdater; - /** * Constructs a {@link ConsensusCustomFeeAssessor} instance. */ @Inject - public ConsensusCustomFeeAssessor(@NonNull final ConsensusAllowanceUpdater allowanceUpdater) { + public ConsensusCustomFeeAssessor() { // Needed for Dagger injection - this.allowanceUpdater = requireNonNull(allowanceUpdater); } public List assessCustomFee(Topic topic, HandleContext context) { @@ -110,17 +104,13 @@ public List assessCustomFee(Topic topic, HandleCo continue; } - validateTokenAllowance(tokenAllowanceMap, fixedFee); + // todo after removing allowances - check limits tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); - // update allowance values - allowanceUpdater.applyFungibleTokenAllowances( - topic.topicIdOrThrow(), tokenAllowanceMap.get(tokenId), topicStore); + } else { - validateHbarAllowance(hbarAllowance, fixedFee); + // todo after removing allowances - check limits hbarTransfers = mergeTransfers( hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); - // update allowance values - allowanceUpdater.applyCryptoAllowances(topic.topicIdOrThrow(), hbarAllowance, topicStore); } transferCounts++; @@ -151,25 +141,6 @@ public List assessCustomFee(Topic topic, HandleCo return transactionBodies; } - private void validateTokenAllowance( - Map tokenAllowanceMap, FixedFee fixedFee) { - final var allowance = tokenAllowanceMap.get(fixedFee.denominatingTokenId()); - validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); - validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); - validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); - } - - private void validateHbarAllowance(TopicCryptoAllowance allowance, FixedFee fixedFee) { - validateTrue(allowance != null, SPENDER_DOES_NOT_HAVE_ALLOWANCE); - validateTrue(allowance.amountPerMessage() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); - validateTrue(allowance.amount() >= fixedFee.amount(), AMOUNT_EXCEEDS_ALLOWANCE); - } - - public void adjustAllowance(CryptoTransferTransactionBody syntheticBody) { - // todo adjust allowance - // extract the code for updating the allowance amounts from ConsensusApproveAllowanceHandler and reuse it here - } - private List buildCustomFeeHbarTransferList(AccountID payer, AccountID collector, FixedFee fee) { return List.of( AccountAmount.newBuilder() diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java deleted file mode 100644 index e947173d9456..000000000000 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusAllowancesValidator.java +++ /dev/null @@ -1,199 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.consensus.impl.validators; - -import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_IS_CONTRACT; -import static com.hedera.hapi.node.base.ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE; -import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_SPENDER_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT; -import static com.hedera.hapi.node.base.ResponseCodeEnum.NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES; -import static com.hedera.hapi.node.base.ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY; -import static com.hedera.hapi.node.base.ResponseCodeEnum.TOKEN_NOT_ASSOCIATED_TO_ACCOUNT; -import static com.hedera.hapi.node.base.ResponseCodeEnum.TOPIC_DELETED; -import static com.hedera.node.app.service.consensus.impl.util.ConsensusHandlerHelper.getIfUsable; -import static com.hedera.node.app.spi.validation.Validations.mustExist; -import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; -import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; -import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; -import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; -import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; -import com.hedera.hapi.node.base.TokenID; -import com.hedera.hapi.node.base.TokenType; -import com.hedera.hapi.node.base.TopicID; -import com.hedera.hapi.node.state.token.Account; -import com.hedera.hapi.node.token.ConsensusApproveAllowanceTransactionBody; -import com.hedera.node.app.service.consensus.ReadableTopicStore; -import com.hedera.node.app.service.token.ReadableAccountStore; -import com.hedera.node.app.service.token.ReadableTokenRelationStore; -import com.hedera.node.app.service.token.ReadableTokenStore; -import com.hedera.node.app.spi.workflows.HandleContext; -import com.hedera.node.app.spi.workflows.PreCheckException; -import edu.umd.cs.findbugs.annotations.NonNull; -import edu.umd.cs.findbugs.annotations.Nullable; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import javax.inject.Inject; - -public class ConsensusAllowancesValidator { - - /** - * Constructs a {@link ConsensusAllowancesValidator} instance. - */ - @Inject - public ConsensusAllowancesValidator() { - // Needed for Dagger injection - } - - public void pureChecks(ConsensusApproveAllowanceTransactionBody op) throws PreCheckException { - // The transaction must have at least one type of allowance. - final var cryptoAllowances = op.consensusCryptoFeeScheduleAllowances(); - final var tokenAllowances = op.consensusTokenFeeScheduleAllowances(); - final var totalAllowancesSize = cryptoAllowances.size() + tokenAllowances.size(); - validateTruePreCheck(totalAllowancesSize != 0, EMPTY_ALLOWANCES); - validateCryptoAllowances(cryptoAllowances); - validateTokenAllowances(tokenAllowances); - } - - public void validateSemantics( - @NonNull final HandleContext context, - @NonNull final ConsensusApproveAllowanceTransactionBody op, - @NonNull final ReadableTopicStore topicStore) { - final var accountStore = context.storeFactory().readableStore(ReadableAccountStore.class); - final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); - final var tokenRelStore = context.storeFactory().readableStore(ReadableTokenRelationStore.class); - - for (final var cryptoAllowance : op.consensusCryptoFeeScheduleAllowances()) { - final var topicId = cryptoAllowance.topicIdOrThrow(); - final var ownerId = cryptoAllowance.ownerOrThrow(); - - // validate spender account - final var spenderAccount = accountStore.getAccountById(ownerId); - validateSpender(cryptoAllowance.amount(), spenderAccount); - validateTopic(topicId, topicStore); - } - - for (var tokenAllowance : op.consensusTokenFeeScheduleAllowances()) { - final var topicId = tokenAllowance.topicIdOrThrow(); - final var ownerId = tokenAllowance.ownerOrThrow(); - final var tokenId = tokenAllowance.tokenIdOrThrow(); - - final var token = tokenStore.get(tokenId); - // check if token exists - validateTrue(token != null, INVALID_TOKEN_ID); - - // validate spender account - final var spenderAccount = accountStore.getAccountById(ownerId); - final var amount = tokenAllowance.amount(); - validateSpender(amount, spenderAccount); - - // validate token amount - validateTrue(TokenType.FUNGIBLE_COMMON.equals(token.tokenType()), NFT_IN_FUNGIBLE_TOKEN_ALLOWANCES); - final var relation = tokenRelStore.get(ownerId, tokenId); - validateTrue(relation != null, TOKEN_NOT_ASSOCIATED_TO_ACCOUNT); - - validateTopic(topicId, topicStore); - } - } - - /** - * Validates that either the amount to be approved is 0, or the spender account actually exists and has not been - * deleted. - * - * @param amount If 0, then always valid. Otherwise, we check the spender account. - * @param spenderAccount If the amount is not zero, then this must be non-null and not deleted. - */ - private void validateSpender(final long amount, @Nullable final Account spenderAccount) { - validateTrue(spenderAccount != null, INVALID_ALLOWANCE_SPENDER_ID); - validateFalse(spenderAccount.smartContract(), ACCOUNT_IS_CONTRACT); - validateTrue(amount == 0 || !spenderAccount.deleted(), INVALID_ALLOWANCE_SPENDER_ID); - } - - /** - * Validates that the topic exists and has not been deleted. - * - * @param topicID Validates that this is non-null and not deleted. - */ - private void validateTopic(@Nullable final TopicID topicID, @NonNull final ReadableTopicStore topicStore) { - requireNonNull(topicStore); - - validateTrue(topicID != null, INVALID_TOPIC_ID); - final var topic = getIfUsable(topicID, topicStore); - validateFalse(topic.deleted(), TOPIC_DELETED); - } - - private static void validateCryptoAllowances(List cryptoAllowances) - throws PreCheckException { - final var uniqueMap = new HashMap(); - for (var hbarAllowance : cryptoAllowances) { - // Check if a given AccountId/TopicId pair already exists in the crypto allowances list - validateFalsePreCheck( - uniqueMap.containsKey(hbarAllowance.owner()) - && uniqueMap.get(hbarAllowance.owner()).equals(hbarAllowance.topicId()), - REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); - // Validate the allowance amount and amount per message - validateTruePreCheck(hbarAllowance.amount() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck(hbarAllowance.amountPerMessage() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck( - hbarAllowance.amount() >= hbarAllowance.amountPerMessage(), - ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); - // Add the unique (AccountID, TopicID) pair to the map - uniqueMap.put(hbarAllowance.owner(), hbarAllowance.topicId()); - } - } - - private static void validateTokenAllowances(List tokenAllowances) - throws PreCheckException { - final var uniqueMap = new HashMap>(); - - for (var tokenAllowance : tokenAllowances) { - final var accountId = tokenAllowance.owner(); - final var topicId = tokenAllowance.topicId(); - final var tokenId = tokenAllowance.tokenId(); - mustExist(tokenId, INVALID_TOKEN_ID); - // Retrieve the map of AccountID -> TokenID for the given TopicID - final var accountTokenMap = uniqueMap.get(topicId); - - // If the TopicID already has an AccountID -> TokenID map, check if the AccountID exists - if (accountTokenMap != null && accountTokenMap.containsKey(accountId)) { - // If the AccountID exists, check if the TokenID matches - validateFalsePreCheck( - accountTokenMap.get(accountId).equals(tokenId), REPEATED_ALLOWANCE_IN_TRANSACTION_BODY); - } else { - // If the TopicID or AccountID does not exist, create the entry - uniqueMap.putIfAbsent(topicId, new HashMap<>()); - } - - // Add or update the (AccountID, TokenID) pair for the TopicID - uniqueMap.get(topicId).put(accountId, tokenId); - - // Validate the allowance amount and amount per message - validateTruePreCheck(tokenAllowance.amount() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck(tokenAllowance.amountPerMessage() >= 0, NEGATIVE_ALLOWANCE_AMOUNT); - validateTruePreCheck( - tokenAllowance.amount() >= tokenAllowance.amountPerMessage(), - ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE); - } - } -} diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java deleted file mode 100644 index d352538014d5..000000000000 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusApproveAllowanceTest.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.consensus.impl.test.handlers; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; -import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; -import com.hedera.hapi.node.base.TopicID; -import com.hedera.hapi.node.base.TransactionID; -import com.hedera.hapi.node.state.consensus.Topic; -import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; -import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; -import com.hedera.hapi.node.token.ConsensusApproveAllowanceTransactionBody; -import com.hedera.hapi.node.transaction.TransactionBody; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; -import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; -import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusAllowanceUpdater; -import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; -import com.hedera.node.app.spi.workflows.HandleContext; -import com.hedera.node.app.spi.workflows.PreCheckException; -import com.hedera.node.app.spi.workflows.PreHandleContext; -import java.util.ArrayList; -import java.util.List; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mock; - -public class ConsensusApproveAllowanceTest extends ConsensusTestBase { - @Mock(strictness = LENIENT) - private HandleContext.SavepointStack stack; - - private ConsensusApproveAllowanceHandler subject; - - @BeforeEach - void setUp() { - subject = new ConsensusApproveAllowanceHandler( - new ConsensusAllowancesValidator(), new ConsensusAllowanceUpdater()); - refreshStoresWithCurrentTopicOnlyInReadable(); - given(handleContext.savepointStack()).willReturn(stack); - } - - @Test - void preHandleWithValidAllowancesShouldPass() throws PreCheckException { - // Arrange - PreHandleContext mockContext = mock(PreHandleContext.class); - var txnBody = consensusApproveAllowanceTransaction(ownerId, List.of(), List.of()); - - when(mockContext.body()).thenReturn(txnBody); - when(mockContext.payer()).thenReturn(ownerId); - - // Act & Assert: should not throw any exception - subject.preHandle(mockContext); - - verify(mockContext, times(1)).body(); - } - - @Test - void handleWithEmptyAllowancesShouldNotUpdateState() { - // Arrange - WritableTopicStore mockTopicStore = mock(WritableTopicStore.class); - var emptyTxnBody = consensusApproveAllowanceTransaction(ownerId, List.of(), List.of()); - - given(handleContext.body()).willReturn(emptyTxnBody); - - when(handleContext.storeFactory().writableStore(WritableTopicStore.class)) - .thenReturn(mockTopicStore); - - // Act - subject.handle(handleContext); - - // Assert - verify(mockTopicStore, never()).put(any()); - } - - @Test - void happyPathAddsAllowances() { - setUpStores(handleContext); - final var txn = consensusApproveAllowanceTransaction( - payerId, List.of(consensusCryptoAllowance()), List.of(consensusTokenAllowance())); - given(handleContext.body()).willReturn(txn); - final var topic = writableStore.getTopic(topicId); - assertNotNull(topic); - assertThat(topic.cryptoAllowances()).isEmpty(); - assertThat(topic.tokenAllowances()).isEmpty(); - - subject.handle(handleContext); - - final var modifiedTopic = writableStore.getTopic(topicId); - assertNotNull(modifiedTopic); - assertThat(modifiedTopic.cryptoAllowances()).hasSize(1); - assertThat(modifiedTopic.tokenAllowances()).hasSize(1); - - assertThat(modifiedTopic.cryptoAllowances().getFirst().spenderId()).isEqualTo(ownerId); - assertThat(modifiedTopic.cryptoAllowances().getFirst().amount()).isEqualTo(100); - assertThat(modifiedTopic.cryptoAllowances().getFirst().amountPerMessage()) - .isEqualTo(10); - assertThat(modifiedTopic.tokenAllowances().getFirst().spenderId()).isEqualTo(ownerId); - assertThat(modifiedTopic.tokenAllowances().getFirst().amount()).isEqualTo(100); - assertThat(modifiedTopic.tokenAllowances().getFirst().amountPerMessage()) - .isEqualTo(10); - assertThat(modifiedTopic.tokenAllowances().getFirst().tokenId()).isEqualTo(fungibleTokenId); - } - - @Test - void handleWithZeroAllowanceShouldRemoveAllowanceFromStore() { - setUpStores(handleContext); - // Mock topic store and a topic with an existing allowance - var topicFromStateId = TopicID.newBuilder().topicNum(1).build(); - List initialCryptoAllowances = new ArrayList<>(); - List initialTokenAllowances = new ArrayList<>(); - - initialCryptoAllowances.add(TopicCryptoAllowance.newBuilder() - .spenderId(ownerId) - .amount(100L) - .amountPerMessage(10L) - .build()); - initialTokenAllowances.add(TopicFungibleTokenAllowance.newBuilder() - .spenderId(ownerId) - .amount(100L) - .amountPerMessage(10L) - .tokenId(fungibleTokenId) - .build()); - - // Add the topic with the initial allowance - Topic topic = Topic.newBuilder() - .topicId(topicFromStateId) - .cryptoAllowances(initialCryptoAllowances) - .tokenAllowances(initialTokenAllowances) - .build(); - writableStore.put(topic); - - // Create an allowance transaction with amount 0 (which should remove the allowance) - ConsensusCryptoFeeScheduleAllowance zeroCryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() - .owner(ownerId) - .amount(0L) // Passing 0 to remove the allowance - .amountPerMessage(0L) - .topicId(topicFromStateId) - .build(); - ConsensusTokenFeeScheduleAllowance zeroTokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(ownerId) - .amount(0L) // Passing 0 to remove the allowance - .amountPerMessage(0L) - .topicId(topicFromStateId) - .tokenId(fungibleTokenId) - .build(); - var allowanceTxnBody = consensusApproveAllowanceTransaction( - ownerId, List.of(zeroCryptoAllowance), List.of(zeroTokenAllowance)); - - when(handleContext.body()).thenReturn(allowanceTxnBody); - - // Act - subject.handle(handleContext); - - // Assert - Topic updatedTopic = writableStore.getTopic(topicFromStateId); - assertNotNull(updatedTopic); - assertTrue(updatedTopic.cryptoAllowances().isEmpty(), "Crypto allowance should be removed from the store"); - assertTrue(updatedTopic.tokenAllowances().isEmpty(), "Token allowance should be removed from the store"); - } - - private TransactionBody consensusApproveAllowanceTransaction( - final AccountID id, - final List cryptoAllowance, - final List tokenAllowance) { - final var transactionID = TransactionID.newBuilder().accountID(id); - final var allowanceTxnBody = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusCryptoFeeScheduleAllowances(cryptoAllowance) - .consensusTokenFeeScheduleAllowances(tokenAllowance) - .build(); - return TransactionBody.newBuilder() - .transactionID(transactionID) - .consensusApproveAllowance(allowanceTxnBody) - .build(); - } - - private ConsensusCryptoFeeScheduleAllowance consensusCryptoAllowance() { - return ConsensusCryptoFeeScheduleAllowance.newBuilder() - .amount(100) - .amountPerMessage(10) - .topicId(topicId) - .owner(ownerId) - .build(); - } - - private ConsensusTokenFeeScheduleAllowance consensusTokenAllowance() { - return ConsensusTokenFeeScheduleAllowance.newBuilder() - .amount(100) - .amountPerMessage(10) - .tokenId(fungibleTokenId) - .topicId(topicId) - .owner(ownerId) - .build(); - } -} diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java index a85226b0c63e..33e7ca5250a8 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusHandlersTest.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; -import com.hedera.node.app.service.consensus.impl.handlers.ConsensusApproveAllowanceHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusCreateTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusDeleteTopicHandler; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusGetTopicInfoHandler; @@ -35,7 +34,6 @@ class ConsensusHandlersTest { private ConsensusGetTopicInfoHandler consensusGetTopicInfoHandler; private ConsensusSubmitMessageHandler consensusSubmitMessageHandler; private ConsensusUpdateTopicHandler consensusUpdateTopicHandler; - private ConsensusApproveAllowanceHandler consensusApproveAllowanceHandler; private ConsensusHandlers consensusHandlers; @@ -46,15 +44,13 @@ public void setUp() { consensusGetTopicInfoHandler = mock(ConsensusGetTopicInfoHandler.class); consensusSubmitMessageHandler = mock(ConsensusSubmitMessageHandler.class); consensusUpdateTopicHandler = mock(ConsensusUpdateTopicHandler.class); - consensusApproveAllowanceHandler = mock(ConsensusApproveAllowanceHandler.class); consensusHandlers = new ConsensusHandlers( consensusCreateTopicHandler, consensusDeleteTopicHandler, consensusGetTopicInfoHandler, consensusSubmitMessageHandler, - consensusUpdateTopicHandler, - consensusApproveAllowanceHandler); + consensusUpdateTopicHandler); } @Test @@ -96,12 +92,4 @@ void consensusUpdateTopicHandlerReturnsCorrectInstance() { consensusHandlers.consensusUpdateTopicHandler(), "consensusUpdateTopicHandler does not return correct instance"); } - - @Test - void consensusApproveAllowanceHandlerReturnsCorrectInstance() { - assertEquals( - consensusApproveAllowanceHandler, - consensusHandlers.consensusApproveAllowanceHandler(), - "consensusApproveAllowanceHandler does not return correct instance"); - } } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index e5889f5b7f48..d6979f4a7d06 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -49,6 +49,7 @@ import com.hedera.node.app.service.consensus.impl.ReadableTopicStoreImpl; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.handlers.ConsensusSubmitMessageHandler; +import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusCustomFeeAssessor; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.spi.fees.FeeCalculator; @@ -90,7 +91,7 @@ class ConsensusSubmitMessageHandlerTest extends ConsensusTestBase { @BeforeEach void setUp() { commonSetUp(); - subject = new ConsensusSubmitMessageHandler(); + subject = new ConsensusSubmitMessageHandler(new ConsensusCustomFeeAssessor()); final var config = HederaTestConfigBuilder.create() .withValue("consensus.message.maxBytesAllowed", 100) @@ -200,7 +201,7 @@ void handleWorksAsExpected() { @DisplayName("Handle throws IOException") void handleThrowsIOException() { givenValidTopic(); - subject = new ConsensusSubmitMessageHandler() { + subject = new ConsensusSubmitMessageHandler(new ConsensusCustomFeeAssessor()) { @Override public Topic updateRunningHashAndSequenceNumber( @NonNull final TransactionBody txn, @NonNull final Topic topic, @Nullable Instant consensusNow) diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java deleted file mode 100644 index 57dcd8d27243..000000000000 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/validators/ConsensusAllowancesValidatorTest.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.consensus.impl.test.validators; - -import static com.hedera.hapi.node.base.ResponseCodeEnum.ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE; -import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOKEN_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.NEGATIVE_ALLOWANCE_AMOUNT; -import static com.hedera.hapi.node.base.ResponseCodeEnum.REPEATED_ALLOWANCE_IN_TRANSACTION_BODY; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.ConsensusCryptoFeeScheduleAllowance; -import com.hedera.hapi.node.base.ConsensusTokenFeeScheduleAllowance; -import com.hedera.hapi.node.base.TokenID; -import com.hedera.hapi.node.base.TopicID; -import com.hedera.hapi.node.token.ConsensusApproveAllowanceTransactionBody; -import com.hedera.node.app.service.consensus.impl.validators.ConsensusAllowancesValidator; -import com.hedera.node.app.spi.workflows.PreCheckException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class ConsensusAllowancesValidatorTest { - - private ConsensusAllowancesValidator validator; - - @BeforeEach - void setUp() { - validator = new ConsensusAllowancesValidator(); - } - - @Test - void validAllowancesPasses() throws PreCheckException { - // Arrange - var cryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .amount(100L) - .amountPerMessage(10L) - .build(); - - var tokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .amount(200L) - .amountPerMessage(20L) - .tokenId(TokenID.DEFAULT) - .build(); - - var op = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusCryptoFeeScheduleAllowances(cryptoAllowance) - .consensusTokenFeeScheduleAllowances(tokenAllowance) - .build(); - - // Act & Assert: Should pass without exception - validator.pureChecks(op); - } - - @Test - void emptyAllowancesThrows() { - // Arrange: Create an operation with no allowances - var op = ConsensusApproveAllowanceTransactionBody.newBuilder().build(); - - // Act & Assert: Should throw PreCheckException with EMPTY_ALLOWANCES response - var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); - assertEquals(exception.responseCode(), EMPTY_ALLOWANCES); - } - - @Test - void testRepeatedAllowanceForSameAccountButDifferentTopic() throws PreCheckException { - // Arrange: Create allowances with the same AccountID but different TopicIDs - var tokenAllowance1 = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.newBuilder().topicNum(1).build()) - .amount(100L) - .amountPerMessage(10L) - .tokenId(TokenID.DEFAULT) - .build(); - - var tokenAllowance2 = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.newBuilder().topicNum(2).build()) - .amount(200L) - .amountPerMessage(20L) - .tokenId(TokenID.DEFAULT) - .build(); - - // Build the operation containing the token allowances - var op = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusTokenFeeScheduleAllowances(tokenAllowance1, tokenAllowance2) - .build(); - - // Act & Assert: Should pass without exception - validator.pureChecks(op); - } - - @Test - void repeatedTokenAllowancesThrows() { - // Arrange: Create two token allowances with the same owner and topicId - var tokenAllowance1 = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .amount(100L) - .amountPerMessage(10L) - .tokenId(TokenID.DEFAULT) - .build(); - - var tokenAllowance2 = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) // Same AccountID and TopicID as tokenAllowance1 - .amount(200L) - .amountPerMessage(20L) - .tokenId(TokenID.DEFAULT) - .build(); - - // Build the operation containing the token allowances - var op = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusTokenFeeScheduleAllowances(tokenAllowance1, tokenAllowance2) - .build(); - - // Act & Assert: Should throw PreCheckException with REPEATED_ALLOWANCE_IN_TRANSACTION_BODY - var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); - assertEquals(REPEATED_ALLOWANCE_IN_TRANSACTION_BODY, exception.responseCode()); - } - - @Test - void repeatedCryptoAllowancesThrows() { - // Arrange: Create two crypto allowances with the same owner and topicId - var cryptoAllowance1 = ConsensusCryptoFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .amount(100L) - .amountPerMessage(10L) - .build(); - - var cryptoAllowance2 = ConsensusCryptoFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) // Same AccountID and TopicID as cryptoAllowance1 - .amount(200L) - .amountPerMessage(20L) - .build(); - - // Build the operation containing the crypto allowances - var op = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusCryptoFeeScheduleAllowances(cryptoAllowance1, cryptoAllowance2) - .build(); - - // Act & Assert: Should throw PreCheckException with REPEATED_ALLOWANCE_IN_TRANSACTION_BODY - var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); - assertEquals(REPEATED_ALLOWANCE_IN_TRANSACTION_BODY, exception.responseCode()); - } - - @Test - void invalidTokenIdThrows() { - // Arrange - var tokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .amount(100L) - .amountPerMessage(10L) - .tokenId((TokenID) null) // Invalid token ID - .build(); - - var op = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusTokenFeeScheduleAllowances(tokenAllowance) - .build(); - - // Act & Assert: Should throw PreCheckException with INVALID_TOKEN_ID code - var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); - assertEquals(INVALID_TOKEN_ID, exception.responseCode()); - } - - @Test - void negativeAmountsThrows() { - // Arrange - var cryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .amount(-100L) // Negative allowance - .amountPerMessage(10L) - .build(); - - var op = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusCryptoFeeScheduleAllowances(cryptoAllowance) - .build(); - - // Act & Assert: Should throw PreCheckException with NEGATIVE_ALLOWANCE_AMOUNT code - var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); - assertEquals(NEGATIVE_ALLOWANCE_AMOUNT, exception.responseCode()); - } - - @Test - void amountPerMessageExceedsTotal() { - // Arrange - var cryptoAllowance = ConsensusCryptoFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .amount(10) - .amountPerMessage(15) - .build(); - var tokenAllowance = ConsensusTokenFeeScheduleAllowance.newBuilder() - .owner(AccountID.DEFAULT) - .topicId(TopicID.DEFAULT) - .tokenId(TokenID.DEFAULT) - .amount(10) - .amountPerMessage(15) - .build(); - - var op = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusCryptoFeeScheduleAllowances(cryptoAllowance) - .build(); - - var tokenOp = ConsensusApproveAllowanceTransactionBody.newBuilder() - .consensusTokenFeeScheduleAllowances(tokenAllowance) - .build(); - - // Act & Assert: Should throw PreCheckException with ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE code - var exception = assertThrows(PreCheckException.class, () -> validator.pureChecks(op)); - assertEquals(ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE, exception.responseCode()); - - var tokenException = assertThrows(PreCheckException.class, () -> validator.pureChecks(tokenOp)); - assertEquals(ALLOWANCE_PER_MESSAGE_EXCEEDS_TOTAL_ALLOWANCE, tokenException.responseCode()); - } -} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index d461e0444a64..8bf8e805ccfb 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -101,18 +101,6 @@ protected static SpecOperation[] associateFeeTokensAndSubmitter() { }; } - protected static SpecOperation[] setupBaseForUpdate() { - return new SpecOperation[]{ - newKeyNamed(ADMIN_KEY), - newKeyNamed(SUBMIT_KEY), - newKeyNamed(FEE_SCHEDULE_KEY), - newKeyNamed(FEE_SCHEDULE_KEY2), - cryptoCreate(COLLECTOR), - tokenCreate(TOKEN).tokenType(TokenType.FUNGIBLE_COMMON).initialSupply(500), - tokenAssociate(COLLECTOR, TOKEN) - }; - } - /** * Create and transfer multiple tokens with 2 layer custom fees to given account. * diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index ed0e095076c1..d01776c43e3b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -19,8 +19,6 @@ import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; @@ -29,10 +27,8 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; -import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHollow; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.flattened; @@ -155,9 +151,9 @@ final Stream messageSubmitToPublicTopicWithFee1Hbar() { return hapiTest( cryptoCreate(collector).balance(0L), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - approveTopicAllowance() - .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(SUBMITTER), + // approveTopicAllowance() + // .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) + // .payingWith(SUBMITTER), submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTinyBars(ONE_HBAR)); } @@ -171,9 +167,6 @@ final Stream messageSubmitToPublicTopicWithFee1token() { cryptoCreate(collector), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, BASE_TOKEN, TOPIC, 100, 1) - .payingWith(SUBMITTER), submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); } @@ -193,10 +186,6 @@ final Stream messageSubmitToPublicTopicWith3layerFee() { cryptoCreate(topicFeeCollector).balance(0L), tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) - .payingWith(SUBMITTER), // submit message submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance @@ -218,7 +207,6 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() associateAllTokensToCollectors(), // create topic with 10 multilayer fees - 9 HTS + 1 HBAR createTopicWith10Different2layerFees(), - approveTopicAllowanceForAllFees(), submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance assertAllCollectorsBalances())); @@ -245,16 +233,7 @@ private SpecOperation createTopicWith10Different2layerFees() { topicCreateOp.withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collectorName + 0)); return topicCreateOp; } - // TOPIC_FEE_108 - private SpecOperation approveTopicAllowanceForAllFees() { - final var approveAllowance = approveTopicAllowance().payingWith(SUBMITTER); - for (int i = 0; i < 9; i++) { - approveAllowance.addTokenAllowance(SUBMITTER, TOKEN_PREFIX + i, TOPIC, 100, 1); - } - approveAllowance.addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR); - return approveAllowance; - } // TOPIC_FEE_108 private SpecOperation[] assertAllCollectorsBalances() { final var collectorName = "collector_"; @@ -284,10 +263,6 @@ final Stream treasurySubmitToPublicTopicWith3layerFees() { cryptoCreate(topicFeeCollector).balance(0L), tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance - approveTopicAllowance() - .addTokenAllowance(SUBMITTER, token, TOPIC, 100, 1) - .payingWith(SUBMITTER), // submit message submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), // assert topic fee collector balance @@ -319,11 +294,6 @@ final Stream treasuryOfSecondLayerSubmitToPublic() { tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(DENOM_TREASURY, token, TOPIC, 100, 1) - .payingWith(DENOM_TREASURY), - // submit submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), @@ -354,11 +324,6 @@ final Stream collectorSubmitToPublicTopicWith3layerFees() { // create topic createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(topicFeeCollector, token, TOPIC, 100, 1) - .payingWith(topicFeeCollector), - // submit submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), @@ -389,11 +354,6 @@ final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFee tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), - // add allowance for denomination token treasury - approveTopicAllowance() - .addTokenAllowance(secondLayerFeeCollector, token, TOPIC, 100, 1) - .payingWith(secondLayerFeeCollector), - // submit submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), @@ -421,10 +381,6 @@ final Stream anotherCollectorSubmitMessageToATopicWithAFee() { cryptoCreate(collector), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - // add allowance and submit with another collector - approveTopicAllowance() - .addTokenAllowance(anotherCollector, BASE_TOKEN, TOPIC, 100, 1) - .payingWith(anotherCollector), submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), // the fee was paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1))); @@ -439,9 +395,6 @@ final Stream messageTopicSubmitToHollowAccountAsFeeCollector() { // create hollow account with ONE_HUNDRED_HBARS createHollow(1, i -> collector), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - approveTopicAllowance() - .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(SUBMITTER), submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // collector should be still a hollow account @@ -463,9 +416,6 @@ final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { .feeScheduleKeyName(feeScheduleKey) .feeExemptKeys(feeScheduleKey) .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - approveTopicAllowance() - .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(SUBMITTER), submitMessageTo(TOPIC).message("TEST").signedByPayerAnd(feeScheduleKey), getAccountBalance(collector).hasTinyBars(0L)); } @@ -479,9 +429,6 @@ final Stream collectorSubmitMessageToTopicWithFTFee() { cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - approveTopicAllowance() - .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) - .payingWith(collector), submitMessageTo(TOPIC).message("TEST").payingWith(collector), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); } @@ -494,23 +441,19 @@ final Stream collectorSubmitMessageToTopicWithHbarFee() { return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - approveTopicAllowance() - .addCryptoAllowance(collector, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - .payingWith(collector) - .via("approveAllowance"), - submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit"), - // assert collector's tinyBars balance - withOpContext((spec, log) -> { - final var submitTxnRecord = getTxnRecord("submit"); - final var allowanceTxnRecord = getTxnRecord("approveAllowance"); - allRunFor(spec, submitTxnRecord, allowanceTxnRecord); - final var transactionTxnFee = - submitTxnRecord.getResponseRecord().getTransactionFee(); - final var allowanceTxnFee = - allowanceTxnRecord.getResponseRecord().getTransactionFee(); - getAccountBalance(collector) - .hasTinyBars(ONE_HUNDRED_HBARS - transactionTxnFee - allowanceTxnFee); - })); + submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit")); + // assert collector's tinyBars balance + // withOpContext((spec, log) -> { + // final var submitTxnRecord = getTxnRecord("submit"); + // final var allowanceTxnRecord = getTxnRecord("approveAllowance"); + // allRunFor(spec, submitTxnRecord, allowanceTxnRecord); + // final var transactionTxnFee = + // submitTxnRecord.getResponseRecord().getTransactionFee(); + // final var allowanceTxnFee = + // allowanceTxnRecord.getResponseRecord().getTransactionFee(); + // getAccountBalance(collector) + // .hasTinyBars(ONE_HUNDRED_HBARS - transactionTxnFee - allowanceTxnFee); + // })); } @HapiTest @@ -530,10 +473,10 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { createTopic(TOPIC) .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)) .withConsensusCustomFee(fixedConsensusHtsFee(1, SECOND_TOKEN, secondCollector)), - approveTopicAllowance() - .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) - .addTokenAllowance(collector, SECOND_TOKEN, TOPIC, 1, 1) - .payingWith(collector), + // approveTopicAllowance() + // .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) + // .addTokenAllowance(collector, SECOND_TOKEN, TOPIC, 1, 1) + // .payingWith(collector), submitMessageTo(TOPIC).message("TEST").payingWith(collector), // only second fee should be paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), From 158ab5baf884641610a15c9c099b05dbb26971c9 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 5 Dec 2024 15:32:59 +0200 Subject: [PATCH 54/94] Remove allowances Signed-off-by: Zhivko Kelchev --- .../services/basic_types.proto | 70 +--------- .../consensus_approve_topic_allowance.proto | 74 ---------- .../services/consensus_service.proto | 8 -- .../services/state/consensus/topic.proto | 94 +------------ .../services/transaction_body.proto | 12 +- .../java/com/hedera/hapi/util/HapiUtils.java | 1 - .../mainnet/upgrade/throttles.json | 1 - .../testnet/upgrade/throttles.json | 1 - .../node/app/services/ServiceScopeLookup.java | 3 +- .../node/app/store/WritableStoreFactory.java | 1 + .../node/config/data/ApiPermissionConfig.java | 3 - .../customfee/ConsensusCustomFeeAssessor.java | 20 --- .../main/resources/genesis/throttles-dev.json | 1 - .../bdd/junit/hedera/utils/GrpcUtils.java | 2 - .../bdd/spec/transactions/TxnFactory.java | 6 - .../bdd/spec/transactions/TxnVerbs.java | 5 - .../consensus/HapiTopicApproveAllowance.java | 128 ------------------ .../bdd/suites/hip991/TopicCustomFeeTest.java | 28 ---- 18 files changed, 14 insertions(+), 444 deletions(-) delete mode 100644 hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto delete mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java diff --git a/hapi/hedera-protobufs/services/basic_types.proto b/hapi/hedera-protobufs/services/basic_types.proto index 261f76bb7398..e3a50ccbbb2a 100644 --- a/hapi/hedera-protobufs/services/basic_types.proto +++ b/hapi/hedera-protobufs/services/basic_types.proto @@ -1241,19 +1241,14 @@ enum HederaFunctionality { TokenAirdrop = 93; /** - * Remove one or more pending airdrops from state on behalf of the sender(s) for each airdrop. - */ + * Remove one or more pending airdrops from state on behalf of the sender(s) for each airdrop. + */ TokenCancelAirdrop = 94; /** - * Claim one or more pending airdrops + * Claim one or more pending airdrops */ TokenClaimAirdrop = 95; - - /** - * Approve allowance for a given topic - */ - ConsensusApproveAllowance = 96; } /** @@ -1778,62 +1773,3 @@ message PendingAirdropValue { */ uint64 amount = 1; } - -/** - * An approved allowance of hbar transfers for a spender.
- * This message SHALL be used to record the allowance of hbars that an account has approved - * for sending messages to a given topic. - */ -message ConsensusCryptoFeeScheduleAllowance { - /** - * The account ID of the hbar owner (ie. the grantor of the allowance). - */ - AccountID owner = 1; - - /** - * The topic ID enabled to spend fees from the hbar allowance. - */ - TopicID topicId = 2; - - /** - * The maximum amount of the spender's allowance in tinybars. - */ - uint64 amount = 3; - - /** - * The maximum amount of the spender's token allowance per message. - */ - uint64 amount_per_message = 4; -} - -/** - * An approved allowance of hbar transfers for a spender.
- * This message SHALL be used to record the allowance of fungible tokens - * that an account has approved for sending messages to a given topic. - */ -message ConsensusTokenFeeScheduleAllowance { - /** - * The token that the allowance pertains to. - */ - TokenID tokenId = 1; - - /** - * The account ID of the token owner (ie. the grantor of the allowance). - */ - AccountID owner = 2; - - /** - * The topic ID enabled to spend fees from the token allowance. - */ - TopicID topicId = 3; - - /** - * The maximum amount of the spender's token allowance. - */ - uint64 amount = 4; - - /** - * The maximum amount of the spender's token allowance per message. - */ - uint64 amount_per_message = 5; -} diff --git a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto b/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto deleted file mode 100644 index f3e29c78cfa2..000000000000 --- a/hapi/hedera-protobufs/services/consensus_approve_topic_allowance.proto +++ /dev/null @@ -1,74 +0,0 @@ -/** - * # Approve Allowance for Topic - * Messages used to approve allowance for a given topic. - * - * ### Keywords - * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", - * "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this - * document are to be interpreted as described in - * [RFC2119](https://www.ietf.org/rfc/rfc2119) and clarified in - * [RFC8174](https://www.ietf.org/rfc/rfc8174). - */ -syntax = "proto3"; - -package proto; - -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -option java_package = "com.hederahashgraph.api.proto.java"; -// <<>> This comment is special code for setting PBJ Compiler java package -option java_multiple_files = true; - -import "basic_types.proto"; - -/** - * Approve Topic Allowance
- * Approve allowance for submitting a message to a topic with custom fee. - * - * The topic MUST have a given custom fee in order to set allowance for that fee, - * otherwise the transaction SHALL fail.
- * Setting the amount to zero in 'CryptoAllowance' or 'TokenAllowance' - * SHALL remove the respective allowance for the spender
- */ -message ConsensusApproveAllowanceTransactionBody { - /** - * A list of Fee Schedule Allowances.
- * This list details hbar allowances approved by the account owner. - *

- * This list MAY be empty.
- * If this list is empty, the `consensus_token_fee_schedule_allowances` - * list MUST NOT be empty.
- * Amounts assigned here SHALL NOT be available to transfer.
- * Consensus Custom Fee Schedule charges SHALL be deducted from allowances - * in this list. - */ - repeated ConsensusCryptoFeeScheduleAllowance consensus_crypto_fee_schedule_allowances = 4; - - /** - * A list of Fee Schedule Allowances.
- * This list details fungible token allowances approved by the account owner. - *

- * This list MAY be empty.
- * If this list is empty, the `consensus_crypto_fee_schedule_allowances` - * list MUST NOT be empty.
- * Amounts assigned here SHALL NOT be available to transfer.
- * Consensus Custom Fee Schedule charges SHALL be deducted from allowances - * in this list. - */ - repeated ConsensusTokenFeeScheduleAllowance consensus_token_fee_schedule_allowances = 5; -} diff --git a/hapi/hedera-protobufs/services/consensus_service.proto b/hapi/hedera-protobufs/services/consensus_service.proto index ea0d88c9abca..fbee23f131b6 100644 --- a/hapi/hedera-protobufs/services/consensus_service.proto +++ b/hapi/hedera-protobufs/services/consensus_service.proto @@ -120,12 +120,4 @@ service ConsensusService { * Request is [ConsensusSubmitMessageTransactionBody](#proto.ConsensusSubmitMessageTransactionBody) */ rpc submitMessage (Transaction) returns (TransactionResponse); - - /** - * Approve allowance for custom fees. - *

- * Set account allowances for a topic. This includes total allowance and allowance per message. - * Request is [ConsensusApproveAllowanceTransactionBody](#proto.ConsensusApproveAllowanceTransactionBody) - */ - rpc approveAllowance (Transaction) returns (TransactionResponse); } diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index eee0cacf6614..7aee87952a09 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -52,8 +52,6 @@ option java_multiple_files = true; * 8. (Optional) A fee schedule key whose signature must be active for the topic's custom fees to be updated. * 9. (Optional) A list of keys that can submit messages without paying custom fees. * 10. (Optional) A list of custom fees to be assessed for each message submitted to the topic. - * 11. (Optional) A list of crypto allowances for the topic. - * 12. (Optional) A list of fungible token allowances for the topic. */ message Topic { /** @@ -112,24 +110,17 @@ message Topic { * If this field is unset, the current custom fees CANNOT be changed.
* If this field is set, that `Key` MUST sign any transaction to update * the custom fee schedule for this topic. - */ + */ Key fee_schedule_key = 11; /** - * A set of "privileged payer" keys
- * Keys in this list are permitted to submit messages to this topic without - * paying custom fees associated with this topic. + * A list of "privileged payer" keys. *

- * If a submit transaction is signed by _any_ key included in this set, + * If a submit transaction is signed by _any_ key from this list, * custom fees SHALL NOT be charged for that transaction.
- * A `fee_exempt_key_list` MUST NOT contain more than - * `MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST` keys.
- * A `fee_exempt_key_list` MUST NOT contain any duplicate keys.
- * A `fee_exempt_key_list` MAY contain keys for accounts that are inactive, - * deleted, or non-existent. - * If not set, there SHALL NOT be any fee-exempt keys. In particular, the - * following keys SHALL NOT be implicitly or automatically added to this - * list: `adminKey`, `submitKey`, `fee_schedule_key`. + * If fee_exempt_key_list is unset, it SHALL _implicitly_ contain + * the key `admin_key`, the key `submit_key`, and the key + * `fee_schedule_key`, if any of those keys are set. */ repeated Key fee_exempt_key_list = 12; @@ -146,77 +137,4 @@ message Topic { * charged _in addition to_ the base network and node fees. */ repeated ConsensusCustomFee custom_fees = 13; - - /** - * A list of crypto allowances for this topic. - *

- * If a submit transaction is submitted by a user with an allowance, - * the custom fee SHALL be paid with the allowance.
- * If the allowance is not enough to pay the fee, the transaction SHALL fail.
- * If an allowance amount is set to 0, the allowance SHALL be removed.
- * It SHALL contain account identifier for which the allowance is approved, - * and the amount approved for that account. - */ - repeated TopicCryptoAllowance crypto_allowances = 14; - - /** - * A list of fungible token allowances for this topic. - *

- * If a submit transaction is submitted by a user with an allowance, - * the custom fee SHALL be payed with the token allowance
- * If the allowance is not enough to pay the fee, the transaction SHALL fail.
- * It contains account number for which the allowance is approved to and the token number.
- * It also contains and the amount approved for that account. - */ - repeated TopicFungibleTokenAllowance token_allowances = 30; -} - -/** - * Representation of crypto allowance for a topic in the network Merkle tree. - * - * A crypto allowance for a topic represents the hbar funds allocated by an account to cover the fees - * required for submitting messages when the topic is configured with 'custom fees'. - * This allows the spender to send messages to this topic while utilizing - * the allocated allowance to cover the associated cost. - */ -message TopicCryptoAllowance { - /** - * The account ID of the spender. - */ - AccountID spender_id = 1; - /** - * The total amount of hbar allocated for the allowance. - */ - uint64 amount = 2; - /** - * The amount of hbar allocated per message. - */ - uint64 amount_per_message = 3; -} - -/** - * Representation of fungible token allowance for a topic in the network Merkle tree. - - * A fungible token allowance for a topic represents the fungible token amounts allocated by an account - * to cover the fees required for submitting messages when the topic is configured with 'custom fees'. - * This allows the spender to send messages to this topic while utilizing - * the allocated fungible token allowance to cover the associated cost. - */ -message TopicFungibleTokenAllowance { - /** - * The ID of the fungible token. - */ - TokenID token_id = 1; - /** - * The account ID of the spender. - */ - AccountID spender_id = 2; - /** - * The total amount of fungible tokens allocated for the allowance. - */ - uint64 amount = 3; - /** - * The amount of fungible tokens allocated per message. - */ - uint64 amount_per_message = 4; } diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index 05bf59a9200a..cd4546147654 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -54,7 +54,6 @@ import "consensus_create_topic.proto"; import "consensus_update_topic.proto"; import "consensus_delete_topic.proto"; import "consensus_submit_message.proto"; -import "consensus_approve_topic_allowance.proto"; import "unchecked_submit.proto"; @@ -411,18 +410,13 @@ message TransactionBody { TokenAirdropTransactionBody tokenAirdrop = 58; /** - * A transaction body for a `cancelAirdrop` request. - */ + * A transaction body for a `cancelAirdrop` request. + */ TokenCancelAirdropTransactionBody tokenCancelAirdrop = 59; /** - * A transaction body for a `claimAirdrop` request. + * A transaction body for a `claimAirdrop` request. */ TokenClaimAirdropTransactionBody tokenClaimAirdrop = 60; - - /** - * A transaction body for a `consensusApproveAllowance` request. - */ - ConsensusApproveAllowanceTransactionBody consensusApproveAllowance = 61; } } diff --git a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java index 07de67e3fab5..e0f23a353d3d 100644 --- a/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java +++ b/hapi/src/main/java/com/hedera/hapi/util/HapiUtils.java @@ -185,7 +185,6 @@ public static HederaFunctionality functionOf(final TransactionBody txn) throws U case CONSENSUS_UPDATE_TOPIC -> HederaFunctionality.CONSENSUS_UPDATE_TOPIC; case CONSENSUS_DELETE_TOPIC -> HederaFunctionality.CONSENSUS_DELETE_TOPIC; case CONSENSUS_SUBMIT_MESSAGE -> HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE; - case CONSENSUS_APPROVE_ALLOWANCE -> HederaFunctionality.CONSENSUS_APPROVE_ALLOWANCE; case CONTRACT_CALL -> HederaFunctionality.CONTRACT_CALL; case CONTRACT_CREATE_INSTANCE -> HederaFunctionality.CONTRACT_CREATE; case CONTRACT_UPDATE_INSTANCE -> HederaFunctionality.CONTRACT_UPDATE; diff --git a/hedera-node/configuration/mainnet/upgrade/throttles.json b/hedera-node/configuration/mainnet/upgrade/throttles.json index 60c6b59d6fc8..06ae864ff7c7 100644 --- a/hedera-node/configuration/mainnet/upgrade/throttles.json +++ b/hedera-node/configuration/mainnet/upgrade/throttles.json @@ -21,7 +21,6 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", - "ConsensusApproveAllowance", "TokenGetNftInfo", "TokenGetInfo", "ScheduleDelete", diff --git a/hedera-node/configuration/testnet/upgrade/throttles.json b/hedera-node/configuration/testnet/upgrade/throttles.json index bb02f0108cbc..e1d83b109ac0 100644 --- a/hedera-node/configuration/testnet/upgrade/throttles.json +++ b/hedera-node/configuration/testnet/upgrade/throttles.json @@ -22,7 +22,6 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", - "ConsensusApproveAllowance", "TokenGetNftInfo", "TokenGetInfo", "ScheduleDelete", diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java index e64082a56f34..46b9ae01faaf 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/ServiceScopeLookup.java @@ -60,8 +60,7 @@ public String getServiceName(@NonNull final TransactionBody txBody) { case CONSENSUS_CREATE_TOPIC, CONSENSUS_UPDATE_TOPIC, CONSENSUS_DELETE_TOPIC, - CONSENSUS_SUBMIT_MESSAGE, - CONSENSUS_APPROVE_ALLOWANCE -> ConsensusService.NAME; + CONSENSUS_SUBMIT_MESSAGE -> ConsensusService.NAME; case CONTRACT_CREATE_INSTANCE, CONTRACT_UPDATE_INSTANCE, diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java index 8dce0561d649..8b225a4042bf 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/store/WritableStoreFactory.java @@ -103,6 +103,7 @@ private static Map, StoreEntry> createFactoryMap() { new StoreEntry(EntityIdService.NAME, (states, config, metrics) -> new WritableEntityIdStore(states))); // Schedule Service newMap.put(WritableScheduleStore.class, new StoreEntry(ScheduleService.NAME, WritableScheduleStoreImpl::new)); + newMap.put(WritableNodeStore.class, new StoreEntry(AddressBookService.NAME, WritableNodeStore::new)); return Collections.unmodifiableMap(newMap); } diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java index 98417998cc92..dfde84696748 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ApiPermissionConfig.java @@ -16,7 +16,6 @@ package com.hedera.node.config.data; -import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_APPROVE_ALLOWANCE; import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_CREATE_TOPIC; import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_DELETE_TOPIC; import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_GET_TOPIC_INFO; @@ -223,7 +222,6 @@ public record ApiPermissionConfig( @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange deleteTopic, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange submitMessage, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange getTopicInfo, - @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange approveTopicAllowance, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange ethereumTransaction, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange scheduleCreate, @ConfigProperty(defaultValue = "0-*") PermissionedAccountsRange scheduleSign, @@ -287,7 +285,6 @@ public record ApiPermissionConfig( permissionKeys.put(CONSENSUS_UPDATE_TOPIC, c -> c.updateTopic); permissionKeys.put(CONSENSUS_DELETE_TOPIC, c -> c.deleteTopic); permissionKeys.put(CONSENSUS_SUBMIT_MESSAGE, c -> c.submitMessage); - permissionKeys.put(CONSENSUS_APPROVE_ALLOWANCE, c -> c.approveTopicAllowance); permissionKeys.put(TOKEN_CREATE, c -> c.tokenCreate); permissionKeys.put(TOKEN_FREEZE_ACCOUNT, c -> c.tokenFreezeAccount); permissionKeys.put(TOKEN_UNFREEZE_ACCOUNT, c -> c.tokenUnfreezeAccount); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index ac394d80b737..e17711c42b7b 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -24,8 +24,6 @@ import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; import com.hedera.hapi.node.state.consensus.Topic; -import com.hedera.hapi.node.state.consensus.TopicCryptoAllowance; -import com.hedera.hapi.node.state.consensus.TopicFungibleTokenAllowance; import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; @@ -36,7 +34,6 @@ import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -63,23 +60,6 @@ public List assessCustomFee(Topic topic, HandleCo final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); - // todo: allowance validation will be changed, when the storage situation is clear. - - // lookup for hbar allowance - TopicCryptoAllowance hbarAllowance = null; - for (final var allowance : topic.cryptoAllowances()) { - if (payer.equals(allowance.spenderId())) { - hbarAllowance = allowance; - } - } - // lookup for fungible token allowance - Map tokenAllowanceMap = new HashMap<>(); - for (final var allowance : topic.tokenAllowances()) { - if (payer.equals(allowance.spenderId())) { - tokenAllowanceMap.put(allowance.tokenId(), allowance); - } - } - final var tokenTransfers = new ArrayList(); List hbarTransfers = new ArrayList<>(); // we need to count the number of balance adjustments, diff --git a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json index f5ae5709e98a..3585961b5488 100644 --- a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json +++ b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles-dev.json @@ -21,7 +21,6 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", - "ConsensusApproveAllowance", "TokenGetInfo", "TokenGetNftInfo", "TokenGetNftInfos", diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java index 646a4daee6f1..7eecf09f69b4 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/hedera/utils/GrpcUtils.java @@ -151,8 +151,6 @@ public static TransactionResponse submit( .deleteTopic(transaction); case ConsensusSubmitMessage -> clients.getConsSvcStub(nodeAccountId, false) .submitMessage(transaction); - case ConsensusApproveAllowance -> clients.getConsSvcStub(nodeAccountId, false) - .approveAllowance(transaction); case UncheckedSubmit -> clients.getNetworkSvcStub(nodeAccountId, false) .uncheckedSubmit(transaction); case TokenCreate -> clients.getTokenSvcStub(nodeAccountId, false).createToken(transaction); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java index d1ae183bd0ef..a936867ee309 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnFactory.java @@ -25,7 +25,6 @@ import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.HapiSpecSetup; import com.hedera.services.bdd.spec.utilops.mod.BodyMutation; -import com.hederahashgraph.api.proto.java.ConsensusApproveAllowanceTransactionBody; import com.hederahashgraph.api.proto.java.ConsensusCreateTopicTransactionBody; import com.hederahashgraph.api.proto.java.ConsensusDeleteTopicTransactionBody; import com.hederahashgraph.api.proto.java.ConsensusSubmitMessageTransactionBody; @@ -463,9 +462,4 @@ public Consumer defaultDefTokenClaimAi public Consumer defaultDefTokenAirdropTransactionBody() { return builder -> {}; } - - public Consumer - defaultDefConsensusApproveAllowanceTransactionBody() { - return builder -> {}; - } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java index d00e98933ace..b1dd836bf42e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/TxnVerbs.java @@ -48,7 +48,6 @@ import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.queries.crypto.ReferenceType; import com.hedera.services.bdd.spec.transactions.consensus.HapiMessageSubmit; -import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicApproveAllowance; import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicCreate; import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicDelete; import com.hedera.services.bdd.spec.transactions.consensus.HapiTopicUpdate; @@ -231,10 +230,6 @@ public static HapiMessageSubmit submitMessageTo(Function topi return new HapiMessageSubmit(topicFn); } - public static HapiTopicApproveAllowance approveTopicAllowance() { - return new HapiTopicApproveAllowance(); - } - /* FILE */ public static HapiFileCreate fileCreate(String fileName) { return new HapiFileCreate(fileName); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java deleted file mode 100644 index 2861b9054fdb..000000000000 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicApproveAllowance.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.services.bdd.spec.transactions.consensus; - -import static com.hedera.services.bdd.spec.transactions.TxnUtils.asId; -import static com.hedera.services.bdd.spec.transactions.TxnUtils.asTokenId; -import static com.hedera.services.bdd.spec.transactions.TxnUtils.asTopicId; - -import com.google.common.base.MoreObjects; -import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.transactions.HapiTxnOp; -import com.hederahashgraph.api.proto.java.ConsensusApproveAllowanceTransactionBody; -import com.hederahashgraph.api.proto.java.ConsensusCryptoFeeScheduleAllowance; -import com.hederahashgraph.api.proto.java.ConsensusTokenFeeScheduleAllowance; -import com.hederahashgraph.api.proto.java.HederaFunctionality; -import com.hederahashgraph.api.proto.java.Transaction; -import com.hederahashgraph.api.proto.java.TransactionBody; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -public class HapiTopicApproveAllowance extends HapiTxnOp { - private final List cryptoAllowances = new ArrayList<>(); - private final List tokenAllowances = new ArrayList<>(); - - public HapiTopicApproveAllowance() {} - - @Override - public HederaFunctionality type() { - return HederaFunctionality.ConsensusApproveAllowance; - } - - @Override - protected HapiTopicApproveAllowance self() { - return this; - } - - public HapiTopicApproveAllowance addCryptoAllowance( - String owner, String topic, long allowance, long allowancePerMessage) { - cryptoAllowances.add(CryptoAllowances.from(owner, topic, allowance, allowancePerMessage)); - return this; - } - - public HapiTopicApproveAllowance addTokenAllowance( - String owner, String token, String topic, long allowance, long allowancePerMessage) { - tokenAllowances.add(TokenAllowances.from(owner, token, topic, allowance, allowancePerMessage)); - return this; - } - - @Override - protected Consumer opBodyDef(HapiSpec spec) throws Throwable { - List callowances = new ArrayList<>(); - List tallowances = new ArrayList<>(); - calculateAllowances(spec, callowances, tallowances); - ConsensusApproveAllowanceTransactionBody opBody = spec.txns() - .body( - ConsensusApproveAllowanceTransactionBody.class, b -> { - b.addAllConsensusCryptoFeeScheduleAllowances(callowances); - b.addAllConsensusTokenFeeScheduleAllowances(tallowances); - }); - return b -> b.setConsensusApproveAllowance(opBody); - } - - @Override - protected long feeFor(HapiSpec spec, Transaction txn, int numPayerKeys) throws Throwable { - return 0; - } - - @Override - protected MoreObjects.ToStringHelper toStringHelper() { - return super.toStringHelper().add("cryptoAllowances", cryptoAllowances).add("tokenAllowances", tokenAllowances); - } - - // @Override - // protected void updateStateOf(HapiSpec spec) { - // // No state changes - // } - - private void calculateAllowances( - final HapiSpec spec, - final List callowances, - final List tallowances) { - for (var entry : cryptoAllowances) { - final var builder = ConsensusCryptoFeeScheduleAllowance.newBuilder() - .setOwner(asId(entry.owner(), spec)) - .setAmount(entry.amount()) - .setAmountPerMessage(entry.amountPerMessage()) - .setTopicId(asTopicId(entry.topic(), spec)); - callowances.add(builder.build()); - } - - for (var entry : tokenAllowances) { - final var builder = ConsensusTokenFeeScheduleAllowance.newBuilder() - .setOwner(asId(entry.owner, spec)) - .setTokenId(asTokenId(entry.token, spec)) - .setTopicId(asTopicId(entry.topic, spec)) - .setAmount(entry.amount) - .setAmountPerMessage(entry.amountPerMessage); - tallowances.add(builder.build()); - } - } - - private record CryptoAllowances(String owner, String topic, Long amount, Long amountPerMessage) { - static CryptoAllowances from(String owner, String topic, Long amount, Long amountPerMessage) { - return new CryptoAllowances(owner, topic, amount, amountPerMessage); - } - } - - private record TokenAllowances(String owner, String token, String topic, Long amount, Long amountPerMessage) { - static TokenAllowances from(String owner, String token, String topic, Long amount, Long amountPerMessage) { - return new TokenAllowances(owner, token, topic, amount, amountPerMessage); - } - } -} diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java index 03bafcf10f9c..6ce50e33f626 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java @@ -18,7 +18,6 @@ import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.approveTopicAllowance; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; @@ -276,31 +275,4 @@ final Stream createTopicWithCustomFeeAndDeletedCollector() { } } } - - @Nested - @DisplayName("Topic approve allowance") - class TopicApproveAllowance { - - @Nested - @DisplayName("Positive scenarios") - class ApproveAllowancePositiveScenarios { - - @BeforeAll - static void beforeAll(@NonNull final TestLifecycle lifecycle) { - lifecycle.doAdhoc(setupBaseKeys()); - } - - @HapiTest - @DisplayName("Approve crypto allowance for topic") - final Stream approveAllowance() { - return hapiTest( - cryptoCreate(OWNER), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY), - approveTopicAllowance().payingWith(OWNER).addCryptoAllowance(OWNER, TOPIC, 100, 10)); - } - } - } } From fa0445f0bfe0aa06e1b01acea73ee901fc18fd11 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 5 Dec 2024 16:27:13 +0200 Subject: [PATCH 55/94] Remove allowances Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 17 +- .../services/state/consensus/topic.proto | 19 ++- .../handlers/ConsensusCreateTopicHandler.java | 4 +- .../impl/util/ConsensusHandlerHelper.java | 4 +- .../handlers/ConsensusCreateTopicTest.java | 161 +++++++++++++++++- 5 files changed, 178 insertions(+), 27 deletions(-) diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 874363630958..3ca2ad54ce3a 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1598,25 +1598,20 @@ enum ResponseCodeEnum { * The provided fee exempt key list size exceeded the limit. */ MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED = 370; + /** * The provided fee exempt key list contains duplicated keys. */ FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS = 371; + /** * The provided fee exempt key list contains an invalid key. */ INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST = 372; /** - * Custom fee list is missing. - *

- * Fee exempt key list MAY be set only if custom fee list is set. - */ - MISSING_CUSTOM_FEES = 373; - - /** - * The provided fee schedule key contains an invalid key. - */ + * The provided fee schedule key contains an invalid key. + */ INVALID_FEE_SCHEDULE_KEY = 374; /** @@ -1625,8 +1620,4 @@ enum ResponseCodeEnum { */ FEE_SCHEDULE_KEY_CANNOT_BE_UPDATED = 375; - /** - * The topic has been marked as deleted. - */ - TOPIC_DELETED = 376; } diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 7aee87952a09..525436378e9c 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -110,17 +110,24 @@ message Topic { * If this field is unset, the current custom fees CANNOT be changed.
* If this field is set, that `Key` MUST sign any transaction to update * the custom fee schedule for this topic. - */ + */ Key fee_schedule_key = 11; /** - * A list of "privileged payer" keys. + * A set of "privileged payer" keys
+ * Keys in this list are permitted to submit messages to this topic without + * paying custom fees associated with this topic. *

- * If a submit transaction is signed by _any_ key from this list, + * If a submit transaction is signed by _any_ key included in this set, * custom fees SHALL NOT be charged for that transaction.
- * If fee_exempt_key_list is unset, it SHALL _implicitly_ contain - * the key `admin_key`, the key `submit_key`, and the key - * `fee_schedule_key`, if any of those keys are set. + * A `fee_exempt_key_list` MUST NOT contain more than + * `MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST` keys.
+ * A `fee_exempt_key_list` MUST NOT contain any duplicate keys.
+ * A `fee_exempt_key_list` MAY contain keys for accounts that are inactive, + * deleted, or non-existent. + * If not set, there SHALL NOT be any fee-exempt keys. In particular, the + * following keys SHALL NOT be implicitly or automatically added to this + * list: `adminKey`, `submitKey`, `fee_schedule_key`. */ repeated Key fee_exempt_key_list = 12; diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index 4a1482e0e134..df1eb57734a1 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -27,7 +27,6 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED; -import static com.hedera.hapi.node.base.ResponseCodeEnum.MISSING_CUSTOM_FEES; import static com.hedera.node.app.hapi.utils.fee.ConsensusServiceFeeBuilder.getConsensusCreateTopicFee; import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.RUNNING_HASH_BYTE_ARRAY_SIZE; import static com.hedera.node.app.spi.validation.AttributeValidator.isImmutableKey; @@ -207,7 +206,8 @@ private void validateSemantics( validateTrue( op.feeExemptKeyList().size() <= topicConfig.maxEntriesForFeeExemptKeyList(), MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED); - validateTrue(!op.customFees().isEmpty(), MISSING_CUSTOM_FEES); + // todo check if we need MISSING_CUSTOM_FEES + // validateTrue(!op.customFees().isEmpty(), MISSING_CUSTOM_FEES); op.feeExemptKeyList() .forEach(key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST)); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java index b228ebd4ff82..8f27416c100e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java @@ -17,7 +17,6 @@ package com.hedera.node.app.service.consensus.impl.util; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; -import static com.hedera.hapi.node.base.ResponseCodeEnum.TOPIC_DELETED; import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; @@ -55,7 +54,8 @@ public static Topic getIfUsable(@NonNull final TopicID topicId, @NonNull final R final var topic = topicStore.getTopic(topicId); validateTrue(topic != null, INVALID_TOPIC_ID); - validateFalse(topic.deleted(), TOPIC_DELETED); + // todo check if we need TOPIC_DELETED + validateFalse(topic.deleted(), INVALID_TOPIC_ID); return topic; } } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java index 20c31dddbb01..09234c87e2fd 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java @@ -17,6 +17,7 @@ package com.hedera.node.app.service.consensus.impl.test.handlers; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.TOPICS_KEY; import static com.hedera.node.app.service.consensus.impl.test.handlers.ConsensusTestUtils.SIMPLE_KEY_A; import static com.hedera.node.app.service.consensus.impl.test.handlers.ConsensusTestUtils.SIMPLE_KEY_B; @@ -28,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.doThrow; @@ -35,6 +37,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.TopicID; @@ -42,6 +45,8 @@ 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.FixedFee; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; @@ -49,6 +54,8 @@ import com.hedera.node.app.service.consensus.impl.records.ConsensusCreateTopicStreamBuilder; import com.hedera.node.app.service.consensus.impl.validators.ConsensusCustomFeesValidator; import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.ReadableTokenRelationStore; +import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; import com.hedera.node.app.spi.ids.EntityNumGenerator; import com.hedera.node.app.spi.metrics.StoreMetricsService; @@ -59,8 +66,11 @@ import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.config.api.Configuration; import java.time.Instant; +import java.util.ArrayList; +import java.util.List; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -75,6 +85,12 @@ class ConsensusCreateTopicTest extends ConsensusTestBase { @Mock private ReadableAccountStore accountStore; + @Mock + private ReadableTokenStore tokenStore; + + @Mock + private ReadableTokenRelationStore tokenRelationStore; + @Mock private AttributeValidator validator; @@ -98,6 +114,15 @@ class ConsensusCreateTopicTest extends ConsensusTestBase { private ConsensusCreateTopicHandler subject; private TransactionBody newCreateTxn(Key adminKey, Key submitKey, boolean hasAutoRenewAccount) { + return newCreateTxn(adminKey, submitKey, hasAutoRenewAccount, null, null); + } + + private TransactionBody newCreateTxn( + Key adminKey, + Key submitKey, + boolean hasAutoRenewAccount, + List customFees, + List feeExemptKeyList) { final var txnId = TransactionID.newBuilder().accountID(payerId).build(); final var createTopicBuilder = ConsensusCreateTopicTransactionBody.newBuilder(); if (adminKey != null) { @@ -111,6 +136,12 @@ private TransactionBody newCreateTxn(Key adminKey, Key submitKey, boolean hasAut if (hasAutoRenewAccount) { createTopicBuilder.autoRenewAccount(autoRenewId); } + if (customFees != null) { + createTopicBuilder.customFees(customFees); + } + if (feeExemptKeyList != null) { + createTopicBuilder.feeExemptKeyList(feeExemptKeyList); + } return TransactionBody.newBuilder() .transactionID(txnId) .consensusCreateTopic(createTopicBuilder.build()) @@ -125,9 +156,17 @@ void setUp() { .getOrCreateConfig(); topicStore = new WritableTopicStore(writableStates, config, storeMetricsService); given(handleContext.configuration()).willReturn(config); + given(handleContext.storeFactory().readableStore(ReadableTopicStore.class)) .willReturn(topicStore); given(storeFactory.writableStore(WritableTopicStore.class)).willReturn(topicStore); + given(handleContext.storeFactory().readableStore(ReadableAccountStore.class)) + .willReturn(accountStore); + given(handleContext.storeFactory().readableStore(ReadableTokenStore.class)) + .willReturn(tokenStore); + given(handleContext.storeFactory().readableStore(ReadableTokenRelationStore.class)) + .willReturn(tokenRelationStore); + given(handleContext.savepointStack()).willReturn(stack); given(stack.getBaseBuilder(ConsensusCreateTopicStreamBuilder.class)).willReturn(recordBuilder); lenient().when(handleContext.entityNumGenerator()).thenReturn(entityNumGenerator); @@ -333,7 +372,6 @@ void translatesInvalidExpiryException() { given(handleContext.expiryValidator()).willReturn(expiryValidator); given(expiryValidator.resolveCreationAttempt(anyBoolean(), any(), any())) .willThrow(new HandleException(ResponseCodeEnum.INVALID_EXPIRATION_TIME)); - final var msg = assertThrows(HandleException.class, () -> subject.handle(handleContext)); assertEquals(ResponseCodeEnum.AUTORENEW_DURATION_NOT_IN_RANGE, msg.getStatus()); } @@ -358,12 +396,11 @@ void doesntTranslateInvalidAutoRenewNum() { @Test @DisplayName("Memo Validation Failure will throw") void handleThrowsIfAttributeValidatorFails() { - final var adminKey = SIMPLE_KEY_A; - final var submitKey = SIMPLE_KEY_B; - final var txnBody = newCreateTxn(adminKey, submitKey, true); + final var txnBody = newCreateTxn(SIMPLE_KEY_A, SIMPLE_KEY_B, true); given(handleContext.body()).willReturn(txnBody); given(handleContext.attributeValidator()).willReturn(validator); + given(handleContext.expiryValidator()).willReturn(expiryValidator); doThrow(new HandleException(ResponseCodeEnum.MEMO_TOO_LONG)) .when(validator) @@ -441,6 +478,113 @@ void validatedAutoRenewAccount() { assertEquals(0, topicStore.modifiedTopics().size()); } + @Test + @DisplayName("Handle works as expected wit custom fees and FEKL") + void validatedCustomFees() { + final var customFees = List.of(ConsensusCustomFee.newBuilder() + .fixedFee(FixedFee.newBuilder().amount(1).build()) + .feeCollectorAccountId(AccountID.DEFAULT) + .build()); + final var feeExemptKeyList = List.of(SIMPLE_KEY_A, SIMPLE_KEY_B); + final var txnBody = newCreateTxn(adminKey, null, true, customFees, feeExemptKeyList); + given(handleContext.body()).willReturn(txnBody); + + given(accountStore.getAliasedAccountById(any())).willReturn(Account.DEFAULT); + + // mock validators + given(handleContext.consensusNow()).willReturn(Instant.ofEpochSecond(1_234_567L)); + given(handleContext.attributeValidator()).willReturn(validator); + given(handleContext.expiryValidator()).willReturn(expiryValidator); + given(entityNumGenerator.newEntityNum()).willReturn(1_234L); + given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); + final var op = txnBody.consensusCreateTopic(); + given(expiryValidator.resolveCreationAttempt(anyBoolean(), any(), any())) + .willReturn(new ExpiryMeta( + 1_234_567L + op.autoRenewPeriod().seconds(), + op.autoRenewPeriod().seconds(), + op.autoRenewAccount())); + + subject.handle(handleContext); + + final var createdTopic = + topicStore.getTopic(TopicID.newBuilder().topicNum(1_234L).build()); + assertNotNull(createdTopic); + + final var actualTopic = createdTopic; + assertEquals(0L, actualTopic.sequenceNumber()); + assertEquals(memo, actualTopic.memo()); + assertEquals(adminKey, actualTopic.adminKey()); + assertEquals(1_234_567L + op.autoRenewPeriod().seconds(), actualTopic.expirationSecond()); + assertEquals(op.autoRenewPeriod().seconds(), actualTopic.autoRenewPeriod()); + assertEquals(autoRenewId, actualTopic.autoRenewAccountId()); + final var topicID = TopicID.newBuilder().topicNum(1_234L).build(); + verify(recordBuilder).topicID(topicID); + assertNotNull(topicStore.getTopic(TopicID.newBuilder().topicNum(1_234L).build())); + } + + @Test + @DisplayName("Handle fail with toо many fee exempt keys") + void failWithTooManyFeeExemptKeys() { + final var feeExemptKeyList = buildFEKL(100); + final var txnBody = newCreateTxn(adminKey, null, true, null, feeExemptKeyList); + given(handleContext.body()).willReturn(txnBody); + + // mock validators + given(handleContext.consensusNow()).willReturn(Instant.ofEpochSecond(1_234_567L)); + given(handleContext.attributeValidator()).willReturn(validator); + given(handleContext.expiryValidator()).willReturn(expiryValidator); + + final var msg = assertThrows(HandleException.class, () -> subject.handle(handleContext)); + assertEquals(ResponseCodeEnum.MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED, msg.getStatus()); + assertEquals(0, topicStore.modifiedTopics().size()); + } + + @Test + @DisplayName("Handle fail with invalid custom fee amount") + void failWithInvalidFeeAmount() { + final var customFees = List.of(ConsensusCustomFee.newBuilder() + .fixedFee(FixedFee.newBuilder().amount(-1).build()) + .feeCollectorAccountId(AccountID.DEFAULT) + .build()); + final var txnBody = newCreateTxn(adminKey, null, true, customFees, null); + given(handleContext.body()).willReturn(txnBody); + + given(accountStore.getAliasedAccountById(any())).willReturn(Account.DEFAULT); + + // mock validators + given(handleContext.consensusNow()).willReturn(Instant.ofEpochSecond(1_234_567L)); + given(handleContext.attributeValidator()).willReturn(validator); + given(handleContext.expiryValidator()).willReturn(expiryValidator); + given(expiryValidator.expirationStatus(any(), anyBoolean(), anyLong())).willReturn(OK); + + final var msg = assertThrows(HandleException.class, () -> subject.handle(handleContext)); + assertEquals(ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE, msg.getStatus()); + assertEquals(0, topicStore.modifiedTopics().size()); + } + + @Test + @DisplayName("Handle fail with invalid collector") + void failWithInvalidCollector() { + final var customFees = List.of(ConsensusCustomFee.newBuilder() + .fixedFee(FixedFee.newBuilder().amount(1).build()) + .feeCollectorAccountId(AccountID.DEFAULT) + .build()); + final var txnBody = newCreateTxn(adminKey, null, true, customFees, null); + given(handleContext.body()).willReturn(txnBody); + + given(accountStore.getAliasedAccountById(any())).willReturn(null); + + // mock + given(handleContext.consensusNow()).willReturn(Instant.ofEpochSecond(1_234_567L)); + + given(handleContext.attributeValidator()).willReturn(validator); + given(handleContext.expiryValidator()).willReturn(expiryValidator); + + final var msg = assertThrows(HandleException.class, () -> subject.handle(handleContext)); + assertEquals(ResponseCodeEnum.INVALID_CUSTOM_FEE_COLLECTOR, msg.getStatus()); + assertEquals(0, topicStore.modifiedTopics().size()); + } + // Note: there are more tests in ConsensusCreateTopicHandlerParityTest.java private Key mockPayerLookup(Key key) { @@ -449,4 +593,13 @@ private Key mockPayerLookup(Key key) { given(accountStore.getAccountById(payerId)).willReturn(account); return key; } + + private List buildFEKL(int count) { + final var list = new ArrayList(); + for (int i = 0; i < count; i++) { + final var value = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + i; + list.add(Key.newBuilder().ed25519(Bytes.wrap(value.getBytes())).build()); + } + return list; + } } From ca60528ec50be3a1ae572e445d3a403f4488bdd4 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 6 Dec 2024 09:22:38 +0200 Subject: [PATCH 56/94] Remove allowances Signed-off-by: Zhivko Kelchev --- .../handlers/ConsensusCreateTopicHandler.java | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index df1eb57734a1..f791b3f6363e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -122,8 +122,8 @@ public void handle(@NonNull final HandleContext handleContext) { final var op = handleContext.body().consensusCreateTopicOrThrow(); final var topicStore = handleContext.storeFactory().writableStore(WritableTopicStore.class); + validateSemantics(op, handleContext); final var builder = new Topic.Builder(); - validateSemantics(op, handleContext, builder); final var impliedExpiry = handleContext.consensusNow().getEpochSecond() + op.autoRenewPeriodOrElse(Duration.DEFAULT).seconds(); @@ -173,8 +173,7 @@ public void handle(@NonNull final HandleContext handleContext) { } } - private void validateSemantics( - ConsensusCreateTopicTransactionBody op, HandleContext handleContext, Topic.Builder builder) { + private void validateSemantics(ConsensusCreateTopicTransactionBody op, HandleContext handleContext) { final var configuration = handleContext.configuration(); final var topicConfig = configuration.getConfigData(TopicsConfig.class); @@ -186,33 +185,25 @@ private void validateSemantics( // Validate admin and submit keys and set them. Empty key list is allowed and is used for immutable entities if (op.hasAdminKey() && !isImmutableKey(op.adminKey())) { handleContext.attributeValidator().validateKey(op.adminKey()); - builder.adminKey(op.adminKey()); } // submitKey() is not checked in preCheck() if (op.hasSubmitKey()) { handleContext.attributeValidator().validateKey(op.submitKey()); - builder.submitKey(op.submitKey()); } // validate hasFeeScheduleKey() if (op.hasFeeScheduleKey()) { handleContext.attributeValidator().validateKey(op.feeScheduleKey(), INVALID_CUSTOM_FEE_SCHEDULE_KEY); - builder.feeScheduleKey(op.feeScheduleKey()); } - // validate size of the list and the keys - if (!op.feeExemptKeyList().isEmpty()) { - validateTrue( - op.feeExemptKeyList().size() <= topicConfig.maxEntriesForFeeExemptKeyList(), - MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED); - // todo check if we need MISSING_CUSTOM_FEES - // validateTrue(!op.customFees().isEmpty(), MISSING_CUSTOM_FEES); - op.feeExemptKeyList() - .forEach(key -> - handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST)); - builder.feeExemptKeyList(op.feeExemptKeyList()); - } + // validate fee exempt key list + validateTrue( + op.feeExemptKeyList().size() <= topicConfig.maxEntriesForFeeExemptKeyList(), + MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED); + op.feeExemptKeyList() + .forEach( + key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST)); // validate custom fees if (!op.customFees().isEmpty()) { From d428b8c79ec6029427d63b153d1b76fa8a91a96b Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 6 Dec 2024 11:53:19 +0200 Subject: [PATCH 57/94] Revert ConsensusCreateTopicHandler Signed-off-by: Zhivko Kelchev --- .../handlers/ConsensusCreateTopicHandler.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index f791b3f6363e..a1e76cde51c9 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -123,7 +123,20 @@ public void handle(@NonNull final HandleContext handleContext) { final var topicStore = handleContext.storeFactory().writableStore(WritableTopicStore.class); validateSemantics(op, handleContext); + final var builder = new Topic.Builder(); + if (op.hasAdminKey() && !isImmutableKey(op.adminKey())) { + builder.adminKey(op.adminKey()); + } + if (op.hasSubmitKey()) { + builder.submitKey(op.submitKey()); + } + if (op.hasFeeScheduleKey()) { + builder.feeScheduleKey(op.feeScheduleKey()); + } + builder.feeExemptKeyList(op.feeExemptKeyList()); + builder.customFees(op.customFees()); + builder.memo(op.memo()); final var impliedExpiry = handleContext.consensusNow().getEpochSecond() + op.autoRenewPeriodOrElse(Duration.DEFAULT).seconds(); @@ -206,13 +219,9 @@ private void validateSemantics(ConsensusCreateTopicTransactionBody op, HandleCon key -> handleContext.attributeValidator().validateKey(key, INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST)); // validate custom fees - if (!op.customFees().isEmpty()) { - validateTrue( - op.customFees().size() <= topicConfig.maxCustomFeeEntriesForTopics(), CUSTOM_FEES_LIST_TOO_LONG); - customFeesValidator.validate( - accountStore, tokenRelStore, tokenStore, op.customFees(), handleContext.expiryValidator()); - builder.customFees(op.customFees()); - } + validateTrue(op.customFees().size() <= topicConfig.maxCustomFeeEntriesForTopics(), CUSTOM_FEES_LIST_TOO_LONG); + customFeesValidator.validate( + accountStore, tokenRelStore, tokenStore, op.customFees(), handleContext.expiryValidator()); /* Validate if the current topic can be created */ validateTrue( @@ -220,7 +229,6 @@ private void validateSemantics(ConsensusCreateTopicTransactionBody op, HandleCon /* Validate the topic memo */ handleContext.attributeValidator().validateMemo(op.memo()); - builder.memo(op.memo()); } @NonNull From 7d5da3a272978577d199534ecce3a77ea6a275a4 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 6 Dec 2024 11:56:36 +0200 Subject: [PATCH 58/94] Remove allowances Signed-off-by: Zhivko Kelchev --- .../app/service/consensus/ConsensusServiceDefinition.java | 8 +------- .../service/consensus/ConsensusServiceDefinitionTest.java | 3 +-- .../src/main/resources/genesis/throttles.json | 1 - 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java b/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java index 66ec9fb2884c..6071781e3196 100644 --- a/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java +++ b/hedera-node/hedera-consensus-service/src/main/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinition.java @@ -82,13 +82,7 @@ public final class ConsensusServiceDefinition implements RpcServiceDefinition { // topicRunningHash. // Request is [ConsensusSubmitMessageTransactionBody](#proto.ConsensusSubmitMessageTransactionBody) // - new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class), - // - // Approve allowance for custom fees. - // Set account allowances for a topic. This includes total allowance and allowance per message. - // Request is [ConsensusApproveAllowanceTransactionBody](#proto.ConsensusApproveAllowanceTransactionBody) - // - new RpcMethodDefinition<>("approveAllowance", Transaction.class, TransactionResponse.class)); + new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class)); private ConsensusServiceDefinition() { // Just something to keep the Gradle build believing we have a non-transitive diff --git a/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java b/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java index 9f7cb1725cee..77fd56a05dcb 100644 --- a/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java +++ b/hedera-node/hedera-consensus-service/src/test/java/com/hedera/node/app/service/consensus/ConsensusServiceDefinitionTest.java @@ -40,7 +40,6 @@ void methodsDefined() { new RpcMethodDefinition<>("updateTopic", Transaction.class, TransactionResponse.class), new RpcMethodDefinition<>("deleteTopic", Transaction.class, TransactionResponse.class), new RpcMethodDefinition<>("getTopicInfo", Query.class, Response.class), - new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class), - new RpcMethodDefinition<>("approveAllowance", Transaction.class, TransactionResponse.class)); + new RpcMethodDefinition<>("submitMessage", Transaction.class, TransactionResponse.class)); } } diff --git a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json index 76b451d98ed9..753f3c656931 100644 --- a/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json +++ b/hedera-node/hedera-file-service-impl/src/main/resources/genesis/throttles.json @@ -21,7 +21,6 @@ "ConsensusUpdateTopic", "ConsensusDeleteTopic", "ConsensusGetTopicInfo", - "ConsensusApproveAllowance", "TokenGetNftInfo", "TokenGetInfo", "ScheduleDelete", From e2227362e15e3d45ac5627a19a3d17996b01a74b Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 6 Dec 2024 13:21:20 +0200 Subject: [PATCH 59/94] Fix merge conflicts Signed-off-by: Zhivko Kelchev --- .../impl/handlers/ConsensusSubmitMessageHandler.java | 12 ++++++------ .../src/main/java/module-info.java | 6 ++---- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 66b05895678b..777614b2a5eb 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -29,6 +29,7 @@ import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.RECEIPT_STORAGE_TIME_SEC; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.TX_HASH_SIZE; import static com.hedera.node.app.spi.validation.Validations.mustExist; +import static com.hedera.node.app.spi.workflows.DispatchOptions.setupDispatch; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; @@ -47,6 +48,7 @@ import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusCustomFeeAssessor; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; +import com.hedera.node.app.service.token.records.CryptoTransferStreamBuilder; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.workflows.HandleContext; @@ -78,6 +80,7 @@ public class ConsensusSubmitMessageHandler implements TransactionHandler { * Running hash version */ public static final long RUNNING_HASH_VERSION = 3L; + private final ConsensusCustomFeeAssessor customFeeAssessor; /** @@ -155,15 +158,12 @@ public void handle(@NonNull final HandleContext handleContext) { final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); for (final var syntheticBody : syntheticBodies) { // dispatch crypto transfer - var record = handleContext.dispatchChildTransaction( + var record = handleContext.dispatch(setupDispatch( + handleContext.payer(), TransactionBody.newBuilder() .cryptoTransfer(syntheticBody) .build(), - ConsensusSubmitMessageStreamBuilder.class, - null, - handleContext.payer(), - HandleContext.TransactionCategory.CHILD, - HandleContext.ConsensusThrottling.OFF); + CryptoTransferStreamBuilder.class)); validateTrue(record.status().equals(SUCCESS), record.status()); } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java index dac938dbc7c6..363dd1e1a340 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java @@ -26,10 +26,8 @@ exports com.hedera.node.app.service.consensus.impl to com.hedera.node.app, com.hedera.node.test.clients; - exports com.hedera.node.app.service.consensus.impl.handlers to - com.hedera.node.app; - exports com.hedera.node.app.service.consensus.impl.handlers.customfee to - com.hedera.node.app; + exports com.hedera.node.app.service.consensus.impl.handlers; + exports com.hedera.node.app.service.consensus.impl.handlers.customfee; exports com.hedera.node.app.service.consensus.impl.records; exports com.hedera.node.app.service.consensus.impl.schemas; exports com.hedera.node.app.service.consensus.impl.validators; From a0446e06d3495b5968e10d9f88d5335f4bed2124 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 9 Dec 2024 12:55:45 +0200 Subject: [PATCH 60/94] Revert changes Signed-off-by: Zhivko Kelchev --- .../handlers/ConsensusDeleteTopicTest.java | 24 +++--- .../test/handlers/ConsensusTestUtils.java | 25 +++--- .../services/bdd/spec/keys/KeyFactory.java | 30 ++----- .../TopicCustomFeeSubmitMessageTest.java | 84 ------------------- 4 files changed, 33 insertions(+), 130 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java index 3b1b2fa5007f..aeb1abbf9843 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java @@ -191,16 +191,20 @@ void adminKeyDoesntExist() { final var txn = newDeleteTxn(); given(handleContext.body()).willReturn(txn); - topic = Topic.newBuilder() - .topicId(topicId) - .sequenceNumber(sequenceNumber) - .expirationSecond(expirationTime) - .autoRenewPeriod(autoRenewSecs) - .autoRenewAccountId(AccountID.newBuilder().accountNum(10L).build()) - .deleted(false) - .runningHash(Bytes.wrap(runningHash)) - .memo(memo) - .build(); + topic = new Topic( + topicId, + sequenceNumber, + expirationTime, + autoRenewSecs, + AccountID.newBuilder().accountNum(10L).build(), + false, + Bytes.wrap(runningHash), + memo, + null, + null, + null, + null, + null); writableTopicState = writableTopicStateWithOneKey(); given(writableStates.get(TOPICS_KEY)).willReturn(writableTopicState); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java index b0eb4a5f062e..d581bc0e3759 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java @@ -68,16 +68,19 @@ static void mockTopicLookup(Key adminKey, Key submitKey, ReadableTopicStore topi } static Topic newTopic(Key admin, Key submit) { - return Topic.newBuilder() - .topicId(TopicID.newBuilder().topicNum(123L).build()) - .sequenceNumber(-1L) - .expirationSecond(0L) - .autoRenewPeriod(-1L) - .autoRenewAccountId(AccountID.newBuilder().accountNum(1234567L).build()) - .deleted(false) - .memo("memo") - .adminKey(admin) - .submitKey(submit) - .build(); + return new Topic( + TopicID.newBuilder().topicNum(123L).build(), + -1L, + 0L, + -1L, + AccountID.newBuilder().accountNum(1234567L).build(), + false, + null, + "memo", + admin, + submit, + null, + null, + null); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java index 32fb0849ca67..b841e7fe0149 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/keys/KeyFactory.java @@ -665,32 +665,12 @@ private void signRecursively(final Key key, final SigControl controller) throws case SIG_ON: signIfNecessary(key); break; - case PREDEFINED: - signPredefinedNatureKey(key, controller); - break; default: - signCompositeKeys(key, controller); - } - } - - private void signPredefinedNatureKey(Key key, SigControl control) throws GeneralSecurityException { - // if key is composite sign recursively - if (key.hasKeyList() || key.hasThresholdKey()) { - signCompositeKeys(key, control); - return; - } - - // skip contract id keys - if (!(key.hasContractID() || key.hasDelegatableContractId())) { - signIfNecessary(key); - } - } - - private void signCompositeKeys(Key key, SigControl control) throws GeneralSecurityException { - final KeyList composite = TxnUtils.getCompositeList(key); - final SigControl[] childControls = control.getChildControls(); - for (int i = 0; i < childControls.length; i++) { - signRecursively(composite.getKeys(i), childControls[i]); + final KeyList composite = TxnUtils.getCompositeList(key); + final SigControl[] childControls = controller.getChildControls(); + for (int i = 0; i < childControls.length; i++) { + signRecursively(composite.getKeys(i), childControls[i]); + } } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index d01776c43e3b..7668a91c735b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -49,90 +49,6 @@ @HapiTestLifecycle @DisplayName("Submit message") public class TopicCustomFeeSubmitMessageTest extends TopicCustomFeeBase { - // @HapiTest - // @DisplayName("submit") - // final Stream submitMessage() { - // final var collector = "collector"; - // final var payer = "submitter"; - // final var treasury = "treasury"; - // final var token = "testToken"; - // final var secondToken = "secondToken"; - // final var denomToken = "denomToken"; - // final var simpleKey = "simpleKey"; - // final var simpleKey2 = "simpleKey2"; - // final var invalidKey = "invalidKey"; - // final var threshKey = "threshKey"; - // - // return hapiTest( - // // create keys - // newKeyNamed(invalidKey), - // newKeyNamed(simpleKey), - // newKeyNamed(simpleKey2), - // newKeyNamed(threshKey) - // .shape(threshOf(1, PREDEFINED_SHAPE, PREDEFINED_SHAPE) - // .signedWith(sigs(simpleKey2, simpleKey))), - // // create accounts and denomination token - // cryptoCreate(collector).balance(0L), - // cryptoCreate(payer).balance(ONE_HUNDRED_HBARS), - // cryptoCreate(treasury), - // tokenCreate(denomToken) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .initialSupply(500), - // tokenAssociate(collector, denomToken), - // tokenAssociate(payer, denomToken), - // tokenCreate(token) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .withCustom(fixedHtsFee(1, denomToken, collector)) - // .initialSupply(500), - // tokenCreate(secondToken) - // .treasury(treasury) - // .tokenType(TokenType.FUNGIBLE_COMMON) - // .initialSupply(500), - // tokenAssociate(collector, token, secondToken), - // tokenAssociate(payer, token, secondToken), - // cryptoTransfer( - // moving(2, token).between(treasury, payer), - // moving(1, secondToken).between(treasury, payer), - // moving(1, denomToken).between(treasury, payer)), - // - // // create topic with custom fees - // createTopic(TOPIC) - // // .withConsensusCustomFee(fixedConsensusHtsFee(1, - // token, - // // collector)) - // // .withConsensusCustomFee(fixedConsensusHtsFee(1, - // secondToken, - // // collector)) - // .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) - // .feeExemptKeys(threshKey) - // .hasKnownStatus(SUCCESS), - // - // // add allowance - // approveTopicAllowance() - // .payingWith(payer) - // .addCryptoAllowance(payer, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR), - // - // // submit message - // submitMessageTo(TOPIC) - // .message("TEST") - // .signedBy(invalidKey, payer) - // .payingWith(payer) - // .via("submit"), - // - // // check records - // getTxnRecord("submit").andAllChildRecords().logged(), - // - // // assert balances - // getAccountBalance(collector).hasTinyBars(ONE_HBAR)); - // // .hasTokenBalance(token, 2) - // // .hasTokenBalance(denomToken,1) - // // .hasTokenBalance(secondToken, 1), - // // getAccountBalance(payer) - // // .hasTokenBalance(token, 0) - // // .hasTokenBalance(secondToken, 0)); - // } @Nested @DisplayName("Positive scenarios") From db637afaa66e75eeefd4422ff7734e8e9355caf4 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 9 Dec 2024 13:48:37 +0200 Subject: [PATCH 61/94] Fix dispatching of child transfers Signed-off-by: Zhivko Kelchev --- .../impl/handlers/ConsensusSubmitMessageHandler.java | 8 +++++--- .../handlers/customfee/ConsensusCustomFeeAssessor.java | 2 -- .../suites/hip991/TopicCustomFeeSubmitMessageTest.java | 7 ------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 777614b2a5eb..8b291b677bfd 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -29,10 +29,11 @@ import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.RECEIPT_STORAGE_TIME_SEC; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.TX_HASH_SIZE; import static com.hedera.node.app.spi.validation.Validations.mustExist; -import static com.hedera.node.app.spi.workflows.DispatchOptions.setupDispatch; +import static com.hedera.node.app.spi.workflows.DispatchOptions.stepDispatch; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck; import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; +import static com.hedera.node.app.spi.workflows.record.StreamBuilder.TransactionCustomizer.NOOP_TRANSACTION_CUSTOMIZER; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; @@ -158,12 +159,13 @@ public void handle(@NonNull final HandleContext handleContext) { final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); for (final var syntheticBody : syntheticBodies) { // dispatch crypto transfer - var record = handleContext.dispatch(setupDispatch( + var record = handleContext.dispatch(stepDispatch( handleContext.payer(), TransactionBody.newBuilder() .cryptoTransfer(syntheticBody) .build(), - CryptoTransferStreamBuilder.class)); + CryptoTransferStreamBuilder.class, + NOOP_TRANSACTION_CUSTOMIZER)); validateTrue(record.status().equals(SUCCESS), record.status()); } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index e17711c42b7b..572e5a3e4a86 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -27,7 +27,6 @@ import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; -import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.config.data.LedgerConfig; @@ -56,7 +55,6 @@ public List assessCustomFee(Topic topic, HandleCo final List transactionBodies = new ArrayList<>(); final var payer = context.payer(); - final var topicStore = context.storeFactory().writableStore(WritableTopicStore.class); final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 7668a91c735b..f8e2ed998380 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -67,9 +67,6 @@ final Stream messageSubmitToPublicTopicWithFee1Hbar() { return hapiTest( cryptoCreate(collector).balance(0L), createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - // approveTopicAllowance() - // .addCryptoAllowance(SUBMITTER, TOPIC, ONE_HUNDRED_HBARS, ONE_HBAR) - // .payingWith(SUBMITTER), submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTinyBars(ONE_HBAR)); } @@ -389,10 +386,6 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { createTopic(TOPIC) .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)) .withConsensusCustomFee(fixedConsensusHtsFee(1, SECOND_TOKEN, secondCollector)), - // approveTopicAllowance() - // .addTokenAllowance(collector, BASE_TOKEN, TOPIC, 1, 1) - // .addTokenAllowance(collector, SECOND_TOKEN, TOPIC, 1, 1) - // .payingWith(collector), submitMessageTo(TOPIC).message("TEST").payingWith(collector), // only second fee should be paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), From d1b17148451fb19cafac0e2ea2370241d861c462 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 9 Dec 2024 15:02:10 +0200 Subject: [PATCH 62/94] fix unit tests Signed-off-by: Zhivko Kelchev --- .../ConsensusSubmitMessageHandlerTest.java | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index d6979f4a7d06..9bb631e4433d 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -29,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Answers.RETURNS_SELF; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.BDDMockito.given; @@ -57,7 +58,9 @@ import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; +import com.hedera.node.app.spi.key.KeyVerifier; import com.hedera.node.app.spi.metrics.StoreMetricsService; +import com.hedera.node.app.spi.signatures.SignatureVerification; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -86,6 +89,12 @@ class ConsensusSubmitMessageHandlerTest extends ConsensusTestBase { @Mock(strictness = LENIENT) private HandleContext.SavepointStack stack; + @Mock + private KeyVerifier keyVerifier; + + @Mock + private SignatureVerification signatureVerification; + private ConsensusSubmitMessageHandler subject; @BeforeEach @@ -186,6 +195,8 @@ void handleWorksAsExpected() { given(handleContext.consensusNow()).willReturn(consensusTimestamp); + mockPayerKeyIsFeeExempt(); + final var initialTopic = writableTopicState.get(topicId); subject.handle(handleContext); @@ -209,7 +220,7 @@ public Topic updateRunningHashAndSequenceNumber( throw new IOException(); } }; - + mockPayerKeyIsFeeExempt(); final var txn = newSubmitMessageTxn(topicEntityNum, ""); given(handleContext.body()).willReturn(txn); @@ -228,6 +239,8 @@ void handleWorksAsExpectedIfConsensusTimeIsNull() { given(handleContext.consensusNow()).willReturn(null); + mockPayerKeyIsFeeExempt(); + final var initialTopic = writableTopicState.get(topicId); subject.handle(handleContext); @@ -414,4 +427,16 @@ private TransactionBody newSubmitMessageTxnWithChunksAndPayer( } private final ByteString NONSENSE = ByteString.copyFromUtf8("NONSENSE"); + + private void mockPayerKeyIsFeeExempt() { + given(handleContext.keyVerifier()).willReturn(keyVerifier); + given(keyVerifier.verificationFor(any(Key.class))).willReturn(signatureVerification); + given(signatureVerification.passed()).willReturn(true); + } + + private void mockPayerKeyIsNotFeeExempt() { + given(handleContext.keyVerifier()).willReturn(keyVerifier); + given(keyVerifier.verificationFor(any(Key.class))).willReturn(signatureVerification); + given(signatureVerification.passed()).willReturn(false); + } } From d472977aa0f053da618b3725408f78ad422f2b6e Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Dec 2024 11:10:18 +0200 Subject: [PATCH 63/94] Add max_custom_fees and accept_all_custom_fees Signed-off-by: Zhivko Kelchev --- .../services/consensus_submit_message.proto | 11 + .../customfee/ConsensusCustomFeeAssessor.java | 25 ++ .../consensus/HapiMessageSubmit.java | 20 ++ .../TopicCustomFeeSubmitMessageTest.java | 105 ++++--- .../bdd/suites/hip991/TopicCustomFeeTest.java | 278 ------------------ 5 files changed, 117 insertions(+), 322 deletions(-) delete mode 100644 hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java diff --git a/hapi/hedera-protobufs/services/consensus_submit_message.proto b/hapi/hedera-protobufs/services/consensus_submit_message.proto index 8ba19396d0e6..87ed9bbb784c 100644 --- a/hapi/hedera-protobufs/services/consensus_submit_message.proto +++ b/hapi/hedera-protobufs/services/consensus_submit_message.proto @@ -27,6 +27,7 @@ option java_package = "com.hederahashgraph.api.proto.java"; option java_multiple_files = true; import "basic_types.proto"; +import "custom_fees.proto"; /** * UNDOCUMENTED @@ -66,4 +67,14 @@ message ConsensusSubmitMessageTransactionBody { * Optional information of the current chunk in a fragmented message. */ ConsensusMessageChunkInfo chunkInfo = 3; + + /** + * The maximum custom fee that the user is willing to pay for the message. This field will be ignored if `accept_all_custom_fees` is set to `true`. + */ + repeated FixedFee max_custom_fees = 4; + + /** + * If set to true, the transaction will accept all custom fees from the topic id + */ + bool accept_all_custom_fees = 5; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 572e5a3e4a86..33d1ebe41640 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -16,10 +16,12 @@ package com.hedera.node.app.service.consensus.impl.handlers.customfee; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; @@ -54,6 +56,7 @@ public ConsensusCustomFeeAssessor() { public List assessCustomFee(Topic topic, HandleContext context) { final List transactionBodies = new ArrayList<>(); + final var op = context.body().consensusSubmitMessageOrThrow(); final var payer = context.payer(); final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); @@ -75,6 +78,28 @@ public List assessCustomFee(Topic topic, HandleCo } final var fixedFee = fee.fixedFeeOrThrow(); + + if (!op.acceptAllCustomFees()) { + // validate limits + boolean passed = false; + if (fixedFee.hasDenominatingTokenId()) { + for (FixedFee feeLimit : op.maxCustomFees()) { + if (feeLimit.hasDenominatingTokenId() + && feeLimit.denominatingTokenId().equals(fixedFee.denominatingTokenId()) + && feeLimit.amount() <= fixedFee.amount()) { + passed = true; + } + } + } else { + for (FixedFee feeLimit : op.maxCustomFees()) { + if (!feeLimit.hasDenominatingTokenId() && feeLimit.amount() <= fixedFee.amount()) { + passed = true; + } + } + } + validateTrue(passed, ResponseCodeEnum.FAIL_FEE); + } + if (fixedFee.hasDenominatingTokenId()) { final var tokenId = fixedFee.denominatingTokenIdOrThrow(); final var tokenTreasury = tokenStore.get(tokenId).treasuryAccountIdOrThrow(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java index 6921fe4f9fdd..f5bf861d029b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java @@ -31,6 +31,7 @@ import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.fees.AdapterUtils; import com.hedera.services.bdd.spec.transactions.HapiTxnOp; +import com.hederahashgraph.api.proto.java.ConsensusCustomFee; import com.hederahashgraph.api.proto.java.ConsensusMessageChunkInfo; import com.hederahashgraph.api.proto.java.ConsensusSubmitMessageTransactionBody; import com.hederahashgraph.api.proto.java.FeeData; @@ -40,6 +41,7 @@ import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionID; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -56,6 +58,8 @@ public class HapiMessageSubmit extends HapiTxnOp { private Optional initialTransactionPayer = Optional.empty(); private Optional initialTransactionID = Optional.empty(); private boolean clearMessage = false; + private final List> maxCustomFeeList = new ArrayList<>(); + private boolean acceptAllCustomFees = false; public HapiMessageSubmit(final String topic) { this.topic = Optional.ofNullable(topic); @@ -121,6 +125,16 @@ public HapiMessageSubmit chunkInfo( return chunkInfo(totalChunks, chunkNumber); } + public HapiMessageSubmit maxCustomFee(Function f) { + maxCustomFeeList.add(f); + return this; + } + + public HapiMessageSubmit acceptAllCustomFees(boolean acceptAllFees) { + this.acceptAllCustomFees = acceptAllFees; + return this; + } + @Override protected Consumer opBodyDef(final HapiSpec spec) throws Throwable { final TopicID id = resolveTopicId(spec); @@ -132,6 +146,12 @@ protected Consumer opBodyDef(final HapiSpec spec) throw if (clearMessage) { b.clearMessage(); } + if (!maxCustomFeeList.isEmpty()) { + for (final var supplier : maxCustomFeeList) { + b.addMaxCustomFees(supplier.apply(spec).getFixedFee()); + } + } + b.setAcceptAllCustomFees(acceptAllCustomFees); if (totalChunks.isPresent() && chunkNumber.isPresent()) { final ConsensusMessageChunkInfo chunkInfo = ConsensusMessageChunkInfo.newBuilder() .setInitialTransactionID(initialTransactionID.orElse(asTransactionID( diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index f8e2ed998380..80a7d7240835 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -19,6 +19,7 @@ import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; @@ -27,8 +28,10 @@ import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; +import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHollow; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; import static com.hedera.services.bdd.suites.HapiSuite.ONE_HUNDRED_HBARS; import static com.hedera.services.bdd.suites.HapiSuite.flattened; @@ -64,10 +67,11 @@ static void beforeAll(@NonNull final TestLifecycle lifecycle) { // TOPIC_FEE_104 final Stream messageSubmitToPublicTopicWithFee1Hbar() { final var collector = "collector"; + final var fee = fixedConsensusHbarFee(ONE_HBAR, collector); return hapiTest( cryptoCreate(collector).balance(0L), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + createTopic(TOPIC).withConsensusCustomFee(fee), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTinyBars(ONE_HBAR)); } @@ -76,11 +80,12 @@ final Stream messageSubmitToPublicTopicWithFee1Hbar() { // TOPIC_FEE_105 final Stream messageSubmitToPublicTopicWithFee1token() { final var collector = "collector"; + final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); return hapiTest( cryptoCreate(collector), tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + createTopic(TOPIC).withConsensusCustomFee(fee), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); } @@ -92,15 +97,16 @@ final Stream messageSubmitToPublicTopicWith3layerFee() { final var token = "token"; final var denomToken = DENOM_TOKEN_PREFIX + token; final var tokenFeeCollector = COLLECTOR_PREFIX + token; + final var fee = fixedConsensusHtsFee(1, token, topicFeeCollector); return hapiTest(flattened( // create denomination token and transfer it to the submitter createTokenWith2LayerFee(SUBMITTER, token, true), // create topic with multilayer fee cryptoCreate(topicFeeCollector).balance(0L), tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + createTopic(TOPIC).withConsensusCustomFee(fee), // submit message - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), // assert token fee collector balance @@ -120,7 +126,8 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() associateAllTokensToCollectors(), // create topic with 10 multilayer fees - 9 HTS + 1 HBAR createTopicWith10Different2layerFees(), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + // todo check if accept all here is OK, or we should create all 10 fees limits + submitMessageTo(TOPIC).acceptAllCustomFees(true).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance assertAllCollectorsBalances())); } @@ -168,16 +175,16 @@ final Stream treasurySubmitToPublicTopicWith3layerFees() { final var token = "token"; final var denomToken = DENOM_TOKEN_PREFIX + token; final var tokenFeeCollector = COLLECTOR_PREFIX + token; - + final var fee = fixedConsensusHtsFee(1, token, topicFeeCollector); return hapiTest(flattened( // create denomination token and transfer it to the submitter createTokenWith2LayerFee(SUBMITTER, token, true), // create topic with multilayer fee cryptoCreate(topicFeeCollector).balance(0L), tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + createTopic(TOPIC).withConsensusCustomFee(fee), // submit message - submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(TOKEN_TREASURY), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), // assert token fee collector balance @@ -193,6 +200,7 @@ final Stream treasuryOfSecondLayerSubmitToPublic() { final var token = "token"; final var denomToken = DENOM_TOKEN_PREFIX + token; final var topicFeeCollector = "topicFeeCollector"; + final var fee = fixedConsensusHtsFee(1, token, topicFeeCollector); return hapiTest(flattened( // create token and transfer it to the submitter @@ -205,10 +213,10 @@ final Stream treasuryOfSecondLayerSubmitToPublic() { // create topic cryptoCreate(topicFeeCollector).balance(0L), tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + createTopic(TOPIC).withConsensusCustomFee(fee), // submit - submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(DENOM_TREASURY), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), @@ -225,7 +233,7 @@ final Stream collectorSubmitToPublicTopicWith3layerFees() { final var token = "token"; final var denomToken = DENOM_TOKEN_PREFIX + token; final var topicFeeCollector = "topicFeeCollector"; - + final var fee = fixedConsensusHtsFee(1, token, topicFeeCollector); return hapiTest(flattened( // create token and transfer it to the submitter createTokenWith2LayerFee(SUBMITTER, token, true), @@ -235,10 +243,10 @@ final Stream collectorSubmitToPublicTopicWith3layerFees() { tokenAssociate(topicFeeCollector, token), // create topic - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + createTopic(TOPIC).withConsensusCustomFee(fee), // submit - submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(topicFeeCollector), // assert balances getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), @@ -253,7 +261,7 @@ final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFee final var denomToken = DENOM_TOKEN_PREFIX + token; final var secondLayerFeeCollector = COLLECTOR_PREFIX + token; final var topicFeeCollector = "topicFeeCollector"; - + final var fee = fixedConsensusHtsFee(1, token, topicFeeCollector); return hapiTest(flattened( // create token and transfer it to the submitter createTokenWith2LayerFee(SUBMITTER, token, true), @@ -265,10 +273,10 @@ final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFee // create topic cryptoCreate(topicFeeCollector).balance(0L), tokenAssociate(topicFeeCollector, token), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, token, topicFeeCollector)), + createTopic(TOPIC).withConsensusCustomFee(fee), // submit - submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(secondLayerFeeCollector), // assert topic fee collector balance - only first layer fee should be paid getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), @@ -283,6 +291,7 @@ final Stream anotherCollectorSubmitMessageToATopicWithAFee() { final var collector = "collector"; final var anotherToken = "anotherToken"; final var anotherCollector = COLLECTOR_PREFIX + anotherToken; + final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); return hapiTest(flattened( // create another token with fixed fee createTokenWith2LayerFee(SUBMITTER, anotherToken, true), @@ -293,8 +302,8 @@ final Stream anotherCollectorSubmitMessageToATopicWithAFee() { // create topic cryptoCreate(collector), tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), + createTopic(TOPIC).withConsensusCustomFee(fee), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(anotherCollector), // the fee was paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1))); } @@ -304,11 +313,12 @@ final Stream anotherCollectorSubmitMessageToATopicWithAFee() { // TOPIC_FEE_116 final Stream messageTopicSubmitToHollowAccountAsFeeCollector() { final var collector = "collector"; + final var fee = fixedConsensusHbarFee(ONE_HBAR, collector); return hapiTest( // create hollow account with ONE_HUNDRED_HBARS createHollow(1, i -> collector), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), + createTopic(TOPIC).withConsensusCustomFee(fee), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), // collector should be still a hollow account // and should have the initial balance + ONE_HBAR fee @@ -322,14 +332,15 @@ final Stream messageTopicSubmitToHollowAccountAsFeeCollector() { final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { final var collector = "collector"; final var feeScheduleKey = "feeScheduleKey"; + final var fee = fixedConsensusHbarFee(ONE_HBAR, collector); return hapiTest( newKeyNamed(feeScheduleKey), cryptoCreate(collector).balance(0L), createTopic(TOPIC) .feeScheduleKeyName(feeScheduleKey) .feeExemptKeys(feeScheduleKey) - .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - submitMessageTo(TOPIC).message("TEST").signedByPayerAnd(feeScheduleKey), + .withConsensusCustomFee(fee), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").signedByPayerAnd(feeScheduleKey), getAccountBalance(collector).hasTinyBars(0L)); } @@ -338,11 +349,12 @@ final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { // TOPIC_FEE_125 final Stream collectorSubmitMessageToTopicWithFTFee() { final var collector = "collector"; + final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)), - submitMessageTo(TOPIC).message("TEST").payingWith(collector), + createTopic(TOPIC).withConsensusCustomFee(fee), + submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(collector), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); } @@ -351,22 +363,23 @@ final Stream collectorSubmitMessageToTopicWithFTFee() { // TOPIC_FEE_126 final Stream collectorSubmitMessageToTopicWithHbarFee() { final var collector = "collector"; + final var fee = fixedConsensusHbarFee(ONE_HBAR, collector); return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), - createTopic(TOPIC).withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit")); - // assert collector's tinyBars balance - // withOpContext((spec, log) -> { - // final var submitTxnRecord = getTxnRecord("submit"); - // final var allowanceTxnRecord = getTxnRecord("approveAllowance"); - // allRunFor(spec, submitTxnRecord, allowanceTxnRecord); - // final var transactionTxnFee = - // submitTxnRecord.getResponseRecord().getTransactionFee(); - // final var allowanceTxnFee = - // allowanceTxnRecord.getResponseRecord().getTransactionFee(); - // getAccountBalance(collector) - // .hasTinyBars(ONE_HUNDRED_HBARS - transactionTxnFee - allowanceTxnFee); - // })); + createTopic(TOPIC).withConsensusCustomFee(fee), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .message("TEST") + .payingWith(collector) + .via("submit"), + // assert collector's tinyBars balance + withOpContext((spec, log) -> { + final var submitTxnRecord = getTxnRecord("submit"); + allRunFor(spec, submitTxnRecord); + final var transactionTxnFee = + submitTxnRecord.getResponseRecord().getTransactionFee(); + getAccountBalance(collector).hasTinyBars(ONE_HUNDRED_HBARS - transactionTxnFee); + })); } @HapiTest @@ -374,6 +387,8 @@ final Stream collectorSubmitMessageToTopicWithHbarFee() { final Stream collectorSubmitMessageToTopicWith2differentFees() { final var collector = "collector"; final var secondCollector = "secondCollector"; + final var fee1 = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var fee2 = fixedConsensusHtsFee(1, SECOND_TOKEN, secondCollector); return hapiTest( // todo create and associate collector in beforeAll() cryptoCreate(collector).balance(ONE_HBAR), @@ -383,10 +398,12 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { tokenAssociate(secondCollector, SECOND_TOKEN), cryptoTransfer(moving(1, SECOND_TOKEN).between(SUBMITTER, collector)), // create topic with two fees - createTopic(TOPIC) - .withConsensusCustomFee(fixedConsensusHtsFee(1, BASE_TOKEN, collector)) - .withConsensusCustomFee(fixedConsensusHtsFee(1, SECOND_TOKEN, secondCollector)), - submitMessageTo(TOPIC).message("TEST").payingWith(collector), + createTopic(TOPIC).withConsensusCustomFee(fee1).withConsensusCustomFee(fee2), + submitMessageTo(TOPIC) + .maxCustomFee(fee1) + .maxCustomFee(fee2) + .message("TEST") + .payingWith(collector), // only second fee should be paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), getAccountBalance(secondCollector).hasTokenBalance(SECOND_TOKEN, 1L)); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java deleted file mode 100644 index 6ce50e33f626..000000000000 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeTest.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.services.bdd.suites.hip991; - -import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; -import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoCreate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoDelete; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; -import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenCreate; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHTSFee; -import static com.hedera.services.bdd.spec.transactions.token.CustomFeeTests.expectedConsensusFixedHbarFee; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.newKeyNamed; -import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext; -import static com.hedera.services.bdd.suites.HapiSuite.ONE_HBAR; -import static com.hedera.services.bdd.suites.HapiSuite.flattened; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.ACCOUNT_DELETED; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.CUSTOM_FEE_MUST_BE_POSITIVE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CUSTOM_FEE_SCHEDULE_KEY; - -import com.hedera.services.bdd.junit.HapiTest; -import com.hedera.services.bdd.junit.HapiTestLifecycle; -import com.hedera.services.bdd.junit.support.TestLifecycle; -import com.hedera.services.bdd.spec.keys.KeyShape; -import com.hederahashgraph.api.proto.java.TokenType; -import edu.umd.cs.findbugs.annotations.NonNull; -import java.util.List; -import java.util.stream.Stream; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.DynamicTest; -import org.junit.jupiter.api.Nested; - -@HapiTestLifecycle -@DisplayName("Topic custom fees") -public class TopicCustomFeeTest extends TopicCustomFeeBase { - - @Nested - @DisplayName("Topic create") - class TopicCreate { - - @Nested - @DisplayName("Positive scenarios") - class TopicCreatePositiveScenarios { - - @BeforeAll - static void beforeAll(@NonNull final TestLifecycle lifecycle) { - lifecycle.doAdhoc(setupBaseKeys()); - } - - @HapiTest - @DisplayName("Create topic with all keys") - // TOPIC_FEE_001 - final Stream createTopicWithAllKeys() { - return hapiTest(flattened( - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY), - getTopicInfo(TOPIC) - .hasAdminKey(ADMIN_KEY) - .hasSubmitKey(SUBMIT_KEY) - .hasFeeScheduleKey(FEE_SCHEDULE_KEY))); - } - - @HapiTest - @DisplayName("Create topic with submitKey and feeScheduleKey") - // TOPIC_FEE_002 - final Stream createTopicWithSubmitKeyAndFeeScheduleKey() { - return hapiTest( - createTopic(TOPIC).submitKeyName(SUBMIT_KEY).feeScheduleKeyName(FEE_SCHEDULE_KEY), - getTopicInfo(TOPIC).hasSubmitKey(SUBMIT_KEY).hasFeeScheduleKey(FEE_SCHEDULE_KEY)); - } - - @HapiTest - @DisplayName("Create topic with only feeScheduleKey") - // TOPIC_FEE_003 - final Stream createTopicWithOnlyFeeScheduleKey() { - return hapiTest( - createTopic(TOPIC).feeScheduleKeyName(FEE_SCHEDULE_KEY), - getTopicInfo(TOPIC).hasFeeScheduleKey(FEE_SCHEDULE_KEY)); - } - - @HapiTest - @DisplayName("Create topic with ECDSA feeScheduleKey") - // TOPIC_FEE_005 - final Stream createTopicWithECDSAFeeScheduleKey() { - return hapiTest( - createTopic(TOPIC).feeScheduleKeyName(FEE_SCHEDULE_KEY_ECDSA), - getTopicInfo(TOPIC).hasFeeScheduleKey(FEE_SCHEDULE_KEY_ECDSA)); - } - - @HapiTest - @DisplayName("Create topic with threshold feeScheduleKey") - // TOPIC_FEE_006 - final Stream createTopicWithThresholdFeeScheduleKey() { - final var threshKey = "threshKey"; - return hapiTest( - newKeyNamed(threshKey).shape(KeyShape.threshOf(1, KeyShape.SIMPLE, KeyShape.SIMPLE)), - createTopic(TOPIC).feeScheduleKeyName(threshKey), - getTopicInfo(TOPIC).hasFeeScheduleKey(threshKey)); - } - - @HapiTest - @DisplayName("Create topic with 1 Hbar fixed fee") - // TOPIC_FEE_008 - final Stream createTopicWithOneHbarFixedFee() { - final var collector = "collector"; - return hapiTest( - cryptoCreate(collector), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)), - getTopicInfo(TOPIC) - .hasAdminKey(ADMIN_KEY) - .hasSubmitKey(SUBMIT_KEY) - .hasFeeScheduleKey(FEE_SCHEDULE_KEY) - .hasCustomFee(expectedConsensusFixedHbarFee(ONE_HBAR, collector))); - } - - @HapiTest - @DisplayName("Create topic with 1 HTS fixed fee") - // TOPIC_FEE_009 - final Stream createTopicWithOneHTSFixedFee() { - final var collector = "collector"; - return hapiTest( - cryptoCreate(collector), - tokenCreate("testToken") - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(500), - tokenAssociate(collector, "testToken"), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .withConsensusCustomFee(fixedConsensusHtsFee(1, "testToken", collector)), - getTopicInfo(TOPIC) - .hasAdminKey(ADMIN_KEY) - .hasSubmitKey(SUBMIT_KEY) - .hasFeeScheduleKey(FEE_SCHEDULE_KEY) - .hasCustomFee(expectedConsensusFixedHTSFee(1, "testToken", collector))); - } - - @HapiTest - @DisplayName("Create topic with 10 keys in FEKL") - // TOPIC_FEE_022 - final Stream createTopicWithFEKL() { - final var collector = "collector"; - return hapiTest(flattened( - // create 10 keys - newNamedKeysForFEKL(10), - cryptoCreate(collector), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .withConsensusCustomFee(fixedConsensusHbarFee(5, collector)) - // set list of 10 keys - .feeExemptKeys(feeExemptKeyNames(10)), - getTopicInfo(TOPIC) - .hasAdminKey(ADMIN_KEY) - .hasSubmitKey(SUBMIT_KEY) - .hasFeeScheduleKey(FEE_SCHEDULE_KEY) - // assert the list - .hasFeeExemptKeys(List.of(feeExemptKeyNames(10))))); - } - } - - @Nested - @DisplayName("Negative scenarios") - class TopicCreateNegativeScenarios { - - @BeforeAll - static void beforeAll(@NonNull final TestLifecycle lifecycle) { - lifecycle.doAdhoc(setupBaseKeys()); - } - - @HapiTest - @DisplayName("Create topic with duplicated signatures in FEKL") - // TOPIC_FEE_029 - final Stream createTopicWithDuplicateSignatures() { - final var testKey = "testKey"; - return hapiTest(flattened( - newKeyNamed(testKey), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .feeExemptKeys(testKey, testKey) - .hasPrecheck(FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS))); - } - - @HapiTest - @DisplayName("Create topic with 0 Hbar fixed fee") - // TOPIC_FEE_030 - final Stream createTopicWithZeroHbarFixedFee() { - final var collector = "collector"; - return hapiTest( - cryptoCreate(collector), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .withConsensusCustomFee(fixedConsensusHbarFee(0, collector)) - .hasKnownStatus(CUSTOM_FEE_MUST_BE_POSITIVE)); - } - - @HapiTest - @DisplayName("Create topic with 0 HTS fixed fee") - // TOPIC_FEE_031 - final Stream createTopicWithZeroHTSFixedFee() { - final var collector = "collector"; - return hapiTest( - cryptoCreate(collector), - tokenCreate("testToken") - .tokenType(TokenType.FUNGIBLE_COMMON) - .initialSupply(500), - tokenAssociate(collector, "testToken"), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .withConsensusCustomFee(fixedConsensusHtsFee(0, "testToken", collector)) - .hasKnownStatus(CUSTOM_FEE_MUST_BE_POSITIVE)); - } - - @HapiTest - @DisplayName("Create topic with invalid fee schedule key") - // TOPIC_FEE_032 - final Stream createTopicWithInvalidFeeScheduleKey() { - final var invalidKey = "invalidKey"; - return hapiTest( - withOpContext((spec, opLog) -> spec.registry().saveKey(invalidKey, STRUCTURALLY_INVALID_KEY)), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(invalidKey) - .hasKnownStatus(INVALID_CUSTOM_FEE_SCHEDULE_KEY)); - } - - @HapiTest - @DisplayName("Create topic with custom fee and deleted collector") - // TOPIC_FEE_033 - final Stream createTopicWithCustomFeeAndDeletedCollector() { - final var collector = "collector"; - return hapiTest( - cryptoCreate(collector), - cryptoDelete(collector), - createTopic(TOPIC) - .adminKeyName(ADMIN_KEY) - .submitKeyName(SUBMIT_KEY) - .feeScheduleKeyName(FEE_SCHEDULE_KEY) - .withConsensusCustomFee(fixedConsensusHbarFee(ONE_HBAR, collector)) - .hasKnownStatus(ACCOUNT_DELETED)); - } - } - } -} From 2570e43d08e94a63faa1175cdb6b80a825397372 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Dec 2024 13:27:25 +0200 Subject: [PATCH 64/94] Revert unnecessary changes Signed-off-by: Zhivko Kelchev --- .../impl/util/ConsensusHandlerHelper.java | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java deleted file mode 100644 index 8f27416c100e..000000000000 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/util/ConsensusHandlerHelper.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2024 Hedera Hashgraph, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hedera.node.app.service.consensus.impl.util; - -import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; -import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; -import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; -import static java.util.Objects.requireNonNull; - -import com.hedera.hapi.node.base.TopicID; -import com.hedera.hapi.node.state.consensus.Topic; -import com.hedera.node.app.service.consensus.ReadableTopicStore; -import com.hedera.node.app.spi.workflows.HandleException; -import edu.umd.cs.findbugs.annotations.NonNull; - -/** - * Class for retrieving objects in a certain context. For example, during a {@code handler.handle(...)} call. - * This allows compartmentalizing common validation logic without requiring store implementations to - * throw inappropriately-contextual exceptions, and also abstracts duplicated business logic out of - * multiple handlers. - */ -public class ConsensusHandlerHelper { - - private ConsensusHandlerHelper() { - throw new UnsupportedOperationException("Utility class only"); - } - - /** - * Returns the topic if it exists and is usable. A {@link HandleException} is thrown if the topic is invalid. - * - * @param topicId the ID of the topic to get - * @param topicStore the {@link ReadableTopicStore} to use for topic retrieval - * @return the topic if it exists and is usable - * @throws HandleException if any of the topic conditions are not met - */ - @NonNull - public static Topic getIfUsable(@NonNull final TopicID topicId, @NonNull final ReadableTopicStore topicStore) { - requireNonNull(topicId); - requireNonNull(topicStore); - - final var topic = topicStore.getTopic(topicId); - validateTrue(topic != null, INVALID_TOPIC_ID); - // todo check if we need TOPIC_DELETED - validateFalse(topic.deleted(), INVALID_TOPIC_ID); - return topic; - } -} From ae381ba8fc387f81dd7c744d25dd79c68a873584 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Dec 2024 14:27:35 +0200 Subject: [PATCH 65/94] Refactor custom fee validations Signed-off-by: Zhivko Kelchev --- .../ConsensusSubmitMessageHandler.java | 79 ++++++++++++++++--- .../customfee/ConsensusCustomFeeAssessor.java | 23 ------ .../TopicCustomFeeSubmitMessageTest.java | 6 +- 3 files changed, 70 insertions(+), 38 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 8b291b677bfd..c22ad92d1216 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -38,11 +38,15 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.Key; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.SubType; import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.consensus.ConsensusSubmitMessageTransactionBody; import com.hedera.hapi.node.state.consensus.Topic; +import com.hedera.hapi.node.transaction.ConsensusCustomFee; +import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.hapi.utils.CommonPbjConverters; import com.hedera.node.app.service.consensus.ReadableTopicStore; @@ -52,6 +56,7 @@ import com.hedera.node.app.service.token.records.CryptoTransferStreamBuilder; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; +import com.hedera.node.app.spi.key.KeyVerifier; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -68,6 +73,7 @@ import java.security.NoSuchAlgorithmException; import java.time.Instant; import java.util.HashSet; +import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; @@ -144,22 +150,15 @@ public void handle(@NonNull final HandleContext handleContext) { validateTransaction(txn, config, topic); /* handle custom fees */ - // check if payer is fee exempt - var payerIsFeeExempted = false; - if (!topic.feeExemptKeyList().isEmpty()) { - for (final var key : topic.feeExemptKeyList()) { - final var keyVerificationResult = handleContext.keyVerifier().verificationFor(key); - if (keyVerificationResult.passed()) { - payerIsFeeExempted = true; - } + if (!topic.customFees().isEmpty() && !isFeeExempted(topic.feeExemptKeyList(), handleContext.keyVerifier())) { + // check payer limits or throw + if (!op.acceptAllCustomFees()) { + validateFeeLimits(topic.customFees(), op.maxCustomFees()); } - } - if (!topic.customFees().isEmpty() && !payerIsFeeExempted) { - // validate and create synthetic body + // create synthetic body and dispatch crypto transfer final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); for (final var syntheticBody : syntheticBodies) { - // dispatch crypto transfer - var record = handleContext.dispatch(stepDispatch( + final var record = handleContext.dispatch(stepDispatch( handleContext.payer(), TransactionBody.newBuilder() .cryptoTransfer(syntheticBody) @@ -322,6 +321,60 @@ public static byte[] noThrowSha384HashOf(final byte[] byteArray) { } } + /** + * Check if the submit message transaction is fee exempt + * + * @param feeExemptKeyList The list of keys that are exempt from fees + * @param keyVerifier The key verifier of this transaction + * @return if the transaction is fee exempt + */ + private boolean isFeeExempted(@NonNull final List feeExemptKeyList, @NonNull final KeyVerifier keyVerifier) { + if (!feeExemptKeyList.isEmpty()) { + for (final var key : feeExemptKeyList) { + final var keyVerificationResult = keyVerifier.verificationFor(key); + if (keyVerificationResult.passed()) { + return true; + } + } + } + return false; + } + + /** + * Validate that each topic custom fee has equal or lower value than the payer's limit + * + * @param topicCustomFees The topic's custom fee list + * @param payerCustomFeeLimits List with limits of fees that the payer is willing to pay + */ + private void validateFeeLimits( + @NonNull final List topicCustomFees, + @NonNull final List payerCustomFeeLimits) { + for (final ConsensusCustomFee fee : topicCustomFees) { + // validate limits + boolean passed = false; + final var fixedFee = fee.fixedFeeOrThrow(); + if (fixedFee.hasDenominatingTokenId()) { + for (FixedFee feeLimit : payerCustomFeeLimits) { + if (feeLimit.hasDenominatingTokenId() + && feeLimit.denominatingTokenId().equals(fixedFee.denominatingTokenId()) + && feeLimit.amount() <= fixedFee.amount()) { + passed = true; + break; + } + } + } else { + for (FixedFee feeLimit : payerCustomFeeLimits) { + if (!feeLimit.hasDenominatingTokenId() && feeLimit.amount() <= fixedFee.amount()) { + passed = true; + break; + } + } + } + // if any fee amount is larger than the corresponding limit, validation should fail + validateTrue(passed, ResponseCodeEnum.FAIL_FEE); + } + } + @NonNull @Override public Fees calculateFees(@NonNull final FeeContext feeContext) { diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 33d1ebe41640..f2588cab794d 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -16,12 +16,10 @@ package com.hedera.node.app.service.consensus.impl.handlers.customfee; -import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; @@ -79,27 +77,6 @@ public List assessCustomFee(Topic topic, HandleCo final var fixedFee = fee.fixedFeeOrThrow(); - if (!op.acceptAllCustomFees()) { - // validate limits - boolean passed = false; - if (fixedFee.hasDenominatingTokenId()) { - for (FixedFee feeLimit : op.maxCustomFees()) { - if (feeLimit.hasDenominatingTokenId() - && feeLimit.denominatingTokenId().equals(fixedFee.denominatingTokenId()) - && feeLimit.amount() <= fixedFee.amount()) { - passed = true; - } - } - } else { - for (FixedFee feeLimit : op.maxCustomFees()) { - if (!feeLimit.hasDenominatingTokenId() && feeLimit.amount() <= fixedFee.amount()) { - passed = true; - } - } - } - validateTrue(passed, ResponseCodeEnum.FAIL_FEE); - } - if (fixedFee.hasDenominatingTokenId()) { final var tokenId = fixedFee.denominatingTokenIdOrThrow(); final var tokenTreasury = tokenStore.get(tokenId).treasuryAccountIdOrThrow(); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 80a7d7240835..649129679997 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -126,8 +126,10 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() associateAllTokensToCollectors(), // create topic with 10 multilayer fees - 9 HTS + 1 HBAR createTopicWith10Different2layerFees(), - // todo check if accept all here is OK, or we should create all 10 fees limits - submitMessageTo(TOPIC).acceptAllCustomFees(true).message("TEST").payingWith(SUBMITTER), + submitMessageTo(TOPIC) + .acceptAllCustomFees(true) + .message("TEST") + .payingWith(SUBMITTER), // assert topic fee collector balance assertAllCollectorsBalances())); } From 07056916c6d5ac5eedce1ad941da7a302a35d69e Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 10 Dec 2024 16:19:07 +0200 Subject: [PATCH 66/94] Cleanup Signed-off-by: Zhivko Kelchev --- .../impl/handlers/customfee/ConsensusCustomFeeAssessor.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index f2588cab794d..934749a35889 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -62,7 +62,7 @@ public List assessCustomFee(Topic topic, HandleCo final var tokenTransfers = new ArrayList(); List hbarTransfers = new ArrayList<>(); // we need to count the number of balance adjustments, - // and if needed to split custom fee transfers in to two separate dispatches + // and if needed to split custom fee transfers in to separate dispatches // todo: add explanation for maxTransfers final var maxTransfers = ledgerConfig.transfersMaxLen() / 3; var transferCounts = 0; @@ -83,12 +83,8 @@ public List assessCustomFee(Topic topic, HandleCo if (context.payer().equals(tokenTreasury)) { continue; } - - // todo after removing allowances - check limits tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); - } else { - // todo after removing allowances - check limits hbarTransfers = mergeTransfers( hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); } From 51c71837883d962adba4a3cea0a0ff14e10b0953 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Wed, 11 Dec 2024 11:19:06 +0200 Subject: [PATCH 67/94] add CUSTOM_FEES_LIMIT_EXCEEDED status code Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 27 +++++++++++-------- .../ConsensusSubmitMessageHandler.java | 2 +- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index 605497803637..c7af077dd809 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1613,29 +1613,34 @@ enum ResponseCodeEnum { MISSING_EXPIRY_TIME = 372; /** - * The provided fee exempt key list size exceeded the limit. - */ + * The provided fee exempt key list size exceeded the limit. + */ MAX_ENTRIES_FOR_FEE_EXEMPT_KEY_LIST_EXCEEDED = 373; /** - * The provided fee exempt key list contains duplicated keys. - */ + * The provided fee exempt key list contains duplicated keys. + */ FEE_EXEMPT_KEY_LIST_CONTAINS_DUPLICATED_KEYS = 374; /** - * The provided fee exempt key list contains an invalid key. - */ + * The provided fee exempt key list contains an invalid key. + */ INVALID_KEY_IN_FEE_EXEMPT_KEY_LIST = 375; /** - * The provided fee schedule key contains an invalid key. - */ + * The provided fee schedule key contains an invalid key. + */ INVALID_FEE_SCHEDULE_KEY = 376; /** - * If a fee schedule key is not set when we create a topic - * we cannot add it on update. - */ + * If a fee schedule key is not set when we create a topic + * we cannot add it on update. + */ FEE_SCHEDULE_KEY_CANNOT_BE_UPDATED = 377; + /** + * The fee amount is exceeding the amount that the payer + * is willing to pay. + */ + CUSTOM_FEES_LIMIT_EXCEEDED = 378; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index c22ad92d1216..7454cde5e3a3 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -371,7 +371,7 @@ private void validateFeeLimits( } } // if any fee amount is larger than the corresponding limit, validation should fail - validateTrue(passed, ResponseCodeEnum.FAIL_FEE); + validateTrue(passed, ResponseCodeEnum.CUSTOM_FEES_LIMIT_EXCEEDED); } } From 5a6f72c85b406289f0bfe777f1751965ec9447d2 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Wed, 11 Dec 2024 15:49:55 +0200 Subject: [PATCH 68/94] Simplify the fee assessment Signed-off-by: Zhivko Kelchev --- .../customfee/ConsensusCustomFeeAssessor.java | 132 +++++------------- 1 file changed, 32 insertions(+), 100 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 934749a35889..3099c742eddd 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -16,10 +16,11 @@ package com.hedera.node.app.service.consensus.impl.handlers.customfee; -import static java.util.Objects.requireNonNull; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; @@ -29,14 +30,9 @@ import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.spi.workflows.HandleContext; -import com.hedera.node.config.data.LedgerConfig; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -51,69 +47,53 @@ public ConsensusCustomFeeAssessor() { // Needed for Dagger injection } - public List assessCustomFee(Topic topic, HandleContext context) { + /** + * Build and return a list of synthetic crypto transfer transaction bodies, that represents custom fees payments. + * It will return one body per topic custom fee. + * + * @param topic The topic + * @param context The transaction handle context + * @return List of synthetic crypto transfer transaction bodies + */ + public List assessCustomFee( + @NonNull final Topic topic, @NonNull final HandleContext context) { final List transactionBodies = new ArrayList<>(); - - final var op = context.body().consensusSubmitMessageOrThrow(); final var payer = context.payer(); final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); - final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class); - - final var tokenTransfers = new ArrayList(); - List hbarTransfers = new ArrayList<>(); - // we need to count the number of balance adjustments, - // and if needed to split custom fee transfers in to separate dispatches - // todo: add explanation for maxTransfers - final var maxTransfers = ledgerConfig.transfersMaxLen() / 3; - var transferCounts = 0; - // build crypto transfer body for the first layer of custom fees, - // if there is a second layer it will be assessed in crypto transfer handler + // build crypto transfer bodies for the first layer of custom fees, + // if there is a second or third layer it will be assessed in crypto transfer handler for (ConsensusCustomFee fee : topic.customFees()) { - // check if payer is treasury or collector + + // check if payer is collector if (context.payer().equals(fee.feeCollectorAccountId())) { continue; } - final var fixedFee = fee.fixedFeeOrThrow(); + final var tokenTransfers = new ArrayList(); + List hbarTransfers = new ArrayList<>(); + final var fixedFee = fee.fixedFeeOrThrow(); if (fixedFee.hasDenominatingTokenId()) { final var tokenId = fixedFee.denominatingTokenIdOrThrow(); - final var tokenTreasury = tokenStore.get(tokenId).treasuryAccountIdOrThrow(); + final var tokenTreasury = getTokenTreasury(tokenId, tokenStore); + // check if payer is treasury if (context.payer().equals(tokenTreasury)) { continue; } tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); } else { - hbarTransfers = mergeTransfers( - hbarTransfers, buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); + hbarTransfers = buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee); } - transferCounts++; - - if (transferCounts == maxTransfers) { - final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); - transactionBodies.add(syntheticBodyBuilder - .transfers( - TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) - .build()); - - // reset lists and counter - transferCounts = 0; - tokenTransfers.clear(); - hbarTransfers.clear(); - } + // build the synthetic body + final var syntheticBodyBuilder = + CryptoTransferTransactionBody.newBuilder().tokenTransfers(tokenTransfers); + transactionBodies.add(syntheticBodyBuilder + .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) + .build()); } - if (tokenTransfers.isEmpty() && hbarTransfers.isEmpty()) { - return transactionBodies; - } - - final var syntheticBodyBuilder = tokenTransfers(tokenTransfers.toArray(TokenTransferList[]::new)); - transactionBodies.add(syntheticBodyBuilder - .transfers(TransferList.newBuilder().accountAmounts(hbarTransfers.toArray(AccountAmount[]::new))) - .build()); - return transactionBodies; } @@ -144,57 +124,9 @@ private TokenTransferList buildCustomFeeTokenTransferList(AccountID payer, Accou .build(); } - private CryptoTransferTransactionBody.Builder tokenTransfers(@NonNull TokenTransferList... tokenTransferLists) { - if (repeatsTokenId(tokenTransferLists)) { - final Map consolidatedTokenTransfers = new LinkedHashMap<>(); - for (final var tokenTransferList : tokenTransferLists) { - consolidatedTokenTransfers.merge( - tokenTransferList.tokenOrThrow(), - tokenTransferList, - ConsensusCustomFeeAssessor::mergeTokenTransferLists); - } - tokenTransferLists = consolidatedTokenTransfers.values().toArray(TokenTransferList[]::new); - } - return CryptoTransferTransactionBody.newBuilder().tokenTransfers(tokenTransferLists); - } - - private static TokenTransferList mergeTokenTransferLists( - @NonNull final TokenTransferList from, @NonNull final TokenTransferList to) { - return from.copyBuilder() - .transfers(mergeTransfers(from.transfers(), to.transfers())) - .build(); - } - - private static List mergeTransfers( - @NonNull final List from, @NonNull final List to) { - requireNonNull(from); - requireNonNull(to); - final Map consolidated = new LinkedHashMap<>(); - consolidateInto(consolidated, from); - consolidateInto(consolidated, to); - return new ArrayList<>(consolidated.values()); - } - - private static void consolidateInto( - @NonNull final Map consolidated, @NonNull final List transfers) { - for (final var transfer : transfers) { - consolidated.merge(transfer.accountID(), transfer, ConsensusCustomFeeAssessor::mergeAdjusts); - } - } - - private static AccountAmount mergeAdjusts(@NonNull final AccountAmount from, @NonNull final AccountAmount to) { - return from.copyBuilder() - .amount(from.amount() + to.amount()) - .isApproval(from.isApproval() || to.isApproval()) - .build(); - } - - private boolean repeatsTokenId(@NonNull final TokenTransferList[] tokenTransferList) { - return tokenTransferList.length > 1 - && Arrays.stream(tokenTransferList) - .map(TokenTransferList::token) - .collect(Collectors.toSet()) - .size() - < tokenTransferList.length; + private AccountID getTokenTreasury(TokenID tokenId, ReadableTokenStore tokenStore) { + final var token = tokenStore.get(tokenId); + validateTrue(token != null, ResponseCodeEnum.INVALID_TOKEN_ID_IN_CUSTOM_FEES); + return token.treasuryAccountIdOrThrow(); } } From 4a9f444bf92638e97ec5582c50dcc2e301a7cd16 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Wed, 11 Dec 2024 17:48:52 +0200 Subject: [PATCH 69/94] Add unit test Signed-off-by: Zhivko Kelchev --- .../customfee/ConsensusCustomFeeAssessor.java | 4 +- .../ConsensusSubmitMessageHandlerTest.java | 56 ++++++++++++++++++- .../impl/test/handlers/ConsensusTestBase.java | 12 +++- 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 3099c742eddd..19825f5b7b24 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -18,6 +18,7 @@ import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import com.google.common.annotations.VisibleForTesting; import com.hedera.hapi.node.base.AccountAmount; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ResponseCodeEnum; @@ -124,7 +125,8 @@ private TokenTransferList buildCustomFeeTokenTransferList(AccountID payer, Accou .build(); } - private AccountID getTokenTreasury(TokenID tokenId, ReadableTokenStore tokenStore) { + @VisibleForTesting + public AccountID getTokenTreasury(TokenID tokenId, ReadableTokenStore tokenStore) { final var token = tokenStore.get(tokenId); validateTrue(token != null, ResponseCodeEnum.INVALID_TOKEN_ID_IN_CUSTOM_FEES); return token.treasuryAccountIdOrThrow(); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index 9bb631e4433d..00ea43aa1bb2 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -18,6 +18,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_MESSAGE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.service.consensus.impl.ConsensusServiceImpl.TOPICS_KEY; import static com.hedera.node.app.service.consensus.impl.handlers.ConsensusSubmitMessageHandler.RUNNING_HASH_VERSION; import static com.hedera.node.app.service.consensus.impl.handlers.ConsensusSubmitMessageHandler.noThrowSha384HashOf; @@ -34,7 +35,9 @@ import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import com.google.protobuf.ByteString; @@ -53,6 +56,7 @@ import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusCustomFeeAssessor; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.records.CryptoTransferStreamBuilder; import com.hedera.node.app.spi.fees.FeeCalculator; import com.hedera.node.app.spi.fees.FeeCalculatorFactory; import com.hedera.node.app.spi.fees.FeeContext; @@ -61,6 +65,7 @@ import com.hedera.node.app.spi.key.KeyVerifier; import com.hedera.node.app.spi.metrics.StoreMetricsService; import com.hedera.node.app.spi.signatures.SignatureVerification; +import com.hedera.node.app.spi.workflows.DispatchOptions; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -95,12 +100,18 @@ class ConsensusSubmitMessageHandlerTest extends ConsensusTestBase { @Mock private SignatureVerification signatureVerification; + @Mock + private CryptoTransferStreamBuilder streamBuilder; + + private ConsensusCustomFeeAssessor customFeeAssessor; + private ConsensusSubmitMessageHandler subject; @BeforeEach void setUp() { commonSetUp(); - subject = new ConsensusSubmitMessageHandler(new ConsensusCustomFeeAssessor()); + customFeeAssessor = spy(new ConsensusCustomFeeAssessor()); + subject = new ConsensusSubmitMessageHandler(customFeeAssessor); final var config = HederaTestConfigBuilder.create() .withValue("consensus.message.maxBytesAllowed", 100) @@ -362,6 +373,28 @@ void calculateFeesHappyPath() { verify(feeCalc).addNetworkRamByteSeconds(10080); } + @Test + @DisplayName("Handle submit to topic with custom fee works as expected") + void handleWorksAsExpectedWithCustomFee() { + givenValidTopic(); + + final var txn = newSubmitMessageTxnWithMaxFee(); + given(handleContext.body()).willReturn(txn); + given(handleContext.consensusNow()).willReturn(consensusTimestamp); + + mockPayerKeyIsNotFeeExempt(); + + final var initialTopic = writableTopicState.get(topicId); + subject.handle(handleContext); + + final var expectedTopic = writableTopicState.get(topicId); + assertNotEquals(initialTopic, expectedTopic); + assertEquals(initialTopic.sequenceNumber() + 1, expectedTopic.sequenceNumber()); + assertNotEquals( + initialTopic.runningHash().toString(), + expectedTopic.runningHash().toString()); + } + /* ----------------- Helper Methods ------------------- */ private Key mockPayerLookup() { @@ -389,6 +422,19 @@ private TransactionBody newSubmitMessageTxn(final long topicEntityNum, final Str .build(); } + private TransactionBody newSubmitMessageTxnWithMaxFee() { + final var txnId = TransactionID.newBuilder().accountID(payerId).build(); + final var submitMessageBuilder = ConsensusSubmitMessageTransactionBody.newBuilder() + .maxCustomFees(tokenCustomFee.fixedFee(), hbarCustomFee.fixedFee()) + .topicID(TopicID.newBuilder().topicNum(topicEntityNum).build()) + .message(Bytes.wrap("Message for test-" + Instant.now() + "." + + Instant.now().getNano())); + return TransactionBody.newBuilder() + .transactionID(txnId) + .consensusSubmitMessage(submitMessageBuilder.build()) + .build(); + } + private TransactionBody newDefaultSubmitMessageTxn() { return newSubmitMessageTxnWithoutId( "Message for test-" + Instant.now() + "." + Instant.now().getNano()); @@ -429,12 +475,20 @@ private TransactionBody newSubmitMessageTxnWithChunksAndPayer( private final ByteString NONSENSE = ByteString.copyFromUtf8("NONSENSE"); private void mockPayerKeyIsFeeExempt() { + // mock signature is in FEKL given(handleContext.keyVerifier()).willReturn(keyVerifier); given(keyVerifier.verificationFor(any(Key.class))).willReturn(signatureVerification); given(signatureVerification.passed()).willReturn(true); } private void mockPayerKeyIsNotFeeExempt() { + // mock payer and token processing + doReturn(anotherPayer).when(customFeeAssessor).getTokenTreasury(any(), any()); + given(handleContext.payer()).willReturn(payerId); + // mock crypto transfer dispatch results + given(handleContext.dispatch(any(DispatchOptions.class))).willReturn(streamBuilder); + given(streamBuilder.status()).willReturn(SUCCESS); + // mock signature is not in FEKL given(handleContext.keyVerifier()).willReturn(keyVerifier); given(keyVerifier.verificationFor(any(Key.class))).willReturn(signatureVerification); given(signatureVerification.passed()).willReturn(false); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java index 09e14fc71c3f..10bc7a9b34dd 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java @@ -124,10 +124,18 @@ 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 customFees = List.of(ConsensusCustomFee.newBuilder() + protected final ConsensusCustomFee tokenCustomFee = ConsensusCustomFee.newBuilder() + .fixedFee(FixedFee.newBuilder() + .denominatingTokenId(fungibleTokenId) + .amount(1) + .build()) + .feeCollectorAccountId(anotherPayer) + .build(); + protected final ConsensusCustomFee hbarCustomFee = ConsensusCustomFee.newBuilder() .fixedFee(FixedFee.newBuilder().amount(1).build()) .feeCollectorAccountId(anotherPayer) - .build()); + .build(); + protected final List customFees = List.of(tokenCustomFee, hbarCustomFee); protected Topic topic; From b54829540347cbdc8c9a07730185ea93e164b05a Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 12 Dec 2024 10:54:29 +0200 Subject: [PATCH 70/94] fix module-info.java Signed-off-by: Zhivko Kelchev --- .../hedera-consensus-service-impl/src/main/java/module-info.java | 1 + 1 file changed, 1 insertion(+) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java index 363dd1e1a340..1b976759ab26 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java @@ -18,6 +18,7 @@ requires com.hedera.node.app.service.token.impl; requires com.hedera.node.config; requires org.apache.logging.log4j; + requires com.google.common; requires static com.github.spotbugs.annotations; provides com.hedera.node.app.service.consensus.ConsensusService with From 5f0acd2a0690eeb55fa4b8b42ebadcb09684234b Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 12 Dec 2024 11:10:34 +0200 Subject: [PATCH 71/94] fix module-info.java Signed-off-by: Zhivko Kelchev --- .../src/main/java/module-info.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java index 1b976759ab26..a5a294ada5d5 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/module-info.java @@ -17,8 +17,8 @@ requires com.hedera.node.app.hapi.utils; requires com.hedera.node.app.service.token.impl; requires com.hedera.node.config; - requires org.apache.logging.log4j; requires com.google.common; + requires org.apache.logging.log4j; requires static com.github.spotbugs.annotations; provides com.hedera.node.app.service.consensus.ConsensusService with From 7ddd522f85b936cba1b404fd7b867e97db95cf42 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 12 Dec 2024 13:09:03 +0200 Subject: [PATCH 72/94] fix limit comparison Signed-off-by: Zhivko Kelchev --- .../impl/handlers/ConsensusSubmitMessageHandler.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 7454cde5e3a3..e1e80af0a71e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -357,14 +357,14 @@ private void validateFeeLimits( for (FixedFee feeLimit : payerCustomFeeLimits) { if (feeLimit.hasDenominatingTokenId() && feeLimit.denominatingTokenId().equals(fixedFee.denominatingTokenId()) - && feeLimit.amount() <= fixedFee.amount()) { + && feeLimit.amount() >= fixedFee.amount()) { passed = true; break; } } } else { for (FixedFee feeLimit : payerCustomFeeLimits) { - if (!feeLimit.hasDenominatingTokenId() && feeLimit.amount() <= fixedFee.amount()) { + if (!feeLimit.hasDenominatingTokenId() && feeLimit.amount() >= fixedFee.amount()) { passed = true; break; } From 29dfbba0f1001a48b10c54486c797abd18f7b195 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 13 Dec 2024 10:21:38 +0200 Subject: [PATCH 73/94] revert HapiTopicCreate changes Signed-off-by: Zhivko Kelchev --- .../bdd/spec/transactions/consensus/HapiTopicCreate.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java index aea3351f50e7..de3cdef09607 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java @@ -66,7 +66,7 @@ public class HapiTopicCreate extends HapiTxnOp { private Optional feeScheduleKeyShape = Optional.empty(); private final List> feeScheduleSuppliers = new ArrayList<>(); private Optional>> feeExemptKeyNamesList = Optional.empty(); - private Optional> freeMesssageKeyList = Optional.empty(); + private Optional> feeExemptKeyList = Optional.empty(); /** For some test we need the capability to build transaction has no autoRenewPeiord */ private boolean clearAutoRenewPeriod = false; @@ -176,7 +176,7 @@ protected Consumer opBodyDef(final HapiSpec spec) throw autoRenewAccountId.ifPresent(id -> b.setAutoRenewAccount(asId(id, spec))); autoRenewPeriod.ifPresent(secs -> b.setAutoRenewPeriod(asDuration(secs))); feeScheduleKey.ifPresent(b::setFeeScheduleKey); - freeMesssageKeyList.ifPresent(keys -> keys.forEach(b::addFeeExemptKeyList)); + feeExemptKeyList.ifPresent(keys -> keys.forEach(b::addFeeExemptKeyList)); if (!feeScheduleSuppliers.isEmpty()) { for (final var supplier : feeScheduleSuppliers) { b.addCustomFees(supplier.apply(spec)); @@ -202,7 +202,7 @@ private void genKeysFor(final HapiSpec spec) { feeScheduleKey = Optional.of(netOf(spec, feeScheduleKeyName, feeScheduleKeyShape)); } - feeExemptKeyNamesList.ifPresent(functions -> freeMesssageKeyList = Optional.of(functions.stream() + feeExemptKeyNamesList.ifPresent(functions -> feeExemptKeyList = Optional.of(functions.stream() .map(f -> f.apply(spec)) .filter(k -> k != null && k != Key.getDefaultInstance()) .collect(toList()))); From 44f98424383c2d35c0042c9894dd8f37c9e5113f Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 16 Dec 2024 14:09:13 +0200 Subject: [PATCH 74/94] Fee limits additional validations Signed-off-by: Zhivko Kelchev --- .../services/response_code.proto | 14 +++- .../ConsensusSubmitMessageHandler.java | 64 +++++++++++++++---- .../TopicCustomFeeSubmitMessageTest.java | 27 ++++++++ 3 files changed, 91 insertions(+), 14 deletions(-) diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index c7af077dd809..6dc06f54a35a 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -1642,5 +1642,17 @@ enum ResponseCodeEnum { * The fee amount is exceeding the amount that the payer * is willing to pay. */ - CUSTOM_FEES_LIMIT_EXCEEDED = 378; + MAX_CUSTOM_FEE_LIMIT_EXCEEDED = 378; + + /** + * No provided max custom fee, or there are no corresponding + * topic fees. + */ + NO_VALID_MAX_CUSTOM_FEE = 379; + + /** + * The provided max custom fee list contains fees with + * duplicate denominations. + */ + DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST = 380; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index e1e80af0a71e..7fc4b427bd14 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -16,13 +16,16 @@ package com.hedera.node.app.service.consensus.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CHUNK_NUMBER; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_CHUNK_TRANSACTION_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SUBMIT_KEY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TOPIC_MESSAGE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_CUSTOM_FEE_LIMIT_EXCEEDED; import static com.hedera.hapi.node.base.ResponseCodeEnum.MESSAGE_SIZE_TOO_LARGE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.NO_VALID_MAX_CUSTOM_FEE; import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.BASIC_ENTITY_ID_SIZE; import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.LONG_SIZE; @@ -39,7 +42,6 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; -import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.hapi.node.base.SubType; import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.base.TransactionID; @@ -53,6 +55,7 @@ import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.handlers.customfee.ConsensusCustomFeeAssessor; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageStreamBuilder; +import com.hedera.node.app.service.token.ReadableTokenStore; import com.hedera.node.app.service.token.records.CryptoTransferStreamBuilder; import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; @@ -74,6 +77,7 @@ import java.time.Instant; import java.util.HashSet; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -153,7 +157,7 @@ public void handle(@NonNull final HandleContext handleContext) { if (!topic.customFees().isEmpty() && !isFeeExempted(topic.feeExemptKeyList(), handleContext.keyVerifier())) { // check payer limits or throw if (!op.acceptAllCustomFees()) { - validateFeeLimits(topic.customFees(), op.maxCustomFees()); + validateFeeLimits(topic.customFees(), op.maxCustomFees(), handleContext); } // create synthetic body and dispatch crypto transfer final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); @@ -348,33 +352,67 @@ private boolean isFeeExempted(@NonNull final List feeExemptKeyList, @NonNul */ private void validateFeeLimits( @NonNull final List topicCustomFees, - @NonNull final List payerCustomFeeLimits) { - for (final ConsensusCustomFee fee : topicCustomFees) { + @NonNull final List payerCustomFeeLimits, + @NonNull final HandleContext context) { + validateDuplicationFeeLimits(payerCustomFeeLimits); + for (final ConsensusCustomFee consensusCustomfee : topicCustomFees) { // validate limits boolean passed = false; - final var fixedFee = fee.fixedFeeOrThrow(); - if (fixedFee.hasDenominatingTokenId()) { - for (FixedFee feeLimit : payerCustomFeeLimits) { - if (feeLimit.hasDenominatingTokenId() - && feeLimit.denominatingTokenId().equals(fixedFee.denominatingTokenId()) - && feeLimit.amount() >= fixedFee.amount()) { + final var fee = consensusCustomfee.fixedFeeOrThrow(); + + if (fee.hasDenominatingTokenId()) { + for (FixedFee limit : payerCustomFeeLimits) { + if (limit.hasDenominatingTokenId() + && limit.denominatingTokenId().equals(fee.denominatingTokenId())) { + validateTrue(limit.amount() >= fee.amount(), MAX_CUSTOM_FEE_LIMIT_EXCEEDED); passed = true; break; } } + // if payer is missing a limit for a token custom fee, + // we can check if the account is fee collector or token treasury + if (!passed) { + if (context.payer().equals(consensusCustomfee.feeCollectorAccountId())) { + passed = true; + } + final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); + final var treasury = customFeeAssessor.getTokenTreasury(fee.denominatingTokenId(), tokenStore); + if (context.payer().equals(treasury)) { + passed = true; + } + } } else { for (FixedFee feeLimit : payerCustomFeeLimits) { - if (!feeLimit.hasDenominatingTokenId() && feeLimit.amount() >= fixedFee.amount()) { + if (!feeLimit.hasDenominatingTokenId()) { + validateTrue(feeLimit.amount() >= fee.amount(), MAX_CUSTOM_FEE_LIMIT_EXCEEDED); passed = true; break; } } } - // if any fee amount is larger than the corresponding limit, validation should fail - validateTrue(passed, ResponseCodeEnum.CUSTOM_FEES_LIMIT_EXCEEDED); + // if no limit provided for corresponding custom fee + validateTrue(passed, NO_VALID_MAX_CUSTOM_FEE); } } + private void validateDuplicationFeeLimits(@NonNull final List payerCustomFeeLimits) { + final var htsCustomFeeLimits = payerCustomFeeLimits.stream() + .filter(FixedFee::hasDenominatingTokenId) + .toList(); + final var hbarCustomFeeLimits = payerCustomFeeLimits.stream() + .filter(fee -> !fee.hasDenominatingTokenId()) + .toList(); + + final var htsLimitHasDuplicate = htsCustomFeeLimits.stream() + .map(FixedFee::denominatingTokenId) + .collect(Collectors.toSet()) + .size() + != htsCustomFeeLimits.size(); + final var hbarLimitsHasDuplicate = new HashSet<>(hbarCustomFeeLimits).size() != hbarCustomFeeLimits.size(); + + validateTrue(htsLimitHasDuplicate && hbarLimitsHasDuplicate, DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST); + } + @NonNull @Override public Fees calculateFees(@NonNull final FeeContext feeContext) { diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 649129679997..20ed3a5b9a86 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -41,6 +41,7 @@ import com.hedera.services.bdd.junit.support.TestLifecycle; import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.transactions.token.TokenMovement; +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.stream.Stream; @@ -144,6 +145,7 @@ private SpecOperation[] associateAllTokensToCollectors() { } return associateTokensToCollectors.toArray(SpecOperation[]::new); } + // TOPIC_FEE_108 private SpecOperation createTopicWith10Different2layerFees() { final var collectorName = "collector_"; @@ -410,5 +412,30 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L), getAccountBalance(secondCollector).hasTokenBalance(SECOND_TOKEN, 1L)); } + + @HapiTest + @DisplayName("Just some tests") + final Stream test() { + final var collector = "collector"; + final var fee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); + final var fee1 = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var hbarFee = fixedConsensusHbarFee(1, collector); + final var hbarFee2 = fixedConsensusHbarFee(2, collector); + return hapiTest( + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fee).withConsensusCustomFee(hbarFee2), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .maxCustomFee(hbarFee) + .message("TEST") + .payingWith(SUBMITTER) + .hasKnownStatus(ResponseCodeEnum.DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST)); + } + + // questions: + // topic with 2 fees with same denomination! + // topic with multiple denomination fees + } } From 2be0067d7f7e2310e5d6497be8715eaf9a2fbd18 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 16 Dec 2024 15:06:46 +0200 Subject: [PATCH 75/94] Fee limits additional validations Signed-off-by: Zhivko Kelchev --- .../consensus/impl/handlers/ConsensusSubmitMessageHandler.java | 2 +- .../bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 7fc4b427bd14..c9d040dc5e1b 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -410,7 +410,7 @@ private void validateDuplicationFeeLimits(@NonNull final List payerCus != htsCustomFeeLimits.size(); final var hbarLimitsHasDuplicate = new HashSet<>(hbarCustomFeeLimits).size() != hbarCustomFeeLimits.size(); - validateTrue(htsLimitHasDuplicate && hbarLimitsHasDuplicate, DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST); + validateTrue(!htsLimitHasDuplicate && !hbarLimitsHasDuplicate, DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST); } @NonNull diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 20ed3a5b9a86..61ab7ae6d79d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -427,7 +427,7 @@ final Stream test() { createTopic(TOPIC).withConsensusCustomFee(fee).withConsensusCustomFee(hbarFee2), submitMessageTo(TOPIC) .maxCustomFee(fee) - .maxCustomFee(hbarFee) + .maxCustomFee(fee1) .message("TEST") .payingWith(SUBMITTER) .hasKnownStatus(ResponseCodeEnum.DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST)); From ac943c5021459fac27593622acf25997fba6f8ce Mon Sep 17 00:00:00 2001 From: ibankov Date: Mon, 16 Dec 2024 15:36:21 +0200 Subject: [PATCH 76/94] fixed fee limit validation Signed-off-by: ibankov --- .../ConsensusSubmitMessageHandler.java | 81 +++++++++++-------- .../TopicCustomFeeSubmitMessageTest.java | 45 ++++++++++- 2 files changed, 91 insertions(+), 35 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index c9d040dc5e1b..b54c293a42ea 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -43,6 +43,7 @@ import com.hedera.hapi.node.base.HederaFunctionality; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.SubType; +import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TopicID; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.consensus.ConsensusSubmitMessageTransactionBody; @@ -75,8 +76,11 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -354,44 +358,53 @@ private void validateFeeLimits( @NonNull final List topicCustomFees, @NonNull final List payerCustomFeeLimits, @NonNull final HandleContext context) { + // Validate the duplication of payer custom fee limits validateDuplicationFeeLimits(payerCustomFeeLimits); - for (final ConsensusCustomFee consensusCustomfee : topicCustomFees) { - // validate limits - boolean passed = false; - final var fee = consensusCustomfee.fixedFeeOrThrow(); - - if (fee.hasDenominatingTokenId()) { - for (FixedFee limit : payerCustomFeeLimits) { - if (limit.hasDenominatingTokenId() - && limit.denominatingTokenId().equals(fee.denominatingTokenId())) { - validateTrue(limit.amount() >= fee.amount(), MAX_CUSTOM_FEE_LIMIT_EXCEEDED); - passed = true; - break; - } - } - // if payer is missing a limit for a token custom fee, - // we can check if the account is fee collector or token treasury - if (!passed) { - if (context.payer().equals(consensusCustomfee.feeCollectorAccountId())) { - passed = true; - } - final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); - final var treasury = customFeeAssessor.getTokenTreasury(fee.denominatingTokenId(), tokenStore); - if (context.payer().equals(treasury)) { - passed = true; - } - } - } else { - for (FixedFee feeLimit : payerCustomFeeLimits) { - if (!feeLimit.hasDenominatingTokenId()) { - validateTrue(feeLimit.amount() >= fee.amount(), MAX_CUSTOM_FEE_LIMIT_EXCEEDED); - passed = true; - break; + + // Extract the token fees and hbar fees from the topic custom fees + Map tokenFees = new HashMap<>(); + AtomicReference hbarFee = new AtomicReference<>(0L); + extractFees(topicCustomFees, context, hbarFee, tokenFees); + // Validate payer token limits + tokenFees.forEach((token, feeAmount) -> { + final boolean isValid = payerCustomFeeLimits.stream() + .filter(fee -> token.equals(fee.denominatingTokenId())) + .anyMatch(fee -> { + validateTrue(fee.amount() >= feeAmount, MAX_CUSTOM_FEE_LIMIT_EXCEEDED); + return true; + }); + validateTrue(isValid, NO_VALID_MAX_CUSTOM_FEE); + }); + // Validate payer HBAR limit + if (hbarFee.get() > 0) { + final var payerHbarLimit = payerCustomFeeLimits.stream() + .filter(fee -> !fee.hasDenominatingTokenId()) + .findFirst() + .orElseThrow(() -> new HandleException(NO_VALID_MAX_CUSTOM_FEE)); + validateTrue(payerHbarLimit.amount() >= hbarFee.get(), MAX_CUSTOM_FEE_LIMIT_EXCEEDED); + } + } + + private void extractFees( + @NonNull List topicCustomFees, + HandleContext context, + AtomicReference hbarFee, + Map tokenFees) { + final var payer = context.payer(); + final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); + for (final var fee : topicCustomFees) { + if (!payer.equals(fee.feeCollectorAccountId())) { + var fixedFee = fee.fixedFeeOrThrow(); + if (!fixedFee.hasDenominatingTokenId()) { + hbarFee.updateAndGet(v -> v + fixedFee.amount()); + } else { + final var denomTokenId = fixedFee.denominatingTokenId(); + final var treasury = customFeeAssessor.getTokenTreasury(denomTokenId, tokenStore); + if (!context.payer().equals(treasury)) { + tokenFees.put(denomTokenId, tokenFees.getOrDefault(denomTokenId, 0L) + fixedFee.amount()); } } } - // if no limit provided for corresponding custom fee - validateTrue(passed, NO_VALID_MAX_CUSTOM_FEE); } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 61ab7ae6d79d..907797d3ee34 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -433,9 +433,52 @@ final Stream test() { .hasKnownStatus(ResponseCodeEnum.DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST)); } + @HapiTest + @DisplayName("Test multiple fees with same denomination") + final Stream multipleFeesSameDenom() { + final var collector = "collector"; + final var fee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); + final var fee1 = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var correctFeeLimit = fixedConsensusHtsFee(3, BASE_TOKEN, collector); + return hapiTest( + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fee).withConsensusCustomFee(fee1), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .message("TEST") + .payingWith(SUBMITTER) + .hasKnownStatus(ResponseCodeEnum.MAX_CUSTOM_FEE_LIMIT_EXCEEDED), + submitMessageTo(TOPIC) + .maxCustomFee(correctFeeLimit) + .message("TEST") + .payingWith(SUBMITTER)); + } + + @HapiTest + @DisplayName("Test multiple hbar fees with") + final Stream multipleHbarFees() { + final var collector = "collector"; + final var fee = fixedConsensusHbarFee(2, collector); + final var fee1 = fixedConsensusHbarFee(1, collector); + final var correctFeeLimit = fixedConsensusHbarFee(3, collector); + return hapiTest( + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fee).withConsensusCustomFee(fee1), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .message("TEST") + .payingWith(SUBMITTER) + .hasKnownStatus(ResponseCodeEnum.MAX_CUSTOM_FEE_LIMIT_EXCEEDED), + submitMessageTo(TOPIC) + .maxCustomFee(correctFeeLimit) + .message("TEST") + .payingWith(SUBMITTER)); + } + // questions: // topic with 2 fees with same denomination! // topic with multiple denomination fees - } } From c2c4f488ab2708c547930d3bd7cc8810547fca53 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 17 Dec 2024 14:41:08 +0200 Subject: [PATCH 77/94] Remove draft test Signed-off-by: Zhivko Kelchev --- .../TopicCustomFeeSubmitMessageTest.java | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 907797d3ee34..ab2a184555fe 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -413,26 +413,6 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { getAccountBalance(secondCollector).hasTokenBalance(SECOND_TOKEN, 1L)); } - @HapiTest - @DisplayName("Just some tests") - final Stream test() { - final var collector = "collector"; - final var fee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); - final var fee1 = fixedConsensusHtsFee(1, BASE_TOKEN, collector); - final var hbarFee = fixedConsensusHbarFee(1, collector); - final var hbarFee2 = fixedConsensusHbarFee(2, collector); - return hapiTest( - cryptoCreate(collector).balance(ONE_HBAR), - tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fee).withConsensusCustomFee(hbarFee2), - submitMessageTo(TOPIC) - .maxCustomFee(fee) - .maxCustomFee(fee1) - .message("TEST") - .payingWith(SUBMITTER) - .hasKnownStatus(ResponseCodeEnum.DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST)); - } - @HapiTest @DisplayName("Test multiple fees with same denomination") final Stream multipleFeesSameDenom() { @@ -476,9 +456,5 @@ final Stream multipleHbarFees() { .message("TEST") .payingWith(SUBMITTER)); } - - // questions: - // topic with 2 fees with same denomination! - // topic with multiple denomination fees } } From 3afeacd5320010df89f747758fa738f241536dea Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 19 Dec 2024 14:06:30 +0200 Subject: [PATCH 78/94] Filter only payable fees and use this list for limit validation and crypto transfer body creation. Signed-off-by: Zhivko Kelchev --- .../ConsensusSubmitMessageHandler.java | 70 +++++++++++++------ .../customfee/ConsensusCustomFeeAssessor.java | 20 +----- 2 files changed, 52 insertions(+), 38 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index b54c293a42ea..574c7315ed2a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -159,12 +159,16 @@ public void handle(@NonNull final HandleContext handleContext) { /* handle custom fees */ if (!topic.customFees().isEmpty() && !isFeeExempted(topic.feeExemptKeyList(), handleContext.keyVerifier())) { + // filter fee list + final var feesToBeCharged = extractFeesToBeCharged(topic.customFees(), handleContext); + // check payer limits or throw if (!op.acceptAllCustomFees()) { - validateFeeLimits(topic.customFees(), op.maxCustomFees(), handleContext); + validateFeeLimits(feesToBeCharged, op.maxCustomFees()); } + // create synthetic body and dispatch crypto transfer - final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); + final var syntheticBodies = customFeeAssessor.assessCustomFee(feesToBeCharged, handleContext.payer()); for (final var syntheticBody : syntheticBodies) { final var record = handleContext.dispatch(stepDispatch( handleContext.payer(), @@ -356,15 +360,14 @@ private boolean isFeeExempted(@NonNull final List feeExemptKeyList, @NonNul */ private void validateFeeLimits( @NonNull final List topicCustomFees, - @NonNull final List payerCustomFeeLimits, - @NonNull final HandleContext context) { + @NonNull final List payerCustomFeeLimits) { // Validate the duplication of payer custom fee limits validateDuplicationFeeLimits(payerCustomFeeLimits); // Extract the token fees and hbar fees from the topic custom fees Map tokenFees = new HashMap<>(); AtomicReference hbarFee = new AtomicReference<>(0L); - extractFees(topicCustomFees, context, hbarFee, tokenFees); + totalAmountToBeCharged(topicCustomFees, hbarFee, tokenFees); // Validate payer token limits tokenFees.forEach((token, feeAmount) -> { final boolean isValid = payerCustomFeeLimits.stream() @@ -385,25 +388,52 @@ private void validateFeeLimits( } } - private void extractFees( + /** + * Extracts only the fees that are going to be charged. + * The payer will not be charged in case he is a fee collector or token treasury. + * + * @param topicCustomFees All topic custom fees + * @param context The handle context + * @return List containing only the fees concerning given payer + */ + private List extractFeesToBeCharged( + @NonNull final List topicCustomFees, @NonNull final HandleContext context) { + final var payer = context.payer(); + final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); + return topicCustomFees.stream() + .filter(fee -> { + var fixedFee = fee.fixedFeeOrThrow(); + if (payer.equals(fee.feeCollectorAccountId())) { + return false; + } + if (fixedFee.hasDenominatingTokenId()) { + final var denomTokenId = fixedFee.denominatingTokenId(); + final var treasury = customFeeAssessor.getTokenTreasury(denomTokenId, tokenStore); + return !payer.equals(treasury); + } + return true; + }) + .toList(); + } + + /** + * Calculate the total amount of fees to be charge per denomination token and hbar fees. + * + * @param topicCustomFees All fees to be charged + * @param hbarFee The total hbar amount. + * @param tokenFees Map with total amount per token. + */ + private void totalAmountToBeCharged( @NonNull List topicCustomFees, - HandleContext context, AtomicReference hbarFee, Map tokenFees) { - final var payer = context.payer(); - final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); for (final var fee : topicCustomFees) { - if (!payer.equals(fee.feeCollectorAccountId())) { - var fixedFee = fee.fixedFeeOrThrow(); - if (!fixedFee.hasDenominatingTokenId()) { - hbarFee.updateAndGet(v -> v + fixedFee.amount()); - } else { - final var denomTokenId = fixedFee.denominatingTokenId(); - final var treasury = customFeeAssessor.getTokenTreasury(denomTokenId, tokenStore); - if (!context.payer().equals(treasury)) { - tokenFees.put(denomTokenId, tokenFees.getOrDefault(denomTokenId, 0L) + fixedFee.amount()); - } - } + var fixedFee = fee.fixedFeeOrThrow(); + if (!fixedFee.hasDenominatingTokenId()) { + hbarFee.updateAndGet(v -> v + fixedFee.amount()); + } else { + final var denomTokenId = fixedFee.denominatingTokenId(); + tokenFees.put(denomTokenId, tokenFees.getOrDefault(denomTokenId, 0L) + fixedFee.amount()); } } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 19825f5b7b24..41ec8dd92a6a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -25,12 +25,10 @@ import com.hedera.hapi.node.base.TokenID; import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; -import com.hedera.hapi.node.state.consensus.Topic; import com.hedera.hapi.node.token.CryptoTransferTransactionBody; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.node.app.service.token.ReadableTokenStore; -import com.hedera.node.app.spi.workflows.HandleContext; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.ArrayList; import java.util.List; @@ -57,31 +55,17 @@ public ConsensusCustomFeeAssessor() { * @return List of synthetic crypto transfer transaction bodies */ public List assessCustomFee( - @NonNull final Topic topic, @NonNull final HandleContext context) { + @NonNull final List customFees, @NonNull final AccountID payer) { final List transactionBodies = new ArrayList<>(); - final var payer = context.payer(); - final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); // build crypto transfer bodies for the first layer of custom fees, // if there is a second or third layer it will be assessed in crypto transfer handler - for (ConsensusCustomFee fee : topic.customFees()) { - - // check if payer is collector - if (context.payer().equals(fee.feeCollectorAccountId())) { - continue; - } - + for (ConsensusCustomFee fee : customFees) { final var tokenTransfers = new ArrayList(); List hbarTransfers = new ArrayList<>(); final var fixedFee = fee.fixedFeeOrThrow(); if (fixedFee.hasDenominatingTokenId()) { - final var tokenId = fixedFee.denominatingTokenIdOrThrow(); - final var tokenTreasury = getTokenTreasury(tokenId, tokenStore); - // check if payer is treasury - if (context.payer().equals(tokenTreasury)) { - continue; - } tokenTransfers.add(buildCustomFeeTokenTransferList(payer, fee.feeCollectorAccountId(), fixedFee)); } else { hbarTransfers = buildCustomFeeHbarTransferList(payer, fee.feeCollectorAccountId(), fixedFee); From a9d333a3c8e00ca3c532e06904e0527345132bc1 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Thu, 19 Dec 2024 14:47:20 +0200 Subject: [PATCH 79/94] Fix docs Signed-off-by: Zhivko Kelchev --- .../impl/handlers/customfee/ConsensusCustomFeeAssessor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 41ec8dd92a6a..47326ace27e5 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -50,8 +50,8 @@ public ConsensusCustomFeeAssessor() { * Build and return a list of synthetic crypto transfer transaction bodies, that represents custom fees payments. * It will return one body per topic custom fee. * - * @param topic The topic - * @param context The transaction handle context + * @param customFees List of custom fees to be charged + * @param payer The payer Account ID * @return List of synthetic crypto transfer transaction bodies */ public List assessCustomFee( From 8bea62b3bc7f97d104b809b83613c272f3b2844c Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 2 Jan 2025 12:53:14 +0200 Subject: [PATCH 80/94] last tests Signed-off-by: ibankov --- .../TopicCustomFeeSubmitMessageTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index ab2a184555fe..f4080a3eb1b5 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -25,6 +25,7 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.cryptoTransfer; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.updateTopic; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; @@ -386,6 +387,35 @@ final Stream collectorSubmitMessageToTopicWithHbarFee() { })); } + @HapiTest + @DisplayName("Submit message to a topic after fee update") + // TOPIC_FEE_126 + final Stream submitMessageAfterUpdate() { + final var collector = "collector"; + final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var updatedFee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); + return hapiTest( + newKeyNamed("adminKey"), + newKeyNamed("feeScheduleKey"), + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fee).adminKeyName("adminKey").feeScheduleKeyName("feeScheduleKey"), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .message("TEST") + .payingWith(SUBMITTER) + .via("submit"), + // assert collector's tinyBars balance + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1), + updateTopic(TOPIC).withConsensusCustomFee(updatedFee).signedByPayerAnd("adminKey"), + submitMessageTo(TOPIC) + .maxCustomFee(updatedFee) + .message("TEST") + .payingWith(SUBMITTER) + .via("submit2"), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 3)); + } + @HapiTest @DisplayName("Collector submits a message to a topic with 2 different FT fees.") final Stream collectorSubmitMessageToTopicWith2differentFees() { From e2366320530c42b69daabd4ada0a7d24b3acdc8b Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 2 Jan 2025 13:15:36 +0200 Subject: [PATCH 81/94] spotless Signed-off-by: ibankov --- .../impl/handlers/ConsensusSubmitMessageHandler.java | 2 +- .../impl/handlers/customfee/ConsensusCustomFeeAssessor.java | 2 +- .../impl/test/handlers/ConsensusSubmitMessageHandlerTest.java | 2 +- .../bdd/spec/transactions/consensus/HapiMessageSubmit.java | 2 +- .../bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java | 2 +- .../services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java | 4 +--- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 574c7315ed2a..185792db94b2 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -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. diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 47326ace27e5..25ea9c01ac03 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index 00ea43aa1bb2..ff4e7273a2da 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java index f5bf861d029b..39e3cd3cc658 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java @@ -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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index ab2a184555fe..eb5afb86e6ee 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java index 2af0d949d375..6e559e45945c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java @@ -112,9 +112,7 @@ final Stream updateFeeScheduleKey() { getTopicInfo(TOPIC).hasAdminKey(ADMIN_KEY).hasFeeScheduleKey(FEE_SCHEDULE_KEY), // Update the fee schedule and verify that it's updated - updateTopic(TOPIC) - .feeScheduleKeyName(FEE_SCHEDULE_KEY2) - .signedByPayerAnd(ADMIN_KEY), + updateTopic(TOPIC).feeScheduleKeyName(FEE_SCHEDULE_KEY2).signedByPayerAnd(ADMIN_KEY), getTopicInfo(TOPIC).hasFeeScheduleKey(FEE_SCHEDULE_KEY2)); } From 55d8100bbfab08082ddb3db12599c57bf936c36c Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 2 Jan 2025 13:16:33 +0200 Subject: [PATCH 82/94] spotless Signed-off-by: ibankov --- .../suites/hip991/TopicCustomFeeSubmitMessageTest.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index c1f719f01bb3..7db2c20f8b69 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -399,7 +399,10 @@ final Stream submitMessageAfterUpdate() { newKeyNamed("feeScheduleKey"), cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fee).adminKeyName("adminKey").feeScheduleKeyName("feeScheduleKey"), + createTopic(TOPIC) + .withConsensusCustomFee(fee) + .adminKeyName("adminKey") + .feeScheduleKeyName("feeScheduleKey"), submitMessageTo(TOPIC) .maxCustomFee(fee) .message("TEST") @@ -407,7 +410,9 @@ final Stream submitMessageAfterUpdate() { .via("submit"), // assert collector's tinyBars balance getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1), - updateTopic(TOPIC).withConsensusCustomFee(updatedFee).signedByPayerAnd("adminKey","feeScheduleKey"), + updateTopic(TOPIC) + .withConsensusCustomFee(updatedFee) + .signedByPayerAnd("adminKey", "feeScheduleKey"), submitMessageTo(TOPIC) .maxCustomFee(updatedFee) .message("TEST") From 4f31f34f637a7e7466fabffc4737de611053e666 Mon Sep 17 00:00:00 2001 From: ibankov Date: Thu, 2 Jan 2025 13:25:11 +0200 Subject: [PATCH 83/94] last tests Signed-off-by: ibankov --- .../TopicCustomFeeSubmitMessageTest.java | 49 ++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 7db2c20f8b69..aaf08548d06a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -408,7 +408,6 @@ final Stream submitMessageAfterUpdate() { .message("TEST") .payingWith(SUBMITTER) .via("submit"), - // assert collector's tinyBars balance getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1), updateTopic(TOPIC) .withConsensusCustomFee(updatedFee) @@ -421,6 +420,54 @@ final Stream submitMessageAfterUpdate() { getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 3)); } + @HapiTest + @DisplayName("Submit message to a topic after key is removed from FEKL") + // TOPIC_FEE_129 + final Stream submitMessageAfterFEKLisRemoved() { + final var collector = "collector"; + final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + return hapiTest( + newKeyNamed("adminKey"), + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC) + .withConsensusCustomFee(fee) + .adminKeyName("adminKey") + .feeExemptKeys(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER).via("submit"), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0), + updateTopic(TOPIC).feeExemptKeys().signedByPayerAnd("adminKey"), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .message("TEST") + .payingWith(SUBMITTER) + .via("submit2"), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); + } + + @HapiTest + @DisplayName("Submit message to a topic after fee is added with update") + // TOPIC_FEE_130 + final Stream submitMessageAfterFeeIsAdded() { + final var collector = "collector"; + final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + return hapiTest( + newKeyNamed("adminKey"), + newKeyNamed("feeScheduleKey"), + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).adminKeyName("adminKey").feeScheduleKeyName("feeScheduleKey"), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER).via("submit"), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0), + updateTopic(TOPIC).withConsensusCustomFee(fee).signedByPayerAnd("adminKey", "feeScheduleKey"), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .message("TEST") + .payingWith(SUBMITTER) + .via("submit2"), + getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); + } + @HapiTest @DisplayName("Collector submits a message to a topic with 2 different FT fees.") final Stream collectorSubmitMessageToTopicWith2differentFees() { From 2c5413d2a16cb793b67fe75bbf90852df3f96669 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 6 Jan 2025 09:56:05 +0200 Subject: [PATCH 84/94] Replace FEKL keys validation Signed-off-by: Zhivko Kelchev --- .../hedera/node/config/data/TopicsConfig.java | 2 +- .../build.gradle.kts | 2 +- .../handlers/ConsensusCreateTopicHandler.java | 2 +- .../ConsensusGetTopicInfoHandler.java | 2 +- .../ConsensusSubmitMessageHandler.java | 43 ++++++++++++++----- .../handlers/ConsensusUpdateTopicHandler.java | 2 +- .../customfee/ConsensusCustomFeeAssessor.java | 2 +- .../ConsensusCustomFeesValidator.java | 2 +- .../handlers/ConsensusCreateTopicTest.java | 2 +- .../handlers/ConsensusDeleteTopicTest.java | 2 +- .../handlers/ConsensusGetTopicInfoTest.java | 2 +- .../ConsensusSubmitMessageHandlerTest.java | 2 +- .../impl/test/handlers/ConsensusTestBase.java | 2 +- .../test/handlers/ConsensusTestUtils.java | 2 +- .../ConsensusUpdateTopicHandlerTest.java | 2 +- .../token/impl/util/TokenHandlerHelper.java | 2 +- .../queries/consensus/HapiGetTopicInfo.java | 2 +- .../consensus/HapiMessageSubmit.java | 2 +- .../consensus/HapiTopicCreate.java | 2 +- .../consensus/HapiTopicUpdate.java | 2 +- .../transactions/token/CustomFeeSpecs.java | 2 +- .../transactions/token/CustomFeeTests.java | 2 +- .../bdd/suites/hip991/TopicCustomFeeBase.java | 2 +- .../hip991/TopicCustomFeeCreateTest.java | 2 +- .../TopicCustomFeeGetTopicInfoTest.java | 2 +- .../TopicCustomFeeSubmitMessageTest.java | 10 ++++- .../hip991/TopicCustomFeeUpdateTest.java | 2 +- 27 files changed, 66 insertions(+), 37 deletions(-) diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java index 7d303119801e..c615f59285fb 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/TopicsConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/build.gradle.kts b/hedera-node/hedera-consensus-service-impl/build.gradle.kts index c52ba5ab70c3..34612a79c329 100644 --- a/hedera-node/hedera-consensus-service-impl/build.gradle.kts +++ b/hedera-node/hedera-consensus-service-impl/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java index a1e76cde51c9..a763574a2037 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusCreateTopicHandler.java @@ -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. diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java index 0d7fc28fca98..a93660283202 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusGetTopicInfoHandler.java @@ -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. diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 574c7315ed2a..a422bff427bc 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -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. @@ -61,6 +61,7 @@ import com.hedera.node.app.spi.fees.FeeContext; import com.hedera.node.app.spi.fees.Fees; import com.hedera.node.app.spi.key.KeyVerifier; +import com.hedera.node.app.spi.signatures.VerificationAssistant; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -80,7 +81,9 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; import java.util.stream.Collectors; import javax.inject.Inject; import javax.inject.Singleton; @@ -128,12 +131,12 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx if (topic.hasSubmitKey()) { context.requireKeyOrThrow(topic.submitKeyOrThrow(), INVALID_SUBMIT_KEY); } - // add optional fee exempt keys in to the key verifieer - // later it will be used to validate if transaction was signed by - // any of these keys and based on that, custom fees will be charged or not - if (!topic.feeExemptKeyList().isEmpty()) { - context.optionalKeys(new HashSet<>(topic.feeExemptKeyList())); - } + // // add optional fee exempt keys in to the key verifieer + // // later it will be used to validate if transaction was signed by + // // any of these keys and based on that, custom fees will be charged or not + // if (!topic.feeExemptKeyList().isEmpty()) { + // context.optionalKeys(new HashSet<>(topic.feeExemptKeyList())); + // } } /** @@ -342,9 +345,13 @@ public static byte[] noThrowSha384HashOf(final byte[] byteArray) { */ private boolean isFeeExempted(@NonNull final List feeExemptKeyList, @NonNull final KeyVerifier keyVerifier) { if (!feeExemptKeyList.isEmpty()) { - for (final var key : feeExemptKeyList) { - final var keyVerificationResult = keyVerifier.verificationFor(key); - if (keyVerificationResult.passed()) { + final var authorizingKeys = + keyVerifier.authorizingSimpleKeys().stream().toList(); + final VerificationAssistant callback = + (k, ignore) -> simpleKeyVerifierFrom(authorizingKeys).test(k); + // check if authorizing keys are satisfying any of the fee exempt keys + for (final var feeExemptKey : feeExemptKeyList) { + if (keyVerifier.verificationFor(feeExemptKey, callback).passed()) { return true; } } @@ -352,6 +359,22 @@ private boolean isFeeExempted(@NonNull final List feeExemptKeyList, @NonNul return false; } + public static Predicate simpleKeyVerifierFrom(@NonNull final List signatories) { + final Set cryptoSigs = new HashSet<>(); + signatories.forEach(k -> { + switch (k.key().kind()) { + case ED25519, ECDSA_SECP256K1 -> cryptoSigs.add(k); + default -> { + // No other key type can be a signatory + } + } + }); + return key -> switch (key.key().kind()) { + case ED25519, ECDSA_SECP256K1 -> cryptoSigs.contains(key); + default -> false; + }; + } + /** * Validate that each topic custom fee has equal or lower value than the payer's limit * diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java index 84c891ca2b95..494d32396b64 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusUpdateTopicHandler.java @@ -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. diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 47326ace27e5..25ea9c01ac03 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java index e205b6160e60..86bf69f4443c 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java index 09234c87e2fd..48cbc870a472 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java index aeb1abbf9843..94ed54da65bc 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusDeleteTopicTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java index 33674afb0484..4415f7d42236 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusGetTopicInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index 00ea43aa1bb2..ff4e7273a2da 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java index 10bc7a9b34dd..103c5e1ded7a 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java index d581bc0e3759..783d4f800472 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusUpdateTopicHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusUpdateTopicHandlerTest.java index 5d7f38ba363a..b625d6063797 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusUpdateTopicHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusUpdateTopicHandlerTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java index c875e4a5c834..19a186180195 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/util/TokenHandlerHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023-2024 Hedera Hashgraph, LLC + * Copyright (C) 2023-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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java index fbd67726e4a4..fcae1cbde7ef 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java @@ -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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java index f5bf861d029b..39e3cd3cc658 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java @@ -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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java index de3cdef09607..886fc0039a84 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java @@ -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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java index 827af8468027..2752ac48c0ac 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java @@ -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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java index 8cabcf4d8c8f..4b01a7fd77a8 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2024 Hedera Hashgraph, LLC + * Copyright (C) 2021-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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java index c132e44bcda1..0a7c59b10f97 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2021-2024 Hedera Hashgraph, LLC + * Copyright (C) 2021-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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 37c13423802a..0e9e0435534c 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java index 04c000ba8644..db19c4d94e44 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java @@ -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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeGetTopicInfoTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeGetTopicInfoTest.java index f3243da0a3de..eed21589cb43 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeGetTopicInfoTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeGetTopicInfoTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index ab2a184555fe..3737bd12565a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. @@ -17,6 +17,7 @@ package com.hedera.services.bdd.suites.hip991; import static com.hedera.services.bdd.spec.HapiSpec.hapiTest; +import static com.hedera.services.bdd.spec.keys.TrieSigMapGenerator.uniqueWithFullPrefixesFor; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountInfo; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; @@ -344,7 +345,12 @@ final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { .feeScheduleKeyName(feeScheduleKey) .feeExemptKeys(feeScheduleKey) .withConsensusCustomFee(fee), - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").signedByPayerAnd(feeScheduleKey), + submitMessageTo(TOPIC) + .maxCustomFee(fee) + .message("TEST") + .signedByPayerAnd(feeScheduleKey) + // any non payer key in FEKL, should sign with full prefixes keys + .sigMapPrefixes(uniqueWithFullPrefixesFor(feeScheduleKey)), getAccountBalance(collector).hasTinyBars(0L)); } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java index 8c5d659a0ccb..f83988833ecb 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeUpdateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 Hedera Hashgraph, LLC + * Copyright (C) 2024-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. From 29da0fb48a7b98de824451fed544a8ec5b54ff39 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 6 Jan 2025 10:19:22 +0200 Subject: [PATCH 85/94] Replace FEKL keys validation Signed-off-by: Zhivko Kelchev --- .../impl/handlers/ConsensusSubmitMessageHandler.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index a422bff427bc..09be36d4cf24 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -131,12 +131,6 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx if (topic.hasSubmitKey()) { context.requireKeyOrThrow(topic.submitKeyOrThrow(), INVALID_SUBMIT_KEY); } - // // add optional fee exempt keys in to the key verifieer - // // later it will be used to validate if transaction was signed by - // // any of these keys and based on that, custom fees will be charged or not - // if (!topic.feeExemptKeyList().isEmpty()) { - // context.optionalKeys(new HashSet<>(topic.feeExemptKeyList())); - // } } /** From 700acbcf6adf45e8fd729917b85281e720356987 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 6 Jan 2025 12:40:56 +0200 Subject: [PATCH 86/94] Fix unit tests Signed-off-by: Zhivko Kelchev --- .../impl/test/handlers/ConsensusSubmitMessageHandlerTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index ff4e7273a2da..10079faf386f 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -477,7 +477,7 @@ private TransactionBody newSubmitMessageTxnWithChunksAndPayer( private void mockPayerKeyIsFeeExempt() { // mock signature is in FEKL given(handleContext.keyVerifier()).willReturn(keyVerifier); - given(keyVerifier.verificationFor(any(Key.class))).willReturn(signatureVerification); + given(keyVerifier.verificationFor(any(Key.class), any())).willReturn(signatureVerification); given(signatureVerification.passed()).willReturn(true); } @@ -490,7 +490,7 @@ private void mockPayerKeyIsNotFeeExempt() { given(streamBuilder.status()).willReturn(SUCCESS); // mock signature is not in FEKL given(handleContext.keyVerifier()).willReturn(keyVerifier); - given(keyVerifier.verificationFor(any(Key.class))).willReturn(signatureVerification); + given(keyVerifier.verificationFor(any(Key.class), any())).willReturn(signatureVerification); given(signatureVerification.passed()).willReturn(false); } } From 07d571ed4030eb62a3655a68c2e7a2815b000a75 Mon Sep 17 00:00:00 2001 From: ibankov Date: Mon, 6 Jan 2025 14:13:33 +0200 Subject: [PATCH 87/94] Added more coverage Signed-off-by: ibankov --- .../ConsensusSubmitMessageHandlerTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index 10079faf386f..c573a9c698a5 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -27,8 +27,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Answers.RETURNS_SELF; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; @@ -76,6 +78,8 @@ import java.io.IOException; import java.time.Instant; import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -107,6 +111,14 @@ class ConsensusSubmitMessageHandlerTest extends ConsensusTestBase { private ConsensusSubmitMessageHandler subject; + private static final Key ED25519KEY = Key.newBuilder() + .ed25519(Bytes.fromHex("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")) + .build(); + + private static final Key ECDSAKEY = Key.newBuilder() + .ecdsaSecp256k1((Bytes.fromHex("0101010101010101010101010101010101010101010101010101010101010101"))) + .build(); + @BeforeEach void setUp() { commonSetUp(); @@ -395,6 +407,43 @@ void handleWorksAsExpectedWithCustomFee() { expectedTopic.runningHash().toString()); } + @Test + void testSimpleKeyVerifierFromWithEd25519Key() { + List signatories = List.of(ED25519KEY); + + Predicate verifier = ConsensusSubmitMessageHandler.simpleKeyVerifierFrom(signatories); + + assertTrue(verifier.test(ED25519KEY)); + } + + @Test + void testSimpleKeyVerifierFromWithEcdsaSecp256k1Key() { + + List signatories = List.of(ECDSAKEY); + + Predicate verifier = ConsensusSubmitMessageHandler.simpleKeyVerifierFrom(signatories); + + assertTrue(verifier.test(ECDSAKEY)); + } + + @Test + void testSimpleKeyVerifierFromWithNonSignatoryKey() { + List signatories = List.of(ED25519KEY); + + Predicate verifier = ConsensusSubmitMessageHandler.simpleKeyVerifierFrom(signatories); + + assertFalse(verifier.test(ECDSAKEY)); + } + + @Test + void testSimpleKeyVerifierFromWithEmptySignatories() { + List signatories = List.of(); + + Predicate verifier = ConsensusSubmitMessageHandler.simpleKeyVerifierFrom(signatories); + + assertFalse(verifier.test(ED25519KEY)); + } + /* ----------------- Helper Methods ------------------- */ private Key mockPayerLookup() { From 35bac654bae9d8280d75c96b13b17dea60c96907 Mon Sep 17 00:00:00 2001 From: ibankov Date: Tue, 7 Jan 2025 16:26:05 +0200 Subject: [PATCH 88/94] java doc Signed-off-by: ibankov --- .../bdd/suites/hip991/TopicCustomFeeBase.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java index 0e9e0435534c..21332cbc976f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeBase.java @@ -125,13 +125,12 @@ protected SpecOperation[] createMultipleTokensWith2LayerFees(String owner, int n } /** + * Create and transfer tokens with 2 layer custom fees to given account. * - * - * - * @param owner - * @param tokenName - * @param createTreasury - * @return + * @param owner account to transfer tokens + * @param tokenName token name + * @param createTreasury create treasury or not + * @return list of spec operations */ protected static List createTokenWith2LayerFee( String owner, String tokenName, boolean createTreasury) { From aa773a6b6d4b6258af143fc895df8036d3c0ca75 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 10 Jan 2025 15:20:45 +0200 Subject: [PATCH 89/94] Update protobufs Signed-off-by: Zhivko Kelchev --- .../services/consensus_create_topic.proto | 2 +- .../services/consensus_topic_info.proto | 2 +- .../services/consensus_update_topic.proto | 2 +- .../services/custom_fees.proto | 24 +++++++- .../services/response_code.proto | 26 +++++++++ .../services/state/consensus/topic.proto | 2 +- .../services/transaction_body.proto | 9 +++ .../app/workflows/TransactionChecker.java | 28 ++++++++- .../ConsensusCustomFeesValidator.java | 6 +- .../handlers/ConsensusCreateTopicTest.java | 10 ++-- .../impl/test/handlers/ConsensusTestBase.java | 4 +- .../services/bdd/spec/HapiSpecOperation.java | 9 ++- .../queries/consensus/HapiGetTopicInfo.java | 6 +- .../bdd/spec/transactions/HapiTxnOp.java | 6 ++ .../consensus/HapiMessageSubmit.java | 5 +- .../consensus/HapiTopicCreate.java | 6 +- .../consensus/HapiTopicUpdate.java | 13 ++--- .../transactions/token/CustomFeeSpecs.java | 58 ++++++++++++------- .../transactions/token/CustomFeeTests.java | 10 ++-- .../hip991/TopicCustomFeeCreateTest.java | 4 +- 20 files changed, 170 insertions(+), 62 deletions(-) diff --git a/hapi/hedera-protobufs/services/consensus_create_topic.proto b/hapi/hedera-protobufs/services/consensus_create_topic.proto index 94382c7a7aa7..ebb3cefe61d3 100644 --- a/hapi/hedera-protobufs/services/consensus_create_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_create_topic.proto @@ -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; } diff --git a/hapi/hedera-protobufs/services/consensus_topic_info.proto b/hapi/hedera-protobufs/services/consensus_topic_info.proto index 12482e205660..da8e12091119 100644 --- a/hapi/hedera-protobufs/services/consensus_topic_info.proto +++ b/hapi/hedera-protobufs/services/consensus_topic_info.proto @@ -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; } diff --git a/hapi/hedera-protobufs/services/consensus_update_topic.proto b/hapi/hedera-protobufs/services/consensus_update_topic.proto index 59c198c19e0b..02e7f6406661 100644 --- a/hapi/hedera-protobufs/services/consensus_update_topic.proto +++ b/hapi/hedera-protobufs/services/consensus_update_topic.proto @@ -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; } diff --git a/hapi/hedera-protobufs/services/custom_fees.proto b/hapi/hedera-protobufs/services/custom_fees.proto index 3b683fa2256c..cdc57570871a 100644 --- a/hapi/hedera-protobufs/services/custom_fees.proto +++ b/hapi/hedera-protobufs/services/custom_fees.proto @@ -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. *

@@ -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.
* These are fees to be assessed for each submit to a topic. */ - repeated ConsensusCustomFee fees = 1; + repeated FixedCustomFee fees = 1; } /** @@ -402,3 +402,21 @@ message FeeExemptKeyList { */ repeated Key keys = 1; } + +/** + * A maximum custom fee that the user is willing to pay. + *

+ * 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; +} diff --git a/hapi/hedera-protobufs/services/response_code.proto b/hapi/hedera-protobufs/services/response_code.proto index e7771b23c195..c8654a34b5c2 100644 --- a/hapi/hedera-protobufs/services/response_code.proto +++ b/hapi/hedera-protobufs/services/response_code.proto @@ -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; } diff --git a/hapi/hedera-protobufs/services/state/consensus/topic.proto b/hapi/hedera-protobufs/services/state/consensus/topic.proto index 0c1c70bf21f7..72c4305a0df4 100644 --- a/hapi/hedera-protobufs/services/state/consensus/topic.proto +++ b/hapi/hedera-protobufs/services/state/consensus/topic.proto @@ -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; } diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index b41e58563849..747aeceb88a6 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -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. @@ -561,4 +562,12 @@ 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. + *

+ * This field is OPTIONAL. If it is not set then users are accepting to pay any custom fee. + * If it is set, but the transaction doesn't support it, the transaction will fail. + */ + repeated CustomFeeLimit maxCustomFees = 66; } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java index 56fd293a2bd8..d04c2c6fc5fc 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/TransactionChecker.java @@ -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. @@ -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; @@ -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; @@ -83,6 +85,8 @@ public class TransactionChecker { private static final Logger logger = LogManager.getLogger(TransactionChecker.class); private static final int USER_TRANSACTION_NONCE = 0; + private static final List 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"; @@ -250,7 +254,7 @@ public TransactionInfo check(@NonNull final Transaction tx, @Nullable Bytes seri 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); @@ -310,10 +314,12 @@ private void checkTransactionDeprecation(@NonNull final Transaction tx) throws P * @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) { @@ -430,6 +436,22 @@ private void checkMemo(@Nullable final String memo, final int maxMemoUtf8Bytes) } } + private void checkMaxCustomFee(List 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 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); + } + } + } + /** * This method converts a {@link Timestamp} to an {@link Instant} limited between {@link Instant#MIN} and * {@link Instant#MAX} diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java index 86bf69f4443c..ec7983981ff7 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/validators/ConsensusCustomFeesValidator.java @@ -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; @@ -66,7 +66,7 @@ public void validate( @NonNull final ReadableAccountStore accountStore, @NonNull final ReadableTokenRelationStore tokenRelationStore, @NonNull final ReadableTokenStore tokenStore, - @NonNull final List customFees, + @NonNull final List customFees, @NonNull final ExpiryValidator expiryValidator) { requireNonNull(accountStore); requireNonNull(tokenRelationStore); @@ -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(); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java index 48cbc870a472..21d26c724a37 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusCreateTopicTest.java @@ -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; @@ -121,7 +121,7 @@ private TransactionBody newCreateTxn( Key adminKey, Key submitKey, boolean hasAutoRenewAccount, - List customFees, + List customFees, List feeExemptKeyList) { final var txnId = TransactionID.newBuilder().accountID(payerId).build(); final var createTopicBuilder = ConsensusCreateTopicTransactionBody.newBuilder(); @@ -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()); @@ -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()); @@ -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()); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java index 0e7052e71792..9e6fa1c4e2fa 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java @@ -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; @@ -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 customFees = List.of(ConsensusCustomFee.newBuilder() + protected final List customFees = List.of(FixedCustomFee.newBuilder() .fixedFee(FixedFee.newBuilder().amount(1).build()) .feeCollectorAccountId(anotherPayer) .build()); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java index 9b5b6b7d510a..ef48ddd07830 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java @@ -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. @@ -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; @@ -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; @@ -128,6 +130,7 @@ public abstract class HapiSpecOperation implements SpecOperation { protected Map overrides = Collections.EMPTY_MAP; protected Optional fee = Optional.empty(); + protected List> maxCustomFeeList = new ArrayList<>(); protected Optional validDurationSecs = Optional.empty(); protected Optional customTxnId = Optional.empty(); protected Optional memo = Optional.empty(); @@ -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 keys = signersToUseFor(spec); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java index fcae1cbde7ef..4c9a6f7c2949 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/consensus/HapiGetTopicInfo.java @@ -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; @@ -68,7 +68,7 @@ public class HapiGetTopicInfo extends HapiQueryOp { private Optional feeScheduleKey = Optional.empty(); private final List expectedFeeExemptKeyList = new ArrayList<>(); private boolean expectFeeExemptKeyListEmpty = false; - private final List>> expectedFees = new ArrayList<>(); + private final List>> expectedFees = new ArrayList<>(); private boolean expectNoFees = false; private Optional expectCustomFeeSize = Optional.empty(); private boolean saveRunningHash = false; @@ -171,7 +171,7 @@ public HapiGetTopicInfo hasFeeExemptKeys(List feeExemptKeyAssertion) { return this; } - public HapiGetTopicInfo hasCustomFee(BiConsumer> feeAssertion) { + public HapiGetTopicInfo hasCustomFee(BiConsumer> feeAssertion) { expectedFees.add(feeAssertion); return this; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java index 388c7e9989a5..ed0a200d1fc0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java @@ -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; @@ -635,6 +636,11 @@ public T feeUsd(double price) { return self(); } + public T maxCustomFee(Function f) { + maxCustomFeeList.add(f); + return self(); + } + public T signedByPayerAnd(String... keys) { final String[] copy = new String[keys.length + 1]; copy[0] = DEFAULT_PAYER; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java index 6921fe4f9fdd..08be50e4ee0e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java @@ -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. @@ -34,12 +34,14 @@ import com.hederahashgraph.api.proto.java.ConsensusMessageChunkInfo; import com.hederahashgraph.api.proto.java.ConsensusSubmitMessageTransactionBody; import com.hederahashgraph.api.proto.java.FeeData; +import com.hederahashgraph.api.proto.java.FixedCustomFee; import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.TopicID; import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionID; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -56,6 +58,7 @@ public class HapiMessageSubmit extends HapiTxnOp { private Optional initialTransactionPayer = Optional.empty(); private Optional initialTransactionID = Optional.empty(); private boolean clearMessage = false; + private final List> maxCustomFeeList = new ArrayList<>(); public HapiMessageSubmit(final String topic) { this.topic = Optional.ofNullable(topic); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java index 886fc0039a84..b2976752acd0 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicCreate.java @@ -32,7 +32,7 @@ import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.transactions.HapiTxnOp; import com.hederahashgraph.api.proto.java.ConsensusCreateTopicTransactionBody; -import com.hederahashgraph.api.proto.java.ConsensusCustomFee; +import com.hederahashgraph.api.proto.java.FixedCustomFee; import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.Transaction; @@ -64,7 +64,7 @@ public class HapiTopicCreate extends HapiTxnOp { private Optional feeScheduleKey = Optional.empty(); private Optional feeScheduleKeyName = Optional.empty(); private Optional feeScheduleKeyShape = Optional.empty(); - private final List> feeScheduleSuppliers = new ArrayList<>(); + private final List> feeScheduleSuppliers = new ArrayList<>(); private Optional>> feeExemptKeyNamesList = Optional.empty(); private Optional> feeExemptKeyList = Optional.empty(); @@ -113,7 +113,7 @@ public HapiTopicCreate feeExemptKeys(Key... keys) { return self(); } - public HapiTopicCreate withConsensusCustomFee(final Function supplier) { + public HapiTopicCreate withConsensusCustomFee(final Function supplier) { feeScheduleSuppliers.add(supplier); return this; } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java index 2319e27ea571..8586367df06f 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiTopicUpdate.java @@ -32,10 +32,10 @@ import com.hedera.services.bdd.spec.fees.FeeCalculator; import com.hedera.services.bdd.spec.transactions.HapiTxnOp; import com.hederahashgraph.api.proto.java.ConsensusCreateTopicTransactionBody; -import com.hederahashgraph.api.proto.java.ConsensusCustomFee; -import com.hederahashgraph.api.proto.java.ConsensusCustomFeeList; import com.hederahashgraph.api.proto.java.ConsensusUpdateTopicTransactionBody; import com.hederahashgraph.api.proto.java.FeeExemptKeyList; +import com.hederahashgraph.api.proto.java.FixedCustomFee; +import com.hederahashgraph.api.proto.java.FixedCustomFeeList; import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.ResponseCodeEnum; @@ -63,7 +63,7 @@ public class HapiTopicUpdate extends HapiTxnOp { private Optional newAutoRenewAccount = Optional.empty(); private Optional feeScheduleKey = Optional.empty(); private Optional feeScheduleKeyName = Optional.empty(); - private final List> feeScheduleSuppliers = new ArrayList<>(); + private final List> feeScheduleSuppliers = new ArrayList<>(); private Optional>> feeExemptKeyNamesList = Optional.empty(); private Optional> freeMessageKeyList = Optional.empty(); private boolean emptyCustomFee = false; @@ -126,7 +126,7 @@ public HapiTopicUpdate feeExemptKeys(String... keys) { return this; } - public HapiTopicUpdate withConsensusCustomFee(final Function supplier) { + public HapiTopicUpdate withConsensusCustomFee(final Function supplier) { feeScheduleSuppliers.add(supplier); return this; } @@ -207,14 +207,13 @@ protected Consumer opBodyDef(final HapiSpec spec) throw b.setFeeExemptKeyList(FeeExemptKeyList.newBuilder()); } if (!feeScheduleSuppliers.isEmpty()) { - var consensusCustomFeeList = ConsensusCustomFeeList.newBuilder(); + var consensusCustomFeeList = FixedCustomFeeList.newBuilder(); for (final var supplier : feeScheduleSuppliers) { consensusCustomFeeList.addFees(supplier.apply(spec)); } b.setCustomFees(consensusCustomFeeList.build()); } else if (emptyCustomFee) { - b.setCustomFees( - ConsensusCustomFeeList.newBuilder().build()); + b.setCustomFees(FixedCustomFeeList.newBuilder().build()); } }); return b -> b.setConsensusUpdateTopic(opBody); diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java index 4b01a7fd77a8..dc618baf7907 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeSpecs.java @@ -18,13 +18,15 @@ import static com.hedera.services.bdd.spec.HapiPropertySource.asAccount; import static com.hedera.services.bdd.spec.HapiPropertySource.asToken; +import static com.hedera.services.bdd.spec.transactions.TxnUtils.asId; +import static com.hedera.services.bdd.spec.transactions.TxnUtils.asTokenId; import static com.hedera.services.bdd.spec.transactions.TxnUtils.isIdLiteral; import com.hedera.services.bdd.spec.HapiSpec; -import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.ConsensusCustomFee; import com.hederahashgraph.api.proto.java.CustomFee; +import com.hederahashgraph.api.proto.java.CustomFeeLimit; +import com.hederahashgraph.api.proto.java.FixedCustomFee; import com.hederahashgraph.api.proto.java.FixedFee; import com.hederahashgraph.api.proto.java.Fraction; import com.hederahashgraph.api.proto.java.FractionalFee; @@ -134,7 +136,7 @@ static FixedFee builtFixedHbarSansCollector(long amount) { static CustomFee builtRoyaltyNoFallback( long numerator, long denominator, String collector, boolean allCollectorsExempt, HapiSpec spec) { - final var feeCollector = TxnUtils.asId(collector, spec); + final var feeCollector = asId(collector, spec); return CustomFee.newBuilder() .setRoyaltyFee(baseRoyaltyBuilder(numerator, denominator)) .setFeeCollectorAccountId(feeCollector) @@ -149,7 +151,7 @@ static CustomFee builtRoyaltyWithFallback( boolean allCollectorsExempt, Function fixedFallback, HapiSpec spec) { - final var feeCollector = TxnUtils.asId(collector, spec); + final var feeCollector = asId(collector, spec); final var fallback = fixedFallback.apply(spec); return CustomFee.newBuilder() .setRoyaltyFee(baseRoyaltyBuilder(numerator, denominator).setFallbackFee(fallback)) @@ -174,7 +176,7 @@ static CustomFee builtFixedHts( } static FixedFee builtFixedHtsSansCollector(long amount, String denom, HapiSpec spec) { - final var denomId = TxnUtils.asTokenId(denom, spec); + final var denomId = asTokenId(denom, spec); return FixedFee.newBuilder() .setAmount(amount) .setDenominatingTokenId(denomId) @@ -216,59 +218,75 @@ static CustomFee.Builder baseFixedBuilder( .setFeeCollectorAccountId(collectorId); } - static ConsensusCustomFee.Builder baseConsensusFixedBuilder(long amount, String collector, HapiSpec spec) { + static FixedCustomFee.Builder baseConsensusFixedBuilder(long amount, String collector, HapiSpec spec) { final var collectorId = isIdLiteral(collector) ? asAccount(collector) : spec.registry().getAccountID(collector); final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); - return ConsensusCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collectorId); + return FixedCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collectorId); } - static ConsensusCustomFee.Builder baseConsensusFixedBuilderNoCollector(long amount) { + static FixedCustomFee.Builder baseConsensusFixedBuilderNoCollector(long amount) { final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); - return ConsensusCustomFee.newBuilder().setFixedFee(fixedBuilder); + return FixedCustomFee.newBuilder().setFixedFee(fixedBuilder); } - static ConsensusCustomFee.Builder baseConsensusFixedBuilder(long amount, AccountID collector) { + static FixedCustomFee.Builder baseConsensusFixedBuilder(long amount, AccountID collector) { final var fixedBuilder = FixedFee.newBuilder().setAmount(amount); - return ConsensusCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collector); + return FixedCustomFee.newBuilder().setFixedFee(fixedBuilder).setFeeCollectorAccountId(collector); } // consensus custom fee suppliers - public static Function fixedConsensusHbarFee(long amount, String collector) { + public static Function fixedConsensusHbarFee(long amount, String collector) { return spec -> builtConsensusFixedHbar(amount, collector, spec); } - public static Function fixedConsensusHbarFeeNoCollector(long amount) { + public static Function fixedConsensusHbarFeeNoCollector(long amount) { return spec -> builtConsensusFixedHbarNoCollector(amount); } - public static Function fixedConsensusHbarFee(long amount, AccountID collector) { + public static Function fixedConsensusHbarFee(long amount, AccountID collector) { return spec -> builtConsensusFixedHbar(amount, collector); } - public static Function fixedConsensusHtsFee( - long amount, String denom, String collector) { + public static Function fixedConsensusHtsFee(long amount, String denom, String collector) { return spec -> builtConsensusFixedHts(amount, denom, collector, spec); } // builders - static ConsensusCustomFee builtConsensusFixedHbar(long amount, String collector, HapiSpec spec) { + static FixedCustomFee builtConsensusFixedHbar(long amount, String collector, HapiSpec spec) { return baseConsensusFixedBuilder(amount, collector, spec).build(); } - static ConsensusCustomFee builtConsensusFixedHbarNoCollector(long amount) { + static FixedCustomFee builtConsensusFixedHbarNoCollector(long amount) { return baseConsensusFixedBuilderNoCollector(amount).build(); } - static ConsensusCustomFee builtConsensusFixedHbar(long amount, AccountID collector) { + static FixedCustomFee builtConsensusFixedHbar(long amount, AccountID collector) { return baseConsensusFixedBuilder(amount, collector).build(); } - static ConsensusCustomFee builtConsensusFixedHts(long amount, String denom, String collector, HapiSpec spec) { + static FixedCustomFee builtConsensusFixedHts(long amount, String denom, String collector, HapiSpec spec) { final var builder = baseConsensusFixedBuilder(amount, collector, spec); final var denomId = isIdLiteral(denom) ? asToken(denom) : spec.registry().getTokenID(denom); builder.getFixedFeeBuilder().setDenominatingTokenId(denomId); return builder.build(); } + + public static Function maxCustomFee(String account, long amount) { + return spec -> CustomFeeLimit.newBuilder() + .setAccountId(asId(account, spec)) + .setAmountLimit(FixedFee.newBuilder().setAmount(amount).build()) + .build(); + } + + public static Function maxHtsCustomFee(String account, String token, long amount) { + return spec -> CustomFeeLimit.newBuilder() + .setAccountId(asId(account, spec)) + .setAmountLimit(FixedFee.newBuilder() + .setAmount(amount) + .setDenominatingTokenId(asTokenId(token, spec)) + .build()) + .build(); + } } diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java index 0a7c59b10f97..dfa946f50761 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/token/CustomFeeTests.java @@ -25,8 +25,8 @@ import com.hedera.services.bdd.spec.HapiSpec; import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.ConsensusCustomFee; import com.hederahashgraph.api.proto.java.CustomFee; +import com.hederahashgraph.api.proto.java.FixedCustomFee; import java.util.List; import java.util.OptionalLong; import java.util.function.BiConsumer; @@ -93,7 +93,7 @@ public static BiConsumer> royaltyFeeWithFallbackInToke }; } - public static BiConsumer> expectedConsensusFixedHbarFee( + public static BiConsumer> expectedConsensusFixedHbarFee( long amount, String collector) { return (spec, actual) -> { final var expected = CustomFeeSpecs.builtConsensusFixedHbar(amount, collector, spec); @@ -101,7 +101,7 @@ public static BiConsumer> expectedConsensusFi }; } - public static BiConsumer> expectedConsensusFixedHbarFee( + public static BiConsumer> expectedConsensusFixedHbarFee( long amount, AccountID collector) { return (spec, actual) -> { final var expected = CustomFeeSpecs.builtConsensusFixedHbar(amount, collector); @@ -109,7 +109,7 @@ public static BiConsumer> expectedConsensusFi }; } - public static BiConsumer> expectedConsensusFixedHTSFee( + public static BiConsumer> expectedConsensusFixedHTSFee( long amount, String token, String collector) { return (spec, actual) -> { final var expected = CustomFeeSpecs.builtConsensusFixedHts(amount, token, collector, spec); @@ -127,7 +127,7 @@ private static void failUnlessPresent(String detail, List actual, Cus } private static void failUnlessConsensusFeePresent( - String detail, List actual, ConsensusCustomFee expected) { + String detail, List actual, FixedCustomFee expected) { for (var customFee : actual) { if (expected.equals(customFee)) { return; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java index db19c4d94e44..f0c9b272173d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeCreateTest.java @@ -62,7 +62,7 @@ import com.hedera.services.bdd.spec.SpecOperation; import com.hedera.services.bdd.spec.keys.KeyShape; import com.hedera.services.bdd.spec.keys.SigControl; -import com.hederahashgraph.api.proto.java.ConsensusCustomFee; +import com.hederahashgraph.api.proto.java.FixedCustomFee; import com.hederahashgraph.api.proto.java.Key; import com.hederahashgraph.api.proto.java.TokenSupplyType; import com.hederahashgraph.api.proto.java.TokenType; @@ -538,7 +538,7 @@ final Stream createTopicNotSpecifiedFee() { return hapiTest( cryptoCreate(collector), createTopic(TOPIC) - .withConsensusCustomFee(spec -> ConsensusCustomFee.newBuilder() + .withConsensusCustomFee(spec -> FixedCustomFee.newBuilder() .setFeeCollectorAccountId(spec.registry().getAccountID(collector)) .build()) .hasKnownStatus(CUSTOM_FEE_NOT_FULLY_SPECIFIED)); From 063d69871723c738f92a4cb20af1382fc32fe929 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Fri, 10 Jan 2025 15:33:42 +0200 Subject: [PATCH 90/94] Update protobufs Signed-off-by: Zhivko Kelchev --- .../services/consensus_submit_message.proto | 11 --- .../ConsensusSubmitMessageHandler.java | 40 ++++++----- .../customfee/ConsensusCustomFeeAssessor.java | 6 +- .../ConsensusSubmitMessageHandlerTest.java | 17 ++++- .../impl/test/handlers/ConsensusTestBase.java | 6 +- .../consensus/HapiMessageSubmit.java | 17 ----- .../TopicCustomFeeSubmitMessageTest.java | 69 +++++++++++-------- 7 files changed, 86 insertions(+), 80 deletions(-) diff --git a/hapi/hedera-protobufs/services/consensus_submit_message.proto b/hapi/hedera-protobufs/services/consensus_submit_message.proto index c9b3f3f5057c..3c53c3161fb2 100644 --- a/hapi/hedera-protobufs/services/consensus_submit_message.proto +++ b/hapi/hedera-protobufs/services/consensus_submit_message.proto @@ -34,7 +34,6 @@ option java_package = "com.hederahashgraph.api.proto.java"; option java_multiple_files = true; import "basic_types.proto"; -import "custom_fees.proto"; /** * Consensus message "chunk" detail.
@@ -108,14 +107,4 @@ message ConsensusSubmitMessageTransactionBody { * field SHOULD NOT be set. */ ConsensusMessageChunkInfo chunkInfo = 3; - - /** - * The maximum custom fee that the user is willing to pay for the message. This field will be ignored if `accept_all_custom_fees` is set to `true`. - */ - repeated FixedFee max_custom_fees = 4; - - /** - * If set to true, the transaction will accept all custom fees from the topic id - */ - bool accept_all_custom_fees = 5; } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java index 09be36d4cf24..dfeb2ba1878e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/ConsensusSubmitMessageHandler.java @@ -48,8 +48,8 @@ import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.consensus.ConsensusSubmitMessageTransactionBody; import com.hedera.hapi.node.state.consensus.Topic; -import com.hedera.hapi.node.transaction.ConsensusCustomFee; -import com.hedera.hapi.node.transaction.FixedFee; +import com.hedera.hapi.node.transaction.CustomFeeLimit; +import com.hedera.hapi.node.transaction.FixedCustomFee; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.hapi.utils.CommonPbjConverters; import com.hedera.node.app.service.consensus.ReadableTopicStore; @@ -160,8 +160,8 @@ public void handle(@NonNull final HandleContext handleContext) { final var feesToBeCharged = extractFeesToBeCharged(topic.customFees(), handleContext); // check payer limits or throw - if (!op.acceptAllCustomFees()) { - validateFeeLimits(feesToBeCharged, op.maxCustomFees()); + if (!txn.maxCustomFees().isEmpty()) { + validateFeeLimits(handleContext.payer(), feesToBeCharged, txn.maxCustomFees()); } // create synthetic body and dispatch crypto transfer @@ -376,8 +376,9 @@ public static Predicate simpleKeyVerifierFrom(@NonNull final List sign * @param payerCustomFeeLimits List with limits of fees that the payer is willing to pay */ private void validateFeeLimits( - @NonNull final List topicCustomFees, - @NonNull final List payerCustomFeeLimits) { + @NonNull final AccountID payer, + @NonNull final List topicCustomFees, + @NonNull final List payerCustomFeeLimits) { // Validate the duplication of payer custom fee limits validateDuplicationFeeLimits(payerCustomFeeLimits); @@ -388,9 +389,11 @@ private void validateFeeLimits( // Validate payer token limits tokenFees.forEach((token, feeAmount) -> { final boolean isValid = payerCustomFeeLimits.stream() - .filter(fee -> token.equals(fee.denominatingTokenId())) - .anyMatch(fee -> { - validateTrue(fee.amount() >= feeAmount, MAX_CUSTOM_FEE_LIMIT_EXCEEDED); + .filter(maxCustomFee -> + token.equals(maxCustomFee.amountLimit().denominatingTokenId())) + .filter(maxCustomFee -> payer.equals(maxCustomFee.accountId())) + .anyMatch(maxCustomFee -> { + validateTrue(maxCustomFee.amountLimit().amount() >= feeAmount, MAX_CUSTOM_FEE_LIMIT_EXCEEDED); return true; }); validateTrue(isValid, NO_VALID_MAX_CUSTOM_FEE); @@ -398,10 +401,11 @@ private void validateFeeLimits( // Validate payer HBAR limit if (hbarFee.get() > 0) { final var payerHbarLimit = payerCustomFeeLimits.stream() - .filter(fee -> !fee.hasDenominatingTokenId()) + .filter(maxCustomFee -> !maxCustomFee.amountLimit().hasDenominatingTokenId()) + .filter(maxCustomFee -> payer.equals(maxCustomFee.accountId())) .findFirst() .orElseThrow(() -> new HandleException(NO_VALID_MAX_CUSTOM_FEE)); - validateTrue(payerHbarLimit.amount() >= hbarFee.get(), MAX_CUSTOM_FEE_LIMIT_EXCEEDED); + validateTrue(payerHbarLimit.amountLimit().amount() >= hbarFee.get(), MAX_CUSTOM_FEE_LIMIT_EXCEEDED); } } @@ -413,8 +417,8 @@ private void validateFeeLimits( * @param context The handle context * @return List containing only the fees concerning given payer */ - private List extractFeesToBeCharged( - @NonNull final List topicCustomFees, @NonNull final HandleContext context) { + private List extractFeesToBeCharged( + @NonNull final List topicCustomFees, @NonNull final HandleContext context) { final var payer = context.payer(); final var tokenStore = context.storeFactory().readableStore(ReadableTokenStore.class); return topicCustomFees.stream() @@ -441,7 +445,7 @@ private List extractFeesToBeCharged( * @param tokenFees Map with total amount per token. */ private void totalAmountToBeCharged( - @NonNull List topicCustomFees, + @NonNull List topicCustomFees, AtomicReference hbarFee, Map tokenFees) { for (final var fee : topicCustomFees) { @@ -455,16 +459,16 @@ private void totalAmountToBeCharged( } } - private void validateDuplicationFeeLimits(@NonNull final List payerCustomFeeLimits) { + private void validateDuplicationFeeLimits(@NonNull final List payerCustomFeeLimits) { final var htsCustomFeeLimits = payerCustomFeeLimits.stream() - .filter(FixedFee::hasDenominatingTokenId) + .filter(maxCustomFee -> maxCustomFee.amountLimit().hasDenominatingTokenId()) .toList(); final var hbarCustomFeeLimits = payerCustomFeeLimits.stream() - .filter(fee -> !fee.hasDenominatingTokenId()) + .filter(maxCustomFee -> !maxCustomFee.amountLimit().hasDenominatingTokenId()) .toList(); final var htsLimitHasDuplicate = htsCustomFeeLimits.stream() - .map(FixedFee::denominatingTokenId) + .map(maxCustomFee -> maxCustomFee.amountLimit().denominatingTokenId()) .collect(Collectors.toSet()) .size() != htsCustomFeeLimits.size(); diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java index 25ea9c01ac03..008a99a5d79c 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/handlers/customfee/ConsensusCustomFeeAssessor.java @@ -26,7 +26,7 @@ import com.hedera.hapi.node.base.TokenTransferList; import com.hedera.hapi.node.base.TransferList; import com.hedera.hapi.node.token.CryptoTransferTransactionBody; -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.token.ReadableTokenStore; import edu.umd.cs.findbugs.annotations.NonNull; @@ -55,12 +55,12 @@ public ConsensusCustomFeeAssessor() { * @return List of synthetic crypto transfer transaction bodies */ public List assessCustomFee( - @NonNull final List customFees, @NonNull final AccountID payer) { + @NonNull final List customFees, @NonNull final AccountID payer) { final List transactionBodies = new ArrayList<>(); // build crypto transfer bodies for the first layer of custom fees, // if there is a second or third layer it will be assessed in crypto transfer handler - for (ConsensusCustomFee fee : customFees) { + for (FixedCustomFee fee : customFees) { final var tokenTransfers = new ArrayList(); List hbarTransfers = new ArrayList<>(); diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java index c573a9c698a5..318df273329b 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusSubmitMessageHandlerTest.java @@ -50,6 +50,8 @@ import com.hedera.hapi.node.consensus.ConsensusMessageChunkInfo; import com.hedera.hapi.node.consensus.ConsensusSubmitMessageTransactionBody; import com.hedera.hapi.node.state.consensus.Topic; +import com.hedera.hapi.node.transaction.CustomFeeLimit; +import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.consensus.ReadableTopicStore; import com.hedera.node.app.service.consensus.impl.ReadableTopicStoreImpl; @@ -473,14 +475,27 @@ private TransactionBody newSubmitMessageTxn(final long topicEntityNum, final Str private TransactionBody newSubmitMessageTxnWithMaxFee() { final var txnId = TransactionID.newBuilder().accountID(payerId).build(); + final var maxCustomFees = List.of( + // fungible token limit + CustomFeeLimit.newBuilder() + .accountId(payerId) + .amountLimit(FixedFee.newBuilder() + .denominatingTokenId(fungibleTokenId) + .amount(1)) + .build(), + // hbar limit + CustomFeeLimit.newBuilder() + .accountId(payerId) + .amountLimit(FixedFee.newBuilder().amount(1)) + .build()); final var submitMessageBuilder = ConsensusSubmitMessageTransactionBody.newBuilder() - .maxCustomFees(tokenCustomFee.fixedFee(), hbarCustomFee.fixedFee()) .topicID(TopicID.newBuilder().topicNum(topicEntityNum).build()) .message(Bytes.wrap("Message for test-" + Instant.now() + "." + Instant.now().getNano())); return TransactionBody.newBuilder() .transactionID(txnId) .consensusSubmitMessage(submitMessageBuilder.build()) + .maxCustomFees(maxCustomFees) .build(); } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java index 6b65850e54f9..c222a7863f12 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/ConsensusTestBase.java @@ -124,18 +124,18 @@ public class ConsensusTestBase { protected final long sequenceNumber = 1L; protected final long autoRenewSecs = 100L; protected final Instant consensusTimestamp = Instant.ofEpochSecond(1_234_567L); - protected final ConsensusCustomFee tokenCustomFee = ConsensusCustomFee.newBuilder() + protected final FixedCustomFee tokenCustomFee = FixedCustomFee.newBuilder() .fixedFee(FixedFee.newBuilder() .denominatingTokenId(fungibleTokenId) .amount(1) .build()) .feeCollectorAccountId(anotherPayer) .build(); - protected final ConsensusCustomFee hbarCustomFee = ConsensusCustomFee.newBuilder() + protected final FixedCustomFee hbarCustomFee = FixedCustomFee.newBuilder() .fixedFee(FixedFee.newBuilder().amount(1).build()) .feeCollectorAccountId(anotherPayer) .build(); - protected final List customFees = List.of(tokenCustomFee, hbarCustomFee); + protected final List customFees = List.of(tokenCustomFee, hbarCustomFee); protected Topic topic; diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java index 8c5a307d7214..08be50e4ee0e 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/consensus/HapiMessageSubmit.java @@ -31,7 +31,6 @@ import com.hedera.services.bdd.spec.HapiSpec; import com.hedera.services.bdd.spec.fees.AdapterUtils; import com.hedera.services.bdd.spec.transactions.HapiTxnOp; -import com.hederahashgraph.api.proto.java.ConsensusCustomFee; import com.hederahashgraph.api.proto.java.ConsensusMessageChunkInfo; import com.hederahashgraph.api.proto.java.ConsensusSubmitMessageTransactionBody; import com.hederahashgraph.api.proto.java.FeeData; @@ -125,16 +124,6 @@ public HapiMessageSubmit chunkInfo( return chunkInfo(totalChunks, chunkNumber); } - public HapiMessageSubmit maxCustomFee(Function f) { - maxCustomFeeList.add(f); - return this; - } - - public HapiMessageSubmit acceptAllCustomFees(boolean acceptAllFees) { - this.acceptAllCustomFees = acceptAllFees; - return this; - } - @Override protected Consumer opBodyDef(final HapiSpec spec) throws Throwable { final TopicID id = resolveTopicId(spec); @@ -146,12 +135,6 @@ protected Consumer opBodyDef(final HapiSpec spec) throw if (clearMessage) { b.clearMessage(); } - if (!maxCustomFeeList.isEmpty()) { - for (final var supplier : maxCustomFeeList) { - b.addMaxCustomFees(supplier.apply(spec).getFixedFee()); - } - } - b.setAcceptAllCustomFees(acceptAllCustomFees); if (totalChunks.isPresent() && chunkNumber.isPresent()) { final ConsensusMessageChunkInfo chunkInfo = ConsensusMessageChunkInfo.newBuilder() .setInitialTransactionID(initialTransactionID.orElse(asTransactionID( diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 3737bd12565a..805cf9dd693d 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -28,6 +28,8 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.tokenAssociate; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHbarFee; import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.fixedConsensusHtsFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.maxCustomFee; +import static com.hedera.services.bdd.spec.transactions.token.CustomFeeSpecs.maxHtsCustomFee; import static com.hedera.services.bdd.spec.transactions.token.TokenMovement.moving; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.createHollow; @@ -73,7 +75,7 @@ final Stream messageSubmitToPublicTopicWithFee1Hbar() { return hapiTest( cryptoCreate(collector).balance(0L), createTopic(TOPIC).withConsensusCustomFee(fee), - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTinyBars(ONE_HBAR)); } @@ -87,7 +89,7 @@ final Stream messageSubmitToPublicTopicWithFee1token() { cryptoCreate(collector), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fee), - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1)); } @@ -108,7 +110,7 @@ final Stream messageSubmitToPublicTopicWith3layerFee() { tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fee), // submit message - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), // assert token fee collector balance @@ -128,10 +130,7 @@ final Stream messageSubmitToPublicTopicWith10different2layerFees() associateAllTokensToCollectors(), // create topic with 10 multilayer fees - 9 HTS + 1 HBAR createTopicWith10Different2layerFees(), - submitMessageTo(TOPIC) - .acceptAllCustomFees(true) - .message("TEST") - .payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // assert topic fee collector balance assertAllCollectorsBalances())); } @@ -189,7 +188,7 @@ final Stream treasurySubmitToPublicTopicWith3layerFees() { tokenAssociate(topicFeeCollector, token), createTopic(TOPIC).withConsensusCustomFee(fee), // submit message - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(TOKEN_TREASURY), + submitMessageTo(TOPIC).message("TEST").payingWith(TOKEN_TREASURY), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), // assert token fee collector balance @@ -221,7 +220,7 @@ final Stream treasuryOfSecondLayerSubmitToPublic() { createTopic(TOPIC).withConsensusCustomFee(fee), // submit - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(DENOM_TREASURY), + submitMessageTo(TOPIC).message("TEST").payingWith(DENOM_TREASURY), // assert topic fee collector balance getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), @@ -251,7 +250,7 @@ final Stream collectorSubmitToPublicTopicWith3layerFees() { createTopic(TOPIC).withConsensusCustomFee(fee), // submit - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(topicFeeCollector), + submitMessageTo(TOPIC).message("TEST").payingWith(topicFeeCollector), // assert balances getAccountBalance(topicFeeCollector).hasTokenBalance(token, 0), @@ -281,7 +280,7 @@ final Stream collectorOfSecondLayerSubmitToPublicTopicWith3layerFee createTopic(TOPIC).withConsensusCustomFee(fee), // submit - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(secondLayerFeeCollector), + submitMessageTo(TOPIC).message("TEST").payingWith(secondLayerFeeCollector), // assert topic fee collector balance - only first layer fee should be paid getAccountBalance(topicFeeCollector).hasTokenBalance(token, 1), @@ -308,7 +307,7 @@ final Stream anotherCollectorSubmitMessageToATopicWithAFee() { cryptoCreate(collector), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fee), - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(anotherCollector), + submitMessageTo(TOPIC).message("TEST").payingWith(anotherCollector), // the fee was paid getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 1))); } @@ -323,7 +322,7 @@ final Stream messageTopicSubmitToHollowAccountAsFeeCollector() { // create hollow account with ONE_HUNDRED_HBARS createHollow(1, i -> collector), createTopic(TOPIC).withConsensusCustomFee(fee), - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(SUBMITTER), + submitMessageTo(TOPIC).message("TEST").payingWith(SUBMITTER), // collector should be still a hollow account // and should have the initial balance + ONE_HBAR fee @@ -346,7 +345,6 @@ final Stream accountMessageSubmitAndSignsWithFeeScheduleKey() { .feeExemptKeys(feeScheduleKey) .withConsensusCustomFee(fee), submitMessageTo(TOPIC) - .maxCustomFee(fee) .message("TEST") .signedByPayerAnd(feeScheduleKey) // any non payer key in FEKL, should sign with full prefixes keys @@ -364,7 +362,7 @@ final Stream collectorSubmitMessageToTopicWithFTFee() { cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fee), - submitMessageTo(TOPIC).maxCustomFee(fee).message("TEST").payingWith(collector), + submitMessageTo(TOPIC).message("TEST").payingWith(collector), getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); } @@ -377,11 +375,7 @@ final Stream collectorSubmitMessageToTopicWithHbarFee() { return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), createTopic(TOPIC).withConsensusCustomFee(fee), - submitMessageTo(TOPIC) - .maxCustomFee(fee) - .message("TEST") - .payingWith(collector) - .via("submit"), + submitMessageTo(TOPIC).message("TEST").payingWith(collector).via("submit"), // assert collector's tinyBars balance withOpContext((spec, log) -> { final var submitTxnRecord = getTxnRecord("submit"); @@ -398,9 +392,10 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { final var collector = "collector"; final var secondCollector = "secondCollector"; final var fee1 = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var fee1Limit = maxHtsCustomFee(collector, BASE_TOKEN, 1); final var fee2 = fixedConsensusHtsFee(1, SECOND_TOKEN, secondCollector); + final var fee2Limit = maxHtsCustomFee(collector, SECOND_TOKEN, 1); return hapiTest( - // todo create and associate collector in beforeAll() cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN, SECOND_TOKEN), // create second collector and send second token @@ -410,8 +405,8 @@ final Stream collectorSubmitMessageToTopicWith2differentFees() { // create topic with two fees createTopic(TOPIC).withConsensusCustomFee(fee1).withConsensusCustomFee(fee2), submitMessageTo(TOPIC) - .maxCustomFee(fee1) - .maxCustomFee(fee2) + .maxCustomFee(fee1Limit) + .maxCustomFee(fee2Limit) .message("TEST") .payingWith(collector), // only second fee should be paid @@ -425,13 +420,16 @@ final Stream multipleFeesSameDenom() { final var collector = "collector"; final var fee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); final var fee1 = fixedConsensusHtsFee(1, BASE_TOKEN, collector); - final var correctFeeLimit = fixedConsensusHtsFee(3, BASE_TOKEN, collector); + + final var feeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 1); + final var correctFeeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 3); + return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fee).withConsensusCustomFee(fee1), submitMessageTo(TOPIC) - .maxCustomFee(fee) + .maxCustomFee(feeLimit) .message("TEST") .payingWith(SUBMITTER) .hasKnownStatus(ResponseCodeEnum.MAX_CUSTOM_FEE_LIMIT_EXCEEDED), @@ -447,13 +445,16 @@ final Stream multipleHbarFees() { final var collector = "collector"; final var fee = fixedConsensusHbarFee(2, collector); final var fee1 = fixedConsensusHbarFee(1, collector); - final var correctFeeLimit = fixedConsensusHbarFee(3, collector); + + final var feeLimit = maxCustomFee(SUBMITTER, 2); + final var correctFeeLimit = maxCustomFee(SUBMITTER, 3); + return hapiTest( cryptoCreate(collector).balance(ONE_HBAR), tokenAssociate(collector, BASE_TOKEN), createTopic(TOPIC).withConsensusCustomFee(fee).withConsensusCustomFee(fee1), submitMessageTo(TOPIC) - .maxCustomFee(fee) + .maxCustomFee(feeLimit) .message("TEST") .payingWith(SUBMITTER) .hasKnownStatus(ResponseCodeEnum.MAX_CUSTOM_FEE_LIMIT_EXCEEDED), @@ -463,4 +464,18 @@ final Stream multipleHbarFees() { .payingWith(SUBMITTER)); } } + + @HapiTest + @DisplayName("Max custom fee is supported only on consensus message submit") + final Stream maxCustomFeesIsSupportedOnlyWithMsgSubmit() { + final var sender = "sender"; + final var receiver = "receiver"; + final var feeLimit = maxCustomFee(sender, 2); + return hapiTest( + cryptoCreate(sender).balance(ONE_HBAR), + cryptoCreate(receiver), + cryptoTransfer(TokenMovement.movingHbar(1).between(sender, receiver)) + .maxCustomFee(feeLimit) + .hasPrecheck(ResponseCodeEnum.MAX_CUSTOM_FEES_IS_NOT_SUPPORTED)); + } } From abb91186196a144ea4925919f2c0302e7abf1c09 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 13 Jan 2025 16:13:02 +0200 Subject: [PATCH 91/94] Update doc Signed-off-by: Zhivko Kelchev --- hapi/hedera-protobufs/services/transaction_body.proto | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index 747aeceb88a6..1a1a9aa0f97a 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -566,8 +566,9 @@ message TransactionBody { /** * A list of maximum custom fees that the users are willing to pay. *

- * This field is OPTIONAL. If it is not set then users are accepting to pay any custom fee. - * If it is set, but the transaction doesn't support it, the transaction will fail. + * This field is OPTIONAL.
+ * If left empty, the users are accepting to pay any custom fee.
+ * If used with a transaction type that does not support custom fee limits, the transaction will fail. */ repeated CustomFeeLimit maxCustomFees = 66; } From a616404e2b8c4ef89770146518290b4fed11934f Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Tue, 14 Jan 2025 10:32:46 +0200 Subject: [PATCH 92/94] Change ordinal of CustomFeeLimit according to HIP changes Signed-off-by: Zhivko Kelchev --- hapi/hedera-protobufs/services/transaction_body.proto | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hapi/hedera-protobufs/services/transaction_body.proto b/hapi/hedera-protobufs/services/transaction_body.proto index 1a1a9aa0f97a..3344b3cebacf 100644 --- a/hapi/hedera-protobufs/services/transaction_body.proto +++ b/hapi/hedera-protobufs/services/transaction_body.proto @@ -570,5 +570,5 @@ message TransactionBody { * If left empty, the users are accepting to pay any custom fee.
* If used with a transaction type that does not support custom fee limits, the transaction will fail. */ - repeated CustomFeeLimit maxCustomFees = 66; + repeated CustomFeeLimit maxCustomFees = 1001; } From 243cb2e3af75f9c231cda2147d081e267a4672b0 Mon Sep 17 00:00:00 2001 From: ibankov Date: Tue, 14 Jan 2025 13:04:08 +0200 Subject: [PATCH 93/94] fix tests Signed-off-by: ibankov --- .../hip991/TopicCustomFeeSubmitMessageTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 7ce40ec766a2..24cdb536235a 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -393,7 +393,9 @@ final Stream collectorSubmitMessageToTopicWithHbarFee() { final Stream submitMessageAfterUpdate() { final var collector = "collector"; final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var feeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 1); final var updatedFee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); + final var updatedFeeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 2); return hapiTest( newKeyNamed("adminKey"), newKeyNamed("feeScheduleKey"), @@ -404,7 +406,7 @@ final Stream submitMessageAfterUpdate() { .adminKeyName("adminKey") .feeScheduleKeyName("feeScheduleKey"), submitMessageTo(TOPIC) - .maxCustomFee(fee) + .maxCustomFee(feeLimit) .message("TEST") .payingWith(SUBMITTER) .via("submit"), @@ -413,7 +415,7 @@ final Stream submitMessageAfterUpdate() { .withConsensusCustomFee(updatedFee) .signedByPayerAnd("adminKey", "feeScheduleKey"), submitMessageTo(TOPIC) - .maxCustomFee(updatedFee) + .maxCustomFee(updatedFeeLimit) .message("TEST") .payingWith(SUBMITTER) .via("submit2"), @@ -426,6 +428,7 @@ final Stream submitMessageAfterUpdate() { final Stream submitMessageAfterFEKLisRemoved() { final var collector = "collector"; final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var feeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 1); return hapiTest( newKeyNamed("adminKey"), cryptoCreate(collector).balance(ONE_HBAR), @@ -438,7 +441,7 @@ final Stream submitMessageAfterFEKLisRemoved() { getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0), updateTopic(TOPIC).feeExemptKeys().signedByPayerAnd("adminKey"), submitMessageTo(TOPIC) - .maxCustomFee(fee) + .maxCustomFee(feeLimit) .message("TEST") .payingWith(SUBMITTER) .via("submit2"), @@ -451,6 +454,7 @@ final Stream submitMessageAfterFEKLisRemoved() { final Stream submitMessageAfterFeeIsAdded() { final var collector = "collector"; final var fee = fixedConsensusHtsFee(1, BASE_TOKEN, collector); + final var feeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 1); return hapiTest( newKeyNamed("adminKey"), newKeyNamed("feeScheduleKey"), @@ -461,7 +465,7 @@ final Stream submitMessageAfterFeeIsAdded() { getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0), updateTopic(TOPIC).withConsensusCustomFee(fee).signedByPayerAnd("adminKey", "feeScheduleKey"), submitMessageTo(TOPIC) - .maxCustomFee(fee) + .maxCustomFee(feeLimit) .message("TEST") .payingWith(SUBMITTER) .via("submit2"), From b4de30a6f78823bb2a103b5e32ead2a10221698a Mon Sep 17 00:00:00 2001 From: ibankov Date: Mon, 20 Jan 2025 12:40:17 +0200 Subject: [PATCH 94/94] merge conflicts Signed-off-by: ibankov --- .../TopicCustomFeeSubmitMessageTest.java | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java index 8c9034085b39..80f14ce26d9b 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/suites/hip991/TopicCustomFeeSubmitMessageTest.java @@ -1030,6 +1030,41 @@ final Stream submitDoesNotHaveEnoughBalanceToPayWithSubmitterAsColl .payingWith(alice) .hasKnownStatus(INSUFFICIENT_TOKEN_BALANCE)); } + + @HapiTest + @DisplayName("Max custom fee is supported only on consensus message submit") + final Stream maxCustomFeesIsSupportedOnlyWithMsgSubmit() { + final var sender = "sender"; + final var receiver = "receiver"; + final var feeLimit = maxCustomFee(sender, 2); + return hapiTest( + cryptoCreate(sender).balance(ONE_HBAR), + cryptoCreate(receiver), + cryptoTransfer(TokenMovement.movingHbar(1).between(sender, receiver)) + .maxCustomFee(feeLimit) + .hasPrecheck(ResponseCodeEnum.MAX_CUSTOM_FEES_IS_NOT_SUPPORTED)); + } + + @HapiTest + @DisplayName("Max custom fee contain duplicate denominations") + final Stream maxCustomFeeContainsDuplicateDenominations() { + final var collector = "collector"; + final var fee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); + final var feeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 2); + final var feeLimit2 = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 10); + return hapiTest(flattened( + associateFeeTokensAndSubmitter(), + cryptoCreate(collector).balance(ONE_HBAR), + tokenAssociate(collector, BASE_TOKEN), + createTopic(TOPIC).withConsensusCustomFee(fee), + submitMessageTo(TOPIC) + // duplicate denominations in maxCustomFee + .maxCustomFee(feeLimit) + .maxCustomFee(feeLimit2) + .message("TEST") + .payingWith(SUBMITTER) + .hasPrecheck(DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST))); + } } @Nested @@ -1115,39 +1150,4 @@ final Stream SubmitWithCollectorAndFEKLIsEmpty() { getAccountBalance(collector).hasTokenBalance(BASE_TOKEN, 0L)); } } - - @HapiTest - @DisplayName("Max custom fee is supported only on consensus message submit") - final Stream maxCustomFeesIsSupportedOnlyWithMsgSubmit() { - final var sender = "sender"; - final var receiver = "receiver"; - final var feeLimit = maxCustomFee(sender, 2); - return hapiTest( - cryptoCreate(sender).balance(ONE_HBAR), - cryptoCreate(receiver), - cryptoTransfer(TokenMovement.movingHbar(1).between(sender, receiver)) - .maxCustomFee(feeLimit) - .hasPrecheck(ResponseCodeEnum.MAX_CUSTOM_FEES_IS_NOT_SUPPORTED)); - } - - @HapiTest - @DisplayName("Max custom fee contain duplicate denominations") - final Stream maxCustomFeeContainsDuplicateDenominations() { - final var collector = "collector"; - final var fee = fixedConsensusHtsFee(2, BASE_TOKEN, collector); - final var feeLimit = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 2); - final var feeLimit2 = maxHtsCustomFee(SUBMITTER, BASE_TOKEN, 10); - return hapiTest(flattened( - associateFeeTokensAndSubmitter(), - cryptoCreate(collector).balance(ONE_HBAR), - tokenAssociate(collector, BASE_TOKEN), - createTopic(TOPIC).withConsensusCustomFee(fee), - submitMessageTo(TOPIC) - // duplicate denominations in maxCustomFee - .maxCustomFee(feeLimit) - .maxCustomFee(feeLimit2) - .message("TEST") - .payingWith(SUBMITTER) - .hasPrecheck(DUPLICATE_DENOMINATION_IN_MAX_CUSTOM_FEE_LIST))); - } }