From c7e22647df9658a36c5c125c4aec8f637a017eb2 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 16 Dec 2024 15:35:27 +0200 Subject: [PATCH 1/2] Suppress child records Signed-off-by: Zhivko Kelchev --- .../stream/output/consensus_service.proto | 12 +++- .../stream/output/transaction_output.proto | 6 ++ .../node/app/blocks/BlockItemsTranslator.java | 7 ++ .../app/blocks/impl/BlockStreamBuilder.java | 5 ++ .../ConsensusSubmitMessageHandler.java | 64 +++++++++++++++++-- .../ConsensusSubmitMessageStreamBuilder.java | 11 ++++ .../records/CryptoTransferStreamBuilder.java | 8 +++ .../impl/SubmitMessageTranslator.java | 4 ++ 8 files changed, 109 insertions(+), 8 deletions(-) diff --git a/hapi/hedera-protobufs/block/stream/output/consensus_service.proto b/hapi/hedera-protobufs/block/stream/output/consensus_service.proto index f9f3bab59062..1b815485d374 100644 --- a/hapi/hedera-protobufs/block/stream/output/consensus_service.proto +++ b/hapi/hedera-protobufs/block/stream/output/consensus_service.proto @@ -23,6 +23,8 @@ syntax = "proto3"; package com.hedera.hapi.block.stream.output; +import "custom_fees.proto"; + /* * Copyright (C) 2024 Hedera Hashgraph, LLC * @@ -75,7 +77,15 @@ message DeleteTopicOutput {} * The actual topic running hash SHALL be present in a `StateChanges` block * item, and is not duplicated here. */ -message SubmitMessageOutput {} +message SubmitMessageOutput { + + /** + * Custom fees assessed during a SubmitMessage. + *

+ * These fees SHALL be present in the full transfer list for the transaction. + */ + repeated proto.AssessedCustomFee assessed_custom_fees = 1; +} /** * A version of the topic running hash. diff --git a/hapi/hedera-protobufs/block/stream/output/transaction_output.proto b/hapi/hedera-protobufs/block/stream/output/transaction_output.proto index 7730c60f86c2..b3f926a88792 100644 --- a/hapi/hedera-protobufs/block/stream/output/transaction_output.proto +++ b/hapi/hedera-protobufs/block/stream/output/transaction_output.proto @@ -40,6 +40,7 @@ import "stream/output/util_service.proto"; import "stream/output/crypto_service.proto"; import "stream/output/token_service.proto"; import "stream/output/smart_contract_service.proto"; +import "stream/output/consensus_service.proto"; /** * Output from a transaction. @@ -155,5 +156,10 @@ message TransactionOutput { * Output from a token airdrop transaction. */ TokenAirdropOutput token_airdrop = 8; + + /** + * Output from a consensus submit message transaction. + */ + SubmitMessageOutput submit_message = 9; } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemsTranslator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemsTranslator.java index ba0b0f97db97..fe237af9cedb 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemsTranslator.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/BlockItemsTranslator.java @@ -182,6 +182,13 @@ public TransactionRecord translateRecord( recordBuilder.assessedCustomFees(cryptoOutput.assessedCustomFees()); } } + case CONSENSUS_SUBMIT_MESSAGE -> { + final var submitMessageOutput = outputValueIfPresent( + TransactionOutput::hasSubmitMessage, TransactionOutput::submitMessageOrThrow, outputs); + if (submitMessageOutput != null) { + recordBuilder.assessedCustomFees(submitMessageOutput.assessedCustomFees()); + } + } case CRYPTO_CREATE, CRYPTO_UPDATE -> recordBuilder.evmAddress( ((CryptoOpContext) context).evmAddress()); case TOKEN_AIRDROP -> recordBuilder.newPendingAirdrops( diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamBuilder.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamBuilder.java index 50bc71243f76..8cb9686a2ef4 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamBuilder.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/blocks/impl/BlockStreamBuilder.java @@ -16,6 +16,7 @@ package com.hedera.node.app.blocks.impl; +import static com.hedera.hapi.node.base.HederaFunctionality.CONSENSUS_SUBMIT_MESSAGE; import static com.hedera.hapi.node.base.HederaFunctionality.CRYPTO_TRANSFER; import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_AIRDROP; import static com.hedera.hapi.node.base.ResponseCodeEnum.IDENTICAL_SCHEDULE_ALREADY_CREATED; @@ -35,6 +36,7 @@ import com.hedera.hapi.block.stream.output.SignScheduleOutput; import com.hedera.hapi.block.stream.output.StateChange; import com.hedera.hapi.block.stream.output.StateChanges; +import com.hedera.hapi.block.stream.output.SubmitMessageOutput; import com.hedera.hapi.block.stream.output.TokenAirdropOutput; import com.hedera.hapi.block.stream.output.TransactionOutput; import com.hedera.hapi.block.stream.output.TransactionResult; @@ -1124,6 +1126,9 @@ private void addOutputItemsTo(@NonNull final List items) { } else if (functionality == TOKEN_AIRDROP && hasAssessedCustomFees) { items.add( itemWith(TransactionOutput.newBuilder().tokenAirdrop(new TokenAirdropOutput(assessedCustomFees)))); + } else if (functionality == CONSENSUS_SUBMIT_MESSAGE && hasAssessedCustomFees) { + items.add(itemWith( + TransactionOutput.newBuilder().submitMessage(new SubmitMessageOutput(assessedCustomFees)))); } } 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..3daeb97f4337 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 @@ -36,7 +36,7 @@ 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 com.hedera.node.app.spi.workflows.record.StreamBuilder.TransactionCustomizer.SUPPRESSING_TRANSACTION_CUSTOMIZER; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; @@ -47,6 +47,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.token.CryptoTransferTransactionBody; +import com.hedera.hapi.node.transaction.AssessedCustomFee; import com.hedera.hapi.node.transaction.ConsensusCustomFee; import com.hedera.hapi.node.transaction.FixedFee; import com.hedera.hapi.node.transaction.TransactionBody; @@ -75,6 +77,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.stream.Collectors; @@ -153,6 +156,9 @@ public void handle(@NonNull final HandleContext handleContext) { final var config = handleContext.configuration().getConfigData(ConsensusConfig.class); validateTransaction(txn, config, topic); + final var streamBuilder = + handleContext.savepointStack().getBaseBuilder(ConsensusSubmitMessageStreamBuilder.class); + /* handle custom fees */ if (!topic.customFees().isEmpty() && !isFeeExempted(topic.feeExemptKeyList(), handleContext.keyVerifier())) { // check payer limits or throw @@ -161,16 +167,31 @@ public void handle(@NonNull final HandleContext handleContext) { } // create synthetic body and dispatch crypto transfer final var syntheticBodies = customFeeAssessor.assessCustomFee(topic, handleContext); + // build a list with the top level custom fees + final var assessedCustomFees = + new ArrayList<>(buildTopLevelAssessedCustomFees(handleContext.payer(), syntheticBodies)); + + // dispatch transfers to pay the fees, but suppress any child records. All assessed fees will be + // externalized in to the top level txn record/stream for (final var syntheticBody : syntheticBodies) { - final var record = handleContext.dispatch(stepDispatch( + final var dispatchedStreamBuilder = handleContext.dispatch(stepDispatch( handleContext.payer(), TransactionBody.newBuilder() .cryptoTransfer(syntheticBody) .build(), CryptoTransferStreamBuilder.class, - NOOP_TRANSACTION_CUSTOMIZER)); - validateTrue(record.status().equals(SUCCESS), record.status()); + SUPPRESSING_TRANSACTION_CUSTOMIZER)); + + validateTrue(dispatchedStreamBuilder.status().equals(SUCCESS), dispatchedStreamBuilder.status()); + + // check if there is nested custom fees + if (!dispatchedStreamBuilder.getAssessedCustomFees().isEmpty()) { + assessedCustomFees.addAll(dispatchedStreamBuilder.getAssessedCustomFees()); + } } + + // externalize all custom fees + streamBuilder.assessedCustomFees(assessedCustomFees); } try { @@ -180,9 +201,7 @@ final var record = handleContext.dispatch(stepDispatch( It will not be committed to state until commit is called on the state.--- */ topicStore.put(updatedTopic); - final var recordBuilder = - handleContext.savepointStack().getBaseBuilder(ConsensusSubmitMessageStreamBuilder.class); - recordBuilder + streamBuilder .topicRunningHash(updatedTopic.runningHash()) .topicSequenceNumber(updatedTopic.sequenceNumber()) .topicRunningHashVersion(RUNNING_HASH_VERSION); @@ -426,4 +445,35 @@ public Fees calculateFees(@NonNull final FeeContext feeContext) { .addNetworkRamByteSeconds((LONG_SIZE + TX_HASH_SIZE) * RECEIPT_STORAGE_TIME_SEC) .calculate(); } + + private List buildTopLevelAssessedCustomFees( + AccountID payer, List bodies) { + final var assessedCustomFees = new ArrayList(); + for (final var body : bodies) { + final var customFeeBuilder = AssessedCustomFee.newBuilder().effectivePayerAccountId(payer); + if (body.tokenTransfers().isEmpty()) { + final var aa = body.transfers().accountAmounts(); + for (final var amount : aa) { + if (amount.amount() > 0) { + customFeeBuilder.amount(amount.amount()); + customFeeBuilder.feeCollectorAccountId(amount.accountID()); + } + } + } else { + final var tokenTransferLists = body.tokenTransfers(); + for (final var tokenTransferList : tokenTransferLists) { + customFeeBuilder.tokenId(tokenTransferList.token()); + for (final var transfer : tokenTransferList.transfers()) { + if (transfer.amount() > 0) { + customFeeBuilder.amount(transfer.amount()); + customFeeBuilder.feeCollectorAccountId(transfer.accountID()); + } + } + } + } + assessedCustomFees.add(customFeeBuilder.build()); + } + + return assessedCustomFees; + } } diff --git a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/records/ConsensusSubmitMessageStreamBuilder.java b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/records/ConsensusSubmitMessageStreamBuilder.java index 1c58e9cff68e..06ef857ac95e 100644 --- a/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/records/ConsensusSubmitMessageStreamBuilder.java +++ b/hedera-node/hedera-consensus-service-impl/src/main/java/com/hedera/node/app/service/consensus/impl/records/ConsensusSubmitMessageStreamBuilder.java @@ -16,9 +16,12 @@ package com.hedera.node.app.service.consensus.impl.records; +import com.hedera.hapi.node.transaction.AssessedCustomFee; +import com.hedera.node.app.service.token.records.CryptoTransferStreamBuilder; import com.hedera.node.app.spi.workflows.record.StreamBuilder; import com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.List; /** * A {@code StreamBuilder} specialization for tracking the side effects of a {@code ConsensusSubmitMessage} @@ -51,4 +54,12 @@ public interface ConsensusSubmitMessageStreamBuilder extends StreamBuilder { */ @NonNull ConsensusSubmitMessageStreamBuilder topicRunningHashVersion(long topicRunningHashVersion); + + /** + * Tracks the total custom fees assessed in the transaction. + * @param assessedCustomFees the total custom fees assessed in the transaction + * @return this builder + */ + @NonNull + CryptoTransferStreamBuilder assessedCustomFees(@NonNull List assessedCustomFees); } diff --git a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/CryptoTransferStreamBuilder.java b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/CryptoTransferStreamBuilder.java index cbc375a30f0c..111a43ac22c4 100644 --- a/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/CryptoTransferStreamBuilder.java +++ b/hedera-node/hedera-token-service/src/main/java/com/hedera/node/app/service/token/records/CryptoTransferStreamBuilder.java @@ -53,6 +53,14 @@ public interface CryptoTransferStreamBuilder extends StreamBuilder { @NonNull CryptoTransferStreamBuilder tokenTransferLists(@NonNull List tokenTransferLists); + /** + * Returns all assessed custom fees for this call. + * + * @return the assessed custom fees + */ + @NonNull + List getAssessedCustomFees(); + /** * Tracks the total custom fees assessed in the transaction. * @param assessedCustomFees the total custom fees assessed in the transaction diff --git a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/impl/SubmitMessageTranslator.java b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/impl/SubmitMessageTranslator.java index ad75a65a233f..4d984d1a2039 100644 --- a/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/impl/SubmitMessageTranslator.java +++ b/hedera-node/test-clients/src/main/java/com/hedera/services/bdd/junit/support/translators/impl/SubmitMessageTranslator.java @@ -19,6 +19,7 @@ import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS; import com.hedera.hapi.block.stream.output.StateChange; +import com.hedera.hapi.block.stream.output.TransactionOutput; import com.hedera.node.app.state.SingleTransactionRecord; import com.hedera.services.bdd.junit.support.translators.BaseTranslator; import com.hedera.services.bdd.junit.support.translators.BlockTransactionPartsTranslator; @@ -45,6 +46,9 @@ public SingleTransactionRecord translate( @NonNull final List remainingStateChanges) { return baseTranslator.recordFrom(parts, (receiptBuilder, recordBuilder) -> { if (parts.status() == SUCCESS) { + parts.outputIfPresent(TransactionOutput.TransactionOneOfType.SUBMIT_MESSAGE) + .ifPresent(output -> recordBuilder.assessedCustomFees( + output.submitMessageOrThrow().assessedCustomFees())); receiptBuilder.topicRunningHashVersion(RUNNING_HASH_VERSION); final var iter = remainingStateChanges.listIterator(); while (iter.hasNext()) { From af9a660719091c5e63dd32db9ed6ffd74ddf6696 Mon Sep 17 00:00:00 2001 From: Zhivko Kelchev Date: Mon, 16 Dec 2024 16:01:08 +0200 Subject: [PATCH 2/2] Spotless Signed-off-by: Zhivko Kelchev --- .../consensus/impl/handlers/ConsensusSubmitMessageHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 464c1174efae..50614deac75b 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 @@ -78,8 +78,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Instant; -import java.util.HashMap; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map;