diff --git a/hapi-utils/src/test/java/com/hedera/services/legacy/proto/utils/SignatureGeneratorTest.java b/hapi-utils/src/test/java/com/hedera/services/legacy/proto/utils/SignatureGeneratorTest.java index c76bbef3b5cc..6fff66616c41 100644 --- a/hapi-utils/src/test/java/com/hedera/services/legacy/proto/utils/SignatureGeneratorTest.java +++ b/hapi-utils/src/test/java/com/hedera/services/legacy/proto/utils/SignatureGeneratorTest.java @@ -1,5 +1,25 @@ package com.hedera.services.legacy.proto.utils; +/*- + * ‌ + * Hedera Services API Utilities + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -10,4 +30,4 @@ void rejectsNonEddsaKeys() { IllegalArgumentException.class, () -> SignatureGenerator.signBytes(new byte[0], null)); } -} \ No newline at end of file +} diff --git a/hedera-node/configuration/dev/api-permission.properties b/hedera-node/configuration/dev/api-permission.properties index bcabdf789eba..87289571fe42 100644 --- a/hedera-node/configuration/dev/api-permission.properties +++ b/hedera-node/configuration/dev/api-permission.properties @@ -54,3 +54,4 @@ getVersionInfo=0-* systemDelete=2-59 systemUndelete=2-60 freeze=2-58 +uncheckedSubmit=2-50 diff --git a/hedera-node/src/main/java/com/hedera/services/context/AwareTransactionContext.java b/hedera-node/src/main/java/com/hedera/services/context/AwareTransactionContext.java index a474cfaae4e6..072280f49bc1 100644 --- a/hedera-node/src/main/java/com/hedera/services/context/AwareTransactionContext.java +++ b/hedera-node/src/main/java/com/hedera/services/context/AwareTransactionContext.java @@ -25,7 +25,6 @@ import com.hedera.services.state.expiry.ExpiringEntity; import com.hedera.services.state.merkle.MerkleTopic; import com.hedera.services.utils.TxnAccessor; -import com.hederahashgraph.api.proto.java.AccountAmount; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractFunctionResult; import com.hederahashgraph.api.proto.java.ContractID; @@ -40,8 +39,6 @@ import com.hederahashgraph.api.proto.java.TransactionID; import com.hederahashgraph.api.proto.java.TransactionReceipt; import com.hederahashgraph.api.proto.java.TransactionRecord; -import com.hederahashgraph.api.proto.java.TransferList; -import com.swirlds.common.Address; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -54,8 +51,6 @@ import static com.hedera.services.state.merkle.MerkleEntityId.fromAccountId; import static com.hedera.services.utils.MiscUtils.asFcKeyUnchecked; import static com.hedera.services.utils.MiscUtils.asTimestamp; -import static com.hedera.services.utils.MiscUtils.canonicalDiffRepr; -import static com.hedera.services.utils.MiscUtils.readableTransferList; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.UNKNOWN; /** @@ -118,7 +113,8 @@ public void resetFor(TxnAccessor accessor, Instant consensusTime, long submittin isPayerSigKnownActive = false; hasComputedRecordSoFar = false; - ctx.charging().resetFor(accessor, submittingNodeAccount()); + ctx.narratedCharging().resetForTxn(accessor, submittingMember); + recordSoFar.clear(); } @@ -154,17 +150,14 @@ public long submittingSwirldsMember() { @Override public TransactionRecord recordSoFar() { - long amount = ctx.charging().totalFeesChargedToPayer() + otherNonThresholdFees; + final long feesCharged = ctx.narratedCharging().totalFeesChargedToPayer() + otherNonThresholdFees; - if (log.isDebugEnabled()) { - logItemized(); - } recordSoFar .setMemo(accessor.getTxn().getMemo()) .setReceipt(receiptSoFar()) .setTransferList(ctx.ledger().netTransfersInTxn()) .setTransactionID(accessor.getTxnId()) - .setTransactionFee(amount) + .setTransactionFee(feesCharged) .setTransactionHash(hash) .setConsensusTimestamp(consensusTimestamp) .addAllTokenTransferLists(ctx.ledger().netTokenTransfersInTxn()); @@ -178,39 +171,6 @@ public TransactionRecord recordSoFar() { return recordSoFar.build(); } - @Override - public TransactionRecord updatedRecordGiven(TransferList listWithNewFees) { - if (!hasComputedRecordSoFar) { - throw new IllegalStateException(String.format( - "No record exists to be updated with '%s'!", - readableTransferList(listWithNewFees))); - } - - long amount = ctx.charging().totalFeesChargedToPayer() + otherNonThresholdFees; - recordSoFar.setTransferList(listWithNewFees).setTransactionFee(amount); - - return recordSoFar.build(); - } - - private void logItemized() { - String readableTransferList = readableTransferList(itemizedRepresentation()); - log.debug( - "Transfer list with itemized fees for {} is {}", - accessor().getSignedTxn4Log(), - readableTransferList); - } - - TransferList itemizedRepresentation() { - TransferList canonicalRepr = ctx.ledger().netTransfersInTxn(); - TransferList itemizedFees = ctx.charging().itemizedFees(); - - List nonFeeAdjustments = - canonicalDiffRepr(canonicalRepr.getAccountAmountsList(), itemizedFees.getAccountAmountsList()); - return itemizedFees.toBuilder() - .addAllAccountAmounts(nonFeeAdjustments) - .build(); - } - private TransactionReceipt.Builder receiptSoFar() { TransactionReceipt.Builder receipt = TransactionReceipt.newBuilder() .setExchangeRate(ctx.exchange().activeRates()) diff --git a/hedera-node/src/main/java/com/hedera/services/context/NodeInfo.java b/hedera-node/src/main/java/com/hedera/services/context/NodeInfo.java index 1c390ef80402..56db8faa3e50 100644 --- a/hedera-node/src/main/java/com/hedera/services/context/NodeInfo.java +++ b/hedera-node/src/main/java/com/hedera/services/context/NodeInfo.java @@ -20,6 +20,7 @@ * ‍ */ +import com.hedera.services.state.merkle.MerkleEntityId; import com.swirlds.common.AddressBook; import java.util.function.Supplier; @@ -43,6 +44,7 @@ public class NodeInfo { private int numberOfNodes; private boolean[] isZeroStake; private AccountID[] accounts; + private MerkleEntityId[] accountKeys; private final long selfId; private final Supplier book; @@ -86,6 +88,18 @@ public boolean isSelfZeroStake() { * @throws IllegalArgumentException if the book did not contain the id, or was missing an account for the id */ public AccountID accountOf(long nodeId) { + final int index = validatedIndexFor(nodeId); + + return accounts[index]; + } + + public MerkleEntityId accountKeyOf(long nodeId) { + final int index = validatedIndexFor(nodeId); + + return accountKeys[index]; + } + + private int validatedIndexFor(long nodeId) { if (!bookIsRead) { readBook(); } @@ -94,11 +108,10 @@ public AccountID accountOf(long nodeId) { if (isIndexOutOfBounds(index)) { throw new IllegalArgumentException("No node with id " + nodeId + " was in the address book!"); } - final var account = accounts[index]; - if (account == null) { + if (accounts[index] == null) { throw new IllegalArgumentException("The address book did not have an account for node id " + nodeId + "!"); } - return account; + return index; } /** @@ -131,6 +144,7 @@ void readBook() { numberOfNodes = staticBook.getSize(); accounts = new AccountID[numberOfNodes]; + accountKeys = new MerkleEntityId[numberOfNodes]; isZeroStake = new boolean[numberOfNodes]; for (int i = 0; i < numberOfNodes; i++) { @@ -138,6 +152,7 @@ void readBook() { isZeroStake[i] = address.getStake() <= 0; try { accounts[i] = parseAccount(address.getMemo()); + accountKeys[i] = MerkleEntityId.fromAccountId(accounts[i]); } catch (IllegalArgumentException e) { if (!isZeroStake[i]) { log.error("Cannot parse account for staked node id {}, potentially fatal!", i, e); diff --git a/hedera-node/src/main/java/com/hedera/services/context/ServicesContext.java b/hedera-node/src/main/java/com/hedera/services/context/ServicesContext.java index 31700ca9c8a7..ac3c2a1b512e 100644 --- a/hedera-node/src/main/java/com/hedera/services/context/ServicesContext.java +++ b/hedera-node/src/main/java/com/hedera/services/context/ServicesContext.java @@ -101,8 +101,10 @@ import com.hedera.services.fees.calculation.token.txns.TokenUnfreezeResourceUsage; import com.hedera.services.fees.calculation.token.txns.TokenUpdateResourceUsage; import com.hedera.services.fees.calculation.token.txns.TokenWipeResourceUsage; -import com.hedera.services.fees.charging.ItemizableFeeCharging; -import com.hedera.services.fees.charging.TxnFeeChargingPolicy; +import com.hedera.services.fees.charging.NarratedCharging; +import com.hedera.services.fees.charging.NarratedLedgerCharging; +import com.hedera.services.fees.charging.FeeChargingPolicy; +import com.hedera.services.fees.charging.TxnChargingPolicyAgent; import com.hedera.services.files.DataMapFactory; import com.hedera.services.files.EntityExpiryMapFactory; import com.hedera.services.files.FileUpdateInterceptor; @@ -305,7 +307,6 @@ import com.hedera.services.usage.crypto.CryptoOpsUsage; import com.hedera.services.usage.file.FileOpsUsage; import com.hedera.services.usage.schedule.ScheduleOpsUsage; -import com.hedera.services.utils.EntityIdUtils; import com.hedera.services.utils.JvmSystemExits; import com.hedera.services.utils.MiscUtils; import com.hedera.services.utils.Pause; @@ -491,6 +492,7 @@ public class ServicesContext { private FreezeController freezeGrpc; private BalancesExporter balancesExporter; private SysFileCallbacks sysFileCallbacks; + private NarratedCharging narratedCharging; private NetworkCtxManager networkCtxManager; private SolidityLifecycle solidityLifecycle; private ExpiringCreations creator; @@ -521,7 +523,7 @@ public class ServicesContext { private TransactionPrecheck transactionPrecheck; private FeeMultiplierSource feeMultiplierSource; private NodeLocalProperties nodeLocalProperties; - private TxnFeeChargingPolicy txnChargingPolicy; + private FeeChargingPolicy txnChargingPolicy; private TxnAwareRatesManager exchangeRatesManager; private ServicesStatsManager statsManager; private LedgerAccountsSource accountSource; @@ -532,7 +534,7 @@ public class ServicesContext { private HfsSystemFilesManager systemFilesManager; private CurrentPlatformStatus platformStatus; private SystemAccountsCreator systemAccountsCreator; - private ItemizableFeeCharging itemizableFeeCharging; + private TxnChargingPolicyAgent chargingPolicyAgent; private ServicesRepositoryRoot repository; private CharacteristicsFactory characteristics; private AccountRecordsHistorian recordsHistorian; @@ -850,16 +852,6 @@ public TransactionThrottling txnThrottling() { return txnThrottling; } - public ItemizableFeeCharging charging() { - if (itemizableFeeCharging == null) { - itemizableFeeCharging = new ItemizableFeeCharging( - ledger(), - exemptions(), - globalDynamicProperties()); - } - return itemizableFeeCharging; - } - public SubmissionFlow submissionFlow() { if (submissionFlow == null) { submissionFlow = new BasicSubmissionFlow(nodeType(), transactionPrecheck(), submissionManager()); @@ -1522,6 +1514,14 @@ public EntityAutoRenewal entityAutoRenewal() { return entityAutoRenewal; } + public NarratedCharging narratedCharging() { + if (narratedCharging == null) { + narratedCharging = new NarratedLedgerCharging( + nodeInfo(), ledger(), exemptions(), globalDynamicProperties(), this::accounts); + } + return narratedCharging; + } + public ExpiryManager expiries() { if (expiries == null) { var histories = txnHistories(); @@ -1926,13 +1926,21 @@ public UsagePricesProvider usagePrices() { return usagePrices; } - public TxnFeeChargingPolicy txnChargingPolicy() { + public FeeChargingPolicy txnChargingPolicy() { if (txnChargingPolicy == null) { - txnChargingPolicy = new TxnFeeChargingPolicy(); + txnChargingPolicy = new FeeChargingPolicy(narratedCharging()); } return txnChargingPolicy; } + public TxnChargingPolicyAgent chargingPolicyAgent() { + if (chargingPolicyAgent == null) { + chargingPolicyAgent = new TxnChargingPolicyAgent( + fees(), txnChargingPolicy(), txnCtx(), this::currentView, nodeDiligenceScreen(), txnHistories()); + } + return chargingPolicyAgent; + } + public SystemAccountsCreator systemAccountsCreator() { if (systemAccountsCreator == null) { systemAccountsCreator = new BackedSystemAccountsCreator( diff --git a/hedera-node/src/main/java/com/hedera/services/context/TransactionContext.java b/hedera-node/src/main/java/com/hedera/services/context/TransactionContext.java index 8c15ae7febf7..f15bb3842f56 100644 --- a/hedera-node/src/main/java/com/hedera/services/context/TransactionContext.java +++ b/hedera-node/src/main/java/com/hedera/services/context/TransactionContext.java @@ -33,7 +33,6 @@ import com.hederahashgraph.api.proto.java.TopicID; import com.hederahashgraph.api.proto.java.TransactionID; import com.hederahashgraph.api.proto.java.TransactionRecord; -import com.hederahashgraph.api.proto.java.TransferList; import java.time.Instant; import java.util.Collection; @@ -123,16 +122,6 @@ default AccountID effectivePayer() { */ TransactionRecord recordSoFar(); - /** - * Returns the last record created by {@link TransactionContext#recordSoFar()}, - * with the transfer list and fees updated. - * - * @param listWithNewFees the new transfer list to use in the record. - * @return the updated historical record of processing the current txn thus far. - * @throws IllegalStateException if {@code recordSoFar} has not been called for the active txn. - */ - TransactionRecord updatedRecordGiven(TransferList listWithNewFees); - /** * Gets an accessor to the defined type {@link TxnAccessor} * currently being processed. diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/FeeChargingPolicy.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/FeeChargingPolicy.java new file mode 100644 index 000000000000..8b55184cdb3f --- /dev/null +++ b/hedera-node/src/main/java/com/hedera/services/fees/charging/FeeChargingPolicy.java @@ -0,0 +1,134 @@ +package com.hedera.services.fees.charging; + +/*- + * ‌ + * Hedera Services Node + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.fee.FeeObject; + +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK; + +/** + * Provides the transaction fee-charging policy for the processing + * logic. The policy offers four basic entry points: + *
    + *
  1. For a txn whose submitting node seemed to ignore due diligence + * (e.g. submitted a txn with an impermissible valid duration); and,
  2. + *
  3. For a txn that looks to have been submitted responsibly, but is + * a duplicate of a txn already submitted by a different node; and,
  4. + *
  5. For a triggered txn; and,
  6. + *
  7. For a txn that was submitted responsibly, and is believed unique.
  8. + *
+ * + * @author Michael Tinker + */ +public class FeeChargingPolicy { + private final NarratedCharging narratedCharging; + + public FeeChargingPolicy(NarratedCharging narratedCharging) { + this.narratedCharging = narratedCharging; + } + + /** + * Apply the fee charging policy to a txn that was submitted responsibly, and + * believed unique. + * + * @param fees the fee to charge + * @return the outcome of applying the policy + */ + public ResponseCodeEnum apply(FeeObject fees) { + return chargePendingSolvency(fees); + } + + /** + * Apply the fee charging policy to a txn that was submitted responsibly, but + * is a duplicate of a txn already submitted by a different node. + * + * @param fees the fee to charge + * @return the outcome of applying the policy + */ + public ResponseCodeEnum applyForDuplicate(FeeObject fees) { + final var feesForDuplicate = new FeeObject(fees.getNodeFee(), fees.getNetworkFee(), 0L); + + return chargePendingSolvency(feesForDuplicate); + } + + /** + * Apply the fee charging policy to a txn that was submitted responsibly, but + * is a triggered txn rather than a parent txn requiring node precheck work. + * + * @param fees the fee to charge + * @return the outcome of applying the policy + */ + public ResponseCodeEnum applyForTriggered(FeeObject fees) { + narratedCharging.setFees(fees); + + if (!narratedCharging.isPayerWillingToCoverServiceFee()) { + return INSUFFICIENT_TX_FEE; + } else if (!narratedCharging.canPayerAffordServiceFee()) { + return INSUFFICIENT_PAYER_BALANCE; + } else { + narratedCharging.chargePayerServiceFee(); + return OK; + } + } + + /** + * Apply the fee charging policy to a txn that looks to have been + * submitted without performing basic due diligence. + * + * @param fees the fee to charge + * @return the outcome of applying the policy + */ + public ResponseCodeEnum applyForIgnoredDueDiligence(FeeObject fees) { + narratedCharging.setFees(fees); + narratedCharging.chargeSubmittingNodeUpToNetworkFee(); + return OK; + } + + private ResponseCodeEnum chargePendingSolvency(FeeObject fees) { + narratedCharging.setFees(fees); + + if (!narratedCharging.isPayerWillingToCoverNetworkFee()) { + narratedCharging.chargeSubmittingNodeUpToNetworkFee(); + return INSUFFICIENT_TX_FEE; + } else if (!narratedCharging.canPayerAffordNetworkFee()) { + narratedCharging.chargeSubmittingNodeUpToNetworkFee(); + return INSUFFICIENT_PAYER_BALANCE; + } else { + return chargeGivenNodeDueDiligence(); + } + } + + private ResponseCodeEnum chargeGivenNodeDueDiligence() { + if (!narratedCharging.isPayerWillingToCoverAllFees()) { + narratedCharging.chargePayerNetworkAndUpToNodeFee(); + return INSUFFICIENT_TX_FEE; + } else if (!narratedCharging.canPayerAffordAllFees()) { + narratedCharging.chargePayerNetworkAndUpToNodeFee(); + return INSUFFICIENT_PAYER_BALANCE; + } else { + narratedCharging.chargePayerAllFees(); + return OK; + } + } +} diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/FieldSourcedFeeScreening.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/FieldSourcedFeeScreening.java deleted file mode 100644 index 621c6a1d6520..000000000000 --- a/hedera-node/src/main/java/com/hedera/services/fees/charging/FieldSourcedFeeScreening.java +++ /dev/null @@ -1,93 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hedera.services.fees.FeeExemptions; -import com.hedera.services.fees.TxnFeeType; -import com.hedera.services.utils.TxnAccessor; -import com.hederahashgraph.api.proto.java.AccountID; - -import java.util.EnumMap; -import java.util.EnumSet; - -/** - * Implements a {@link TxnScopedFeeScreening} using an injected - * {@link FeeExemptions} and fee information configured by collaborators - * via the {@link FieldSourcedFeeScreening#setFor(TxnFeeType, long)} - * method. - * - * @author Michael Tinker - */ -public class FieldSourcedFeeScreening implements TxnScopedFeeScreening { - private boolean payerExemption; - private BalanceCheck check; - private final FeeExemptions exemptions; - protected TxnAccessor accessor; - EnumMap feeAmounts = new EnumMap<>(TxnFeeType.class); - - public FieldSourcedFeeScreening(FeeExemptions exemptions) { - this.exemptions = exemptions; - } - - public void setBalanceCheck(BalanceCheck check) { - this.check = check; - } - - public void resetFor(TxnAccessor accessor) { - this.accessor = accessor; - payerExemption = exemptions.hasExemptPayer(accessor); - } - - public void setFor(TxnFeeType fee, long amount) { - feeAmounts.put(fee, amount); - } - - @Override - public boolean canPayerAfford(EnumSet fees) { - return isPayerExempt() || check.canAfford(accessor.getPayer(), totalAmountOf(fees)); - } - - @Override - public boolean isPayerWillingToCover(EnumSet fees) { - return isPayerExempt() || accessor.getTxn().getTransactionFee() >= totalAmountOf(fees); - } - - @Override - public boolean isPayerWillingnessCredible() { - return isPayerExempt() || check.canAfford(accessor.getPayer(), accessor.getTxn().getTransactionFee()); - } - - protected boolean isPayerExempt() { - return payerExemption; - } - - @Override - public boolean canParticipantAfford(AccountID participant, EnumSet fees) { - return check.canAfford(participant, totalAmountOf(fees)); - } - - protected long totalAmountOf(EnumSet fees) { - return fees.stream() - .filter(fee -> feeAmounts.containsKey(fee)) - .mapToLong(feeAmounts::get) - .sum(); - } -} diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java deleted file mode 100644 index 23ac2f04928d..000000000000 --- a/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java +++ /dev/null @@ -1,246 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hedera.services.context.properties.GlobalDynamicProperties; -import com.hedera.services.fees.FeeExemptions; -import com.hedera.services.fees.TxnFeeType; -import com.hedera.services.ledger.HederaLedger; -import com.hedera.services.utils.TxnAccessor; -import com.hederahashgraph.api.proto.java.AccountAmount; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.TransferList; - -import java.util.EnumMap; -import java.util.EnumSet; -import java.util.List; -import java.util.Optional; -import java.util.function.Consumer; - -import static com.hedera.services.fees.TxnFeeType.NETWORK; -import static com.hedera.services.fees.TxnFeeType.NODE; -import static com.hedera.services.fees.TxnFeeType.SERVICE; - -/** - * A {@link FieldSourcedFeeScreening} which also implements - * {@link TxnScopedFeeCharging} via an injected {@link HederaLedger}. - * Keeps a history of per-transaction fees charging activity to - * make it easy to construct the final {@link com.hederahashgraph.api.proto.java.TransactionRecord}. - * - * @author Michael Tinker - */ -public class ItemizableFeeCharging extends FieldSourcedFeeScreening implements TxnScopedFeeCharging { - public static EnumSet NODE_FEE = EnumSet.of(NODE); - public static EnumSet NETWORK_FEE = EnumSet.of(NETWORK); - public static EnumSet SERVICE_FEE = EnumSet.of(SERVICE); - public static EnumSet NETWORK_NODE_SERVICE_FEES = EnumSet.of(NETWORK, NODE, SERVICE); - - private HederaLedger ledger; - - private final GlobalDynamicProperties properties; - - AccountID funding; - AccountID submittingNode; - EnumMap payerFeesCharged = new EnumMap<>(TxnFeeType.class); - EnumMap submittingNodeFeesCharged = new EnumMap<>(TxnFeeType.class); - - public ItemizableFeeCharging( - HederaLedger ledger, - FeeExemptions exemptions, - GlobalDynamicProperties properties - ) { - super(exemptions); - this.ledger = ledger; - this.properties = properties; - setBalanceCheck((payer, amount) -> ledger.getBalance(payer) >= amount); - } - - public void setLedger(HederaLedger ledger) { - this.ledger = ledger; - } - - public long totalFeesChargedToPayer() { - return payerFeesCharged.values().stream().mapToLong(Long::longValue).sum(); - } - - long chargedToPayer(TxnFeeType fee) { - return Optional.ofNullable(payerFeesCharged.get(fee)).orElse(0L); - } - - long chargedToSubmittingNode(TxnFeeType fee) { - return Optional.ofNullable(submittingNodeFeesCharged.get(fee)).orElse(0L); - } - - public void resetFor(TxnAccessor accessor, AccountID submittingNode) { - super.resetFor(accessor); - - funding = properties.fundingAccount(); - this.submittingNode = submittingNode; - - payerFeesCharged.clear(); - submittingNodeFeesCharged.clear(); - } - - /** - * Fees for a txn submitted by an a irresponsible node are itemized as: - *
    - *
  1. Received by funding, sent by the submitting node, for network operating costs.
  2. - *
- * - * Fees for a correctly signed txn submitted by a responsible node are itemized as: - *
    - *
  1. Received by funding, sent by the txn payer, for network operating costs.
  2. - *
  3. Received by the submitting node, sent by the txn payer, for handling costs.
  4. - *
  5. Received by funding, sent by the txn payer, for service costs.
  6. - *
- * - * @return the itemized charges in canonical order - */ - public TransferList itemizedFees() { - TransferList.Builder fees = TransferList.newBuilder(); - - AccountID payer = accessor.getPayer(); - if (!payer.equals(submittingNode) && !submittingNodeFeesCharged.isEmpty()) { - includeIfCharged(NETWORK, submittingNode, submittingNodeFeesCharged, fees); - } else { - includeIfCharged(NETWORK, payer, payerFeesCharged, fees); - includeIfCharged(NODE, payer, payerFeesCharged, fees); - includeIfCharged(SERVICE, payer, payerFeesCharged, fees); - } - - return fees.build(); - } - - private void includeIfCharged( - TxnFeeType fee, - AccountID source, - EnumMap feesCharged, - TransferList.Builder fees - ) { - if (feesCharged.containsKey(fee)) { - AccountID receiver = (fee == NODE) ? submittingNode : funding; - fees.addAllAccountAmounts(receiverFirst(source, receiver, feesCharged.get(fee))); - } - } - - private List receiverFirst(AccountID payer, AccountID receiver, long amount) { - return itemized(payer, receiver, amount, true); - } - private List itemized(AccountID payer, AccountID receiver, long amount, boolean isReceiverFirst) { - return List.of( - AccountAmount.newBuilder() - .setAccountID(isReceiverFirst ? receiver : payer) - .setAmount(isReceiverFirst ? amount : -1 * amount) - .build(), - AccountAmount.newBuilder() - .setAccountID(isReceiverFirst ? payer : receiver) - .setAmount(isReceiverFirst ? -1 * amount : amount) - .build()); - } - - @Override - public void chargeSubmittingNodeUpTo(EnumSet fees) { - pay( - fees, - () -> {}, - (fee) -> chargeUpTo(submittingNode, funding, fee)); - } - - @Override - public void chargePayer(EnumSet fees) { - chargeParticipant(accessor.getPayer(), fees); - } - - @Override - public void chargePayerUpTo(EnumSet fees) { - pay( - fees, - () -> chargeUpTo(accessor.getPayer(), submittingNode, NODE), - (fee) -> chargeUpTo(accessor.getPayer(), funding, fee)); - } - - @Override - public void chargeParticipant(AccountID participant, EnumSet fees) { - pay( - fees, - () -> charge(participant, submittingNode, NODE), - fee -> charge(participant, funding, fee)); - } - - private void pay( - EnumSet fees, - Runnable nodePayment, - Consumer fundingPayment - ) { - /* Treasury gets priority over node. */ - for (TxnFeeType fee : fees) { - if (fee != NODE) { - fundingPayment.accept(fee); - } - } - - if (fees.contains(NODE)) { - nodePayment.run(); - } - } - - private void charge(AccountID payer, AccountID payee, TxnFeeType fee) { - if (noCharge(payer, payee, fee)) { - return; - } - long amount = feeAmounts.get(fee); - completeNonVanishing(payer, payee, amount, fee); - } - - private void chargeUpTo(AccountID payer, AccountID payee, TxnFeeType fee) { - if (noCharge(payer, payee, fee)) { - return; - } - long actionableAmount = Math.min(ledger.getBalance(payer), feeAmounts.get(fee)); - completeNonVanishing(payer, payee, actionableAmount, fee); - } - - private void completeNonVanishing(AccountID payer, AccountID payee, long amount, TxnFeeType fee) { - if (amount > 0) { - ledger.doTransfer(payer, payee, amount); - updateRecords(payer, fee, amount); - } - } - - private boolean noCharge(AccountID payer, AccountID payee, TxnFeeType fee) { - if (payer.equals(payee)) { - return true; - } else if (payer.equals(accessor.getPayer()) && isPayerExempt()) { - return true; - } else { - return false; - } - } - - private void updateRecords(AccountID source, TxnFeeType fee, long amount) { - if (source.equals(accessor.getPayer())) { - payerFeesCharged.put(fee, amount); - } - if (source.equals(submittingNode)) { - submittingNodeFeesCharged.put(fee, amount); - } - } -} diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/BalanceCheck.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/NarratedCharging.java similarity index 50% rename from hedera-node/src/main/java/com/hedera/services/fees/charging/BalanceCheck.java rename to hedera-node/src/main/java/com/hedera/services/fees/charging/NarratedCharging.java index 1eeb158417cf..9079a934fbaf 100644 --- a/hedera-node/src/main/java/com/hedera/services/fees/charging/BalanceCheck.java +++ b/hedera-node/src/main/java/com/hedera/services/fees/charging/NarratedCharging.java @@ -20,21 +20,27 @@ * ‍ */ -import com.hederahashgraph.api.proto.java.AccountID; +import com.hedera.services.utils.TxnAccessor; +import com.hederahashgraph.fee.FeeObject; /** - * Defines a type able to determine if a given payer can afford - * to pay a given amount. - * - * @author Michael Tinker + * Defines the checks and charging actions we need to apply the Services fee policy. */ -@FunctionalInterface -public interface BalanceCheck { - /** - * Flags if the given payer can afford the given amount. - * @param payer a payer - * @param amount an amount - * @return if the payer can afford the amount - */ - boolean canAfford(AccountID payer, long amount); +public interface NarratedCharging { + void setFees(FeeObject fees); + void resetForTxn(TxnAccessor accessor, long submittingNodeId); + + boolean canPayerAffordAllFees(); + boolean canPayerAffordNetworkFee(); + boolean canPayerAffordServiceFee(); + boolean isPayerWillingToCoverAllFees(); + boolean isPayerWillingToCoverNetworkFee(); + boolean isPayerWillingToCoverServiceFee(); + + void chargePayerAllFees(); + void chargePayerServiceFee(); + void chargePayerNetworkAndUpToNodeFee(); + void chargeSubmittingNodeUpToNetworkFee(); + + long totalFeesChargedToPayer(); } diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/NarratedLedgerCharging.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/NarratedLedgerCharging.java new file mode 100644 index 000000000000..31a5649dab7b --- /dev/null +++ b/hedera-node/src/main/java/com/hedera/services/fees/charging/NarratedLedgerCharging.java @@ -0,0 +1,204 @@ +package com.hedera.services.fees.charging; + +/*- + * ‌ + * Hedera Services Node + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + +import com.hedera.services.context.NodeInfo; +import com.hedera.services.context.properties.GlobalDynamicProperties; +import com.hedera.services.fees.FeeExemptions; +import com.hedera.services.ledger.HederaLedger; +import com.hedera.services.state.merkle.MerkleAccount; +import com.hedera.services.state.merkle.MerkleEntityId; +import com.hedera.services.utils.TxnAccessor; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.fee.FeeObject; +import com.swirlds.fcmap.FCMap; + +import java.util.Optional; +import java.util.function.Supplier; + +/** + * Implements the {@link NarratedCharging} contract using a injected {@link HederaLedger} + * to charge the requested fees. + */ +public class NarratedLedgerCharging implements NarratedCharging { + private static final long UNKNOWN_ACCOUNT_BALANCE = -1L; + + private final NodeInfo nodeInfo; + private final HederaLedger ledger; + private final FeeExemptions feeExemptions; + private final GlobalDynamicProperties dynamicProperties; + private final Supplier> accounts; + + private long effPayerStartingBalance = UNKNOWN_ACCOUNT_BALANCE; + private long nodeFee; + private long networkFee; + private long serviceFee; + private long totalOfferedFee; + private long totalCharged; + private boolean payerExempt; + private AccountID grpcNodeId; + private AccountID grpcPayerId; + private MerkleEntityId nodeId; + private MerkleEntityId payerId; + + public NarratedLedgerCharging( + NodeInfo nodeInfo, + HederaLedger ledger, + FeeExemptions feeExemptions, + GlobalDynamicProperties dynamicProperties, + Supplier> accounts + ) { + this.ledger = ledger; + this.accounts = accounts; + this.nodeInfo = nodeInfo; + this.feeExemptions = feeExemptions; + this.dynamicProperties = dynamicProperties; + } + + @Override + public long totalFeesChargedToPayer() { + return totalCharged; + } + + @Override + public void resetForTxn(TxnAccessor accessor, long submittingNodeId) { + this.grpcPayerId = accessor.getPayer(); + this.payerId = MerkleEntityId.fromAccountId(grpcPayerId); + this.totalOfferedFee = accessor.getOfferedFee(); + + nodeId = nodeInfo.accountKeyOf(submittingNodeId); + grpcNodeId = nodeInfo.accountOf(submittingNodeId); + payerExempt = feeExemptions.hasExemptPayer(accessor); + totalCharged = 0L; + effPayerStartingBalance = UNKNOWN_ACCOUNT_BALANCE; + } + + @Override + public void setFees(FeeObject fees) { + this.nodeFee = fees.getNodeFee(); + this.networkFee = fees.getNetworkFee(); + this.serviceFee = fees.getServiceFee(); + } + + @Override + public boolean canPayerAffordAllFees() { + if (payerExempt) { + return true; + } + if (effPayerStartingBalance == UNKNOWN_ACCOUNT_BALANCE) { + initEffPayerBalance(payerId); + } + return effPayerStartingBalance >= (nodeFee + networkFee + serviceFee); + } + + @Override + public boolean canPayerAffordNetworkFee() { + if (payerExempt) { + return true; + } + if (effPayerStartingBalance == UNKNOWN_ACCOUNT_BALANCE) { + initEffPayerBalance(payerId); + } + return effPayerStartingBalance >= networkFee; + } + + @Override + public boolean canPayerAffordServiceFee() { + if (payerExempt) { + return true; + } + if (effPayerStartingBalance == UNKNOWN_ACCOUNT_BALANCE) { + initEffPayerBalance(payerId); + } + return effPayerStartingBalance >= serviceFee; + } + + @Override + public boolean isPayerWillingToCoverAllFees() { + return payerExempt || totalOfferedFee >= (nodeFee + networkFee + serviceFee); + } + + @Override + public boolean isPayerWillingToCoverNetworkFee() { + return payerExempt || totalOfferedFee >= networkFee; + } + + @Override + public boolean isPayerWillingToCoverServiceFee() { + return payerExempt || totalOfferedFee >= serviceFee; + } + + @Override + public void chargePayerAllFees() { + if (payerExempt) { + return; + } + ledger.adjustBalance(grpcNodeId, +nodeFee); + ledger.adjustBalance(dynamicProperties.fundingAccount(), +(networkFee + serviceFee)); + totalCharged = nodeFee + networkFee + serviceFee; + ledger.adjustBalance(grpcPayerId, -totalCharged); + } + + @Override + public void chargePayerServiceFee() { + if (payerExempt) { + return; + } + ledger.adjustBalance(dynamicProperties.fundingAccount(), +serviceFee); + totalCharged = serviceFee; + ledger.adjustBalance(grpcPayerId, -totalCharged); + } + + @Override + public void chargePayerNetworkAndUpToNodeFee() { + if (payerExempt) { + return; + } + if (effPayerStartingBalance == UNKNOWN_ACCOUNT_BALANCE) { + initEffPayerBalance(payerId); + } + long chargeableNodeFee = Math.min(nodeFee, effPayerStartingBalance - networkFee); + ledger.adjustBalance(grpcNodeId, +chargeableNodeFee); + ledger.adjustBalance(dynamicProperties.fundingAccount(), +networkFee); + totalCharged = networkFee + chargeableNodeFee; + ledger.adjustBalance(grpcPayerId, -totalCharged); + } + + @Override + public void chargeSubmittingNodeUpToNetworkFee() { + if (effPayerStartingBalance == UNKNOWN_ACCOUNT_BALANCE) { + initEffPayerBalance(nodeId); + } + long chargeableNetworkFee = Math.min(networkFee, effPayerStartingBalance); + ledger.adjustBalance(grpcNodeId, -chargeableNetworkFee); + ledger.adjustBalance(dynamicProperties.fundingAccount(), +chargeableNetworkFee); + } + + private void initEffPayerBalance(MerkleEntityId effPayerId) { + final var payerAccount = accounts.get().get(effPayerId); + if (payerAccount == null) { + throw new IllegalStateException("Invariant failure, effective payer account " + + Optional.ofNullable(effPayerId).map(MerkleEntityId::toAbbrevString).orElse("null") + + " is missing!"); + } + effPayerStartingBalance = payerAccount.getBalance(); + } +} diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnChargingPolicyAgent.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnChargingPolicyAgent.java new file mode 100644 index 000000000000..0bd1f0cdf0b1 --- /dev/null +++ b/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnChargingPolicyAgent.java @@ -0,0 +1,99 @@ +package com.hedera.services.fees.charging; + +/*- + * ‌ + * Hedera Services Node + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + +import com.hedera.services.context.TransactionContext; +import com.hedera.services.context.primitives.StateView; +import com.hedera.services.fees.FeeCalculator; +import com.hedera.services.records.TxnIdRecentHistory; +import com.hedera.services.state.logic.AwareNodeDiligenceScreen; +import com.hedera.services.utils.TxnAccessor; +import com.hederahashgraph.api.proto.java.TransactionID; + +import java.util.Map; +import java.util.function.Supplier; + +import static com.hedera.services.txns.diligence.DuplicateClassification.BELIEVED_UNIQUE; +import static com.hedera.services.txns.diligence.DuplicateClassification.DUPLICATE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.DUPLICATE_TRANSACTION; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK; + +/** + * Uses a (non-triggered) transaction's duplicate classification and + * node due diligence screen to pick one of three charging policies + * to use for the fees due for the active transaction. + * + * Please see {@link FeeChargingPolicy} for details. + */ +public class TxnChargingPolicyAgent { + private final FeeCalculator feeCalc; + private final FeeChargingPolicy chargingPolicy; + private final TransactionContext txnCtx; + private final Supplier currentView; + private final AwareNodeDiligenceScreen nodeDiligenceScreen; + private final Map txnHistories; + + public TxnChargingPolicyAgent( + FeeCalculator feeCalc, + FeeChargingPolicy chargingPolicy, + TransactionContext txnCtx, + Supplier currentView, + AwareNodeDiligenceScreen nodeDiligenceScreen, + Map txnHistories + ) { + this.feeCalc = feeCalc; + this.txnCtx = txnCtx; + this.currentView = currentView; + this.txnHistories = txnHistories; + this.chargingPolicy = chargingPolicy; + this.nodeDiligenceScreen = nodeDiligenceScreen; + } + + /** + * Returns {@code true} if {@code handleTransaction} can continue after policy application; {@code false} otherwise. + */ + public boolean applyPolicyFor(TxnAccessor accessor) { + final var fees = feeCalc.computeFee(accessor, txnCtx.activePayerKey(), currentView.get()); + final var recentHistory = txnHistories.get(accessor.getTxnId()); + var duplicity = (recentHistory == null) + ? BELIEVED_UNIQUE + : recentHistory.currentDuplicityFor(txnCtx.submittingSwirldsMember()); + + if (nodeDiligenceScreen.nodeIgnoredDueDiligence(duplicity)) { + chargingPolicy.applyForIgnoredDueDiligence(fees); + return false; + } + + if (duplicity == DUPLICATE) { + chargingPolicy.applyForDuplicate(fees); + txnCtx.setStatus(DUPLICATE_TRANSACTION); + return false; + } + + var chargingOutcome = chargingPolicy.apply(fees); + if (chargingOutcome != OK) { + txnCtx.setStatus(chargingOutcome); + return false; + } + + return true; + } +} diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java deleted file mode 100644 index 428101e4b762..000000000000 --- a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hederahashgraph.api.proto.java.ResponseCodeEnum; -import com.hederahashgraph.fee.FeeObject; - -import java.util.function.Consumer; - -import static com.hedera.services.fees.TxnFeeType.NETWORK; -import static com.hedera.services.fees.TxnFeeType.NODE; -import static com.hedera.services.fees.TxnFeeType.SERVICE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_FEE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_NODE_SERVICE_FEES; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NODE_FEE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.SERVICE_FEE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK; - -/** - * Provides the transaction fee-charging policy for the processing - * logic. The policy offers four basic entry points: - *
    - *
  1. For a txn whose submitting node seemed to ignore due diligence - * (e.g. submitted a txn with an impermissible valid duration); and,
  2. - *
  3. For a txn that looks to have been submitted responsibly, but is - * a duplicate of a txn already submitted by a different node; and,
  4. - *
  5. For a triggered txn; and,
  6. - *
  7. For a txn that was submitted responsibly, and is believed unique.
  8. - *
- * - * @author Michael Tinker - */ -public class TxnFeeChargingPolicy { - private final Consumer NO_DISCOUNT = c -> {}; - private final Consumer DUPLICATE_TXN_DISCOUNT = c -> c.setFor(SERVICE, 0); - - /** - * Apply the fee charging policy to a txn that was submitted responsibly, and - * believed unique. - * - * @param charging the charging facility to use - * @param fee the fee to charge - * @return the outcome of applying the policy - */ - public ResponseCodeEnum apply(ItemizableFeeCharging charging, FeeObject fee) { - return applyWithDiscount(charging, fee, NO_DISCOUNT); - } - - /** - * Apply the fee charging policy to a txn that was submitted responsibly, but - * is a triggered txn rather than a parent txn requiring node precheck work. - * - * @param charging the charging facility to use - * @param fee the fee to charge - * @return the outcome of applying the policy - */ - public ResponseCodeEnum applyForTriggered(ItemizableFeeCharging charging, FeeObject fee) { - charging.setFor(SERVICE, fee.getServiceFee()); - - if (!charging.isPayerWillingToCover(SERVICE_FEE)) { - return INSUFFICIENT_TX_FEE; - } else if (!charging.canPayerAfford(SERVICE_FEE)) { - return INSUFFICIENT_PAYER_BALANCE; - } else { - charging.chargePayer(SERVICE_FEE); - return OK; - } - } - - /** - * Apply the fee charging policy to a txn that was submitted responsibly, but - * is a duplicate of a txn already submitted by a different node. - * - * @param charging the charging facility to use - * @param fee the fee to charge - * @return the outcome of applying the policy - */ - public ResponseCodeEnum applyForDuplicate(ItemizableFeeCharging charging, FeeObject fee) { - return applyWithDiscount(charging, fee, DUPLICATE_TXN_DISCOUNT); - } - - /** - * Apply the fee charging policy to a txn that looks to have been - * submitted without performing basic due diligence. - * - * @param charging the charging facility to use - * @param fee the fee to charge - * @return the outcome of applying the policy - */ - public ResponseCodeEnum applyForIgnoredDueDiligence(ItemizableFeeCharging charging, FeeObject fee) { - charging.setFor(NETWORK, fee.getNetworkFee()); - charging.chargeSubmittingNodeUpTo(NETWORK_FEE); - return OK; - } - - private ResponseCodeEnum applyWithDiscount( - ItemizableFeeCharging charging, - FeeObject fee, - Consumer discount - ) { - setStandardFees(charging, fee); - - if (!charging.isPayerWillingToCover(NETWORK_FEE)) { - charging.chargeSubmittingNodeUpTo(NETWORK_FEE); - return INSUFFICIENT_TX_FEE; - } else if (!charging.canPayerAfford(NETWORK_FEE)) { - charging.chargeSubmittingNodeUpTo(NETWORK_FEE); - return INSUFFICIENT_PAYER_BALANCE; - } else { - return applyGivenNodeDueDiligence(charging, discount); - } - } - - private ResponseCodeEnum applyGivenNodeDueDiligence( - ItemizableFeeCharging charging, - Consumer discount - ) { - discount.accept(charging); - if (!charging.isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES)) { - penalizePayer(charging); - return INSUFFICIENT_TX_FEE; - } else if (!charging.canPayerAfford(NETWORK_NODE_SERVICE_FEES)) { - penalizePayer(charging); - return INSUFFICIENT_PAYER_BALANCE; - } else { - charging.chargePayer(NETWORK_NODE_SERVICE_FEES); - return OK; - } - } - - private void penalizePayer(ItemizableFeeCharging charging) { - charging.chargePayer(NETWORK_FEE); - charging.chargePayerUpTo(NODE_FEE); - } - - private void setStandardFees(ItemizableFeeCharging charging, FeeObject fee) { - charging.setFor(NODE, fee.getNodeFee()); - charging.setFor(NETWORK, fee.getNetworkFee()); - charging.setFor(SERVICE, fee.getServiceFee()); - } -} diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnScopedFeeCharging.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnScopedFeeCharging.java deleted file mode 100644 index d853ffb9f2e5..000000000000 --- a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnScopedFeeCharging.java +++ /dev/null @@ -1,63 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hedera.services.fees.TxnFeeType; -import com.hederahashgraph.api.proto.java.AccountID; - -import java.util.EnumSet; - -/** - * Defines a type able to not only screen fees, but charge them - * to the payer, node, and/or participants of a well-known transaction. - * - * @author Michael Tinker - */ -public interface TxnScopedFeeCharging extends TxnScopedFeeScreening { - /** - * Charges the submitting node of the in-scope txn up to suggested fees. - * - * @param fees the suggested fees - */ - void chargeSubmittingNodeUpTo(EnumSet fees); - - /** - * Unconditionally charges the payer of the in-scope txn the given fees. - * - * @param fees the required fees - * @throws IllegalStateException or analogous if the payer cannot afford the fees - */ - void chargePayer(EnumSet fees); - /** - * Charges the payer of the in-scope txn up to suggested fees. - * - * @param fees the suggested fees - */ - void chargePayerUpTo(EnumSet fees); - - /** - * Unconditionally charges the given participant of the in-scope txn the given fees. - * - * @param fees the required fees - * @throws IllegalStateException or analogous if the participant cannot afford the fees - */ - void chargeParticipant(AccountID participant, EnumSet fees); -} diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnScopedFeeScreening.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnScopedFeeScreening.java deleted file mode 100644 index c0aa066b9a53..000000000000 --- a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnScopedFeeScreening.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hedera.services.fees.TxnFeeType; -import com.hederahashgraph.api.proto.java.AccountID; - -import java.util.EnumSet; - -/** - * Defines a type able to screen whether the payer, node, and/or participants - * of a well-known transaction can afford various fees. (In the case of the - * payer, also whether its advertised willingness to pay is sufficient.) - * - * @author Michael Tinker - */ -public interface TxnScopedFeeScreening { - /** - * Flags if the payer of the in-scope txn can afford the given fees. - * - * @param fees the fees in question - * @return if the payer can afford them - */ - boolean canPayerAfford(EnumSet fees); - - /** - * Flags if the payer of the in-scope txn is willing to pay the given fees. - * - * @param fees the fees in question - * @return if the payer is willing to pay them - */ - boolean isPayerWillingToCover(EnumSet fees); - /** - * Flags if the payer of the in-scope txn is able to pay all fees - * it has advertised willingness to supply. - * - * @return if the payer can afford to fund its advertised willingness - */ - boolean isPayerWillingnessCredible(); - - /** - * Flags if the given participant in the in-scope txn can afford the given fees. - * - * @param fees the fees in question - * @return if the participant can afford them - */ - boolean canParticipantAfford(AccountID participant, EnumSet fees); -} diff --git a/hedera-node/src/main/java/com/hedera/services/ledger/accounts/BackingTokenRels.java b/hedera-node/src/main/java/com/hedera/services/ledger/accounts/BackingTokenRels.java index f86ad290776a..b5620a578eed 100644 --- a/hedera-node/src/main/java/com/hedera/services/ledger/accounts/BackingTokenRels.java +++ b/hedera-node/src/main/java/com/hedera/services/ledger/accounts/BackingTokenRels.java @@ -51,8 +51,6 @@ public class BackingTokenRels implements BackingStore, public static final Comparator> REL_CMP = Comparator., AccountID>comparing(Pair::getLeft, ACCOUNT_ID_COMPARATOR) .thenComparing(Pair::getRight, TOKEN_ID_COMPARATOR); - private static final Comparator, MerkleTokenRelStatus>> REL_ENTRY_CMP = - Comparator.comparing(Map.Entry::getKey, REL_CMP); Set> existingRels = new HashSet<>(); Map, MerkleTokenRelStatus> cache = new HashMap<>(); diff --git a/hedera-node/src/main/java/com/hedera/services/ledger/accounts/FCMapBackingAccounts.java b/hedera-node/src/main/java/com/hedera/services/ledger/accounts/FCMapBackingAccounts.java index 62518048a7e6..f179c4ebf076 100644 --- a/hedera-node/src/main/java/com/hedera/services/ledger/accounts/FCMapBackingAccounts.java +++ b/hedera-node/src/main/java/com/hedera/services/ledger/accounts/FCMapBackingAccounts.java @@ -20,7 +20,6 @@ * ‍ */ -import com.hedera.services.ledger.HederaLedger; import com.hedera.services.state.merkle.MerkleAccount; import com.hedera.services.state.merkle.MerkleEntityId; import com.hederahashgraph.api.proto.java.AccountID; diff --git a/hedera-node/src/main/java/com/hedera/services/legacy/handler/FreezeHandler.java b/hedera-node/src/main/java/com/hedera/services/legacy/handler/FreezeHandler.java index 4b78b07b0087..9aff0d404f53 100644 --- a/hedera-node/src/main/java/com/hedera/services/legacy/handler/FreezeHandler.java +++ b/hedera-node/src/main/java/com/hedera/services/legacy/handler/FreezeHandler.java @@ -131,7 +131,7 @@ private Instant nextNaturalInstant(Instant now, int nominalHour, int nominalMin) /* Can't go back in time, so add a day's worth of minutes to hit the nominal time tomorrow */ diffMins += 24 * 60; } - return now.plusSeconds(diffMins * 60); + return now.plusSeconds(diffMins * 60L); } public int minutesSinceMidnight(Instant now) { diff --git a/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java b/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java index 40be2c5fb69f..8f54044d8ef4 100644 --- a/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java +++ b/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java @@ -44,9 +44,6 @@ import static com.hedera.services.legacy.crypto.SignatureStatusCode.SUCCESS_VERIFY_ASYNC; import static com.hedera.services.sigs.HederaToPlatformSigOps.rationalizeIn; import static com.hedera.services.sigs.Rationalization.IN_HANDLE_SUMMARY_FACTORY; -import static com.hedera.services.txns.diligence.DuplicateClassification.BELIEVED_UNIQUE; -import static com.hedera.services.txns.diligence.DuplicateClassification.DUPLICATE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.DUPLICATE_TRANSACTION; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.FAIL_INVALID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_CONTRACT_ID; @@ -144,8 +141,8 @@ private void doTriggeredProcess(TxnAccessor accessor, Instant consensusTime) { ctx.networkCtxManager().advanceConsensusClockTo(consensusTime); ctx.networkCtxManager().prepareForIncorporating(accessor.getFunction()); - FeeObject fee = ctx.fees().computeFee(accessor, ctx.txnCtx().activePayerKey(), ctx.currentView()); - var chargingOutcome = ctx.txnChargingPolicy().applyForTriggered(ctx.charging(), fee); + FeeObject fees = ctx.fees().computeFee(accessor, ctx.txnCtx().activePayerKey(), ctx.currentView()); + var chargingOutcome = ctx.txnChargingPolicy().applyForTriggered(fees); if (chargingOutcome != OK) { ctx.txnCtx().setStatus(chargingOutcome); return; @@ -163,28 +160,10 @@ private void doProcess(TxnAccessor accessor, Instant consensusTime) { ctx.networkCtxManager().prepareForIncorporating(accessor.getFunction()); } - FeeObject fee = ctx.fees().computeFee(accessor, ctx.txnCtx().activePayerKey(), ctx.currentView()); - - var recentHistory = ctx.txnHistories().get(accessor.getTxnId()); - var duplicity = (recentHistory == null) - ? BELIEVED_UNIQUE - : recentHistory.currentDuplicityFor(ctx.txnCtx().submittingSwirldsMember()); - - if (ctx.nodeDiligenceScreen().nodeIgnoredDueDiligence(duplicity)) { - ctx.txnChargingPolicy().applyForIgnoredDueDiligence(ctx.charging(), fee); - return; - } - if (duplicity == DUPLICATE) { - ctx.txnChargingPolicy().applyForDuplicate(ctx.charging(), fee); - ctx.txnCtx().setStatus(DUPLICATE_TRANSACTION); + if (!ctx.chargingPolicyAgent().applyPolicyFor(accessor)) { return; } - var chargingOutcome = ctx.txnChargingPolicy().apply(ctx.charging(), fee); - if (chargingOutcome != OK) { - ctx.txnCtx().setStatus(chargingOutcome); - return; - } if (SIG_RATIONALIZATION_ERRORS.contains(sigStatus.getResponseCode())) { ctx.txnCtx().setStatus(sigStatus.getResponseCode()); return; diff --git a/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java b/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java index 115e5afe88f9..a6d61badea8d 100644 --- a/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java +++ b/hedera-node/src/main/java/com/hedera/services/state/exports/SignedStateBalancesExporter.java @@ -135,7 +135,6 @@ public void exportBalancesFrom(ServicesState signedState, Instant when) { var watch = StopWatch.createStarted(); summary = summarized(signedState); var expected = BigInteger.valueOf(expectedFloat); - System.out.println("Summary: " + summary.getTotalFloat() + " vs expected " + expected); if (!expected.equals(summary.getTotalFloat())) { throw new IllegalStateException(String.format( "Signed state @ %s had total balance %d not %d!", @@ -274,7 +273,6 @@ BalancesSummary summarized(ServicesState signedState) { for (var entry : accounts.entrySet()) { var id = entry.getKey(); var account = entry.getValue(); - System.out.println(account); if (!account.isDeleted()) { var accountId = id.toAccountId(); var balance = account.getBalance(); diff --git a/hedera-node/src/main/java/com/hedera/services/utils/PlatformTxnAccessor.java b/hedera-node/src/main/java/com/hedera/services/utils/PlatformTxnAccessor.java index ced8cf8c1daa..ae3e034fd1ce 100644 --- a/hedera-node/src/main/java/com/hedera/services/utils/PlatformTxnAccessor.java +++ b/hedera-node/src/main/java/com/hedera/services/utils/PlatformTxnAccessor.java @@ -55,6 +55,7 @@ public static PlatformTxnAccessor uncheckedAccessorFor(SwirldTransaction platfor } } + @Override public SwirldTransaction getPlatformTxn() { return platformTxn; } diff --git a/hedera-node/src/main/java/com/hedera/services/utils/SignedTxnAccessor.java b/hedera-node/src/main/java/com/hedera/services/utils/SignedTxnAccessor.java index 678c77085d92..8ee0bace0ae7 100644 --- a/hedera-node/src/main/java/com/hedera/services/utils/SignedTxnAccessor.java +++ b/hedera-node/src/main/java/com/hedera/services/utils/SignedTxnAccessor.java @@ -101,6 +101,10 @@ public HederaFunctionality getFunction() { return function; } + public long getOfferedFee() { + return txn.getTransactionFee(); + } + public Transaction getSignedTxn4Log() { return backwardCompatibleSignedTxn; } diff --git a/hedera-node/src/main/java/com/hedera/services/utils/TxnAccessor.java b/hedera-node/src/main/java/com/hedera/services/utils/TxnAccessor.java index 82b82f055457..f0dd11b8e3c8 100644 --- a/hedera-node/src/main/java/com/hedera/services/utils/TxnAccessor.java +++ b/hedera-node/src/main/java/com/hedera/services/utils/TxnAccessor.java @@ -61,5 +61,7 @@ public interface TxnAccessor { ScheduleID getScheduleRef(); + long getOfferedFee(); + default SwirldTransaction getPlatformTxn() { throw new UnsupportedOperationException(); } } diff --git a/hedera-node/src/test/java/com/hedera/services/context/AwareTransactionContextTest.java b/hedera-node/src/test/java/com/hedera/services/context/AwareTransactionContextTest.java index 47d836f3da04..7e863299588f 100644 --- a/hedera-node/src/test/java/com/hedera/services/context/AwareTransactionContextTest.java +++ b/hedera-node/src/test/java/com/hedera/services/context/AwareTransactionContextTest.java @@ -22,7 +22,7 @@ import com.google.protobuf.ByteString; import com.hedera.services.fees.HbarCentExchange; -import com.hedera.services.fees.charging.ItemizableFeeCharging; +import com.hedera.services.fees.charging.NarratedCharging; import com.hedera.services.ledger.HederaLedger; import com.hedera.services.legacy.core.jproto.JKey; import com.hedera.services.state.expiry.ExpiringEntity; @@ -34,7 +34,6 @@ import com.hedera.test.extensions.LogCaptureExtension; import com.hedera.test.extensions.LoggingSubject; import com.hedera.test.utils.IdUtils; -import com.hederahashgraph.api.proto.java.AccountAmount; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ContractFunctionResult; import com.hederahashgraph.api.proto.java.ContractID; @@ -80,7 +79,6 @@ 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.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.BDDMockito.given; @@ -89,6 +87,7 @@ @ExtendWith(LogCaptureExtension.class) class AwareTransactionContextTest { + private final long offeredFee = 123_000_000L; private final TransactionID scheduledTxnId = TransactionID.newBuilder() .setAccountID(IdUtils.asAccount("0.0.2")) .build(); @@ -102,9 +101,7 @@ class AwareTransactionContextTest { private ExchangeRate rateNow = ExchangeRate.newBuilder().setHbarEquiv(1).setCentEquiv(100).build(); private ExchangeRateSet ratesNow = ExchangeRateSet.newBuilder().setCurrentRate(rateNow).setNextRate(rateNow).build(); private AccountID payer = asAccount("0.0.2"); - private AccountID node = asAccount("0.0.3"); private AccountID anotherNodeAccount = asAccount("0.0.4"); - private AccountID funding = asAccount("0.0.98"); private AccountID created = asAccount("1.0.2"); private AccountID another = asAccount("1.0.300"); private TransferList transfers = withAdjustments(payer, -2L, created, 1L, another, 1L); @@ -119,7 +116,6 @@ class AwareTransactionContextTest { private TopicID topicCreated = asTopic("5.4.3"); private long txnValidStart = now.getEpochSecond() - 1_234L; private HederaLedger ledger; - private ItemizableFeeCharging itemizableFeeCharging; private AccountID nodeAccount = asAccount("0.0.3"); private Address address; private Address anotherAddress; @@ -127,6 +123,7 @@ class AwareTransactionContextTest { private HbarCentExchange exchange; private NodeInfo nodeInfo; private ServicesContext ctx; + private NarratedCharging narratedCharging; private PlatformTxnAccessor accessor; private Transaction signedTxn; private TransactionBody txn; @@ -164,8 +161,7 @@ private void setup() { exchange = mock(HbarCentExchange.class); given(exchange.activeRates()).willReturn(ratesNow); - itemizableFeeCharging = mock(ItemizableFeeCharging.class); - given(itemizableFeeCharging.itemizedFees()).willReturn(TransferList.getDefaultInstance()); + narratedCharging = mock(NarratedCharging.class); payerKey = mock(JKey.class); MerkleAccount payerAccount = mock(MerkleAccount.class); @@ -177,7 +173,7 @@ private void setup() { given(ctx.exchange()).willReturn(exchange); given(ctx.ledger()).willReturn(ledger); given(ctx.accounts()).willReturn(accounts); - given(ctx.charging()).willReturn(itemizableFeeCharging); + given(ctx.narratedCharging()).willReturn(narratedCharging); given(ctx.accounts()).willReturn(accounts); given(ctx.addressBook()).willReturn(book); @@ -190,6 +186,7 @@ private void setup() { signedTxn = mock(Transaction.class); given(signedTxn.toByteArray()).willReturn(memo.getBytes()); accessor = mock(PlatformTxnAccessor.class); + given(accessor.getOfferedFee()).willReturn(offeredFee); given(accessor.getTxnId()).willReturn(txnId); given(accessor.getTxn()).willReturn(txn); given(accessor.getBackwardCompatibleSignedTxn()).willReturn(signedTxn); @@ -202,39 +199,6 @@ private void setup() { subject.resetFor(accessor, now, memberId); } - @Test - void throwsOnUpdateIfNoRecordSoFar() { - // expect: - assertThrows( - IllegalStateException.class, - () -> subject.updatedRecordGiven(withAdjustments(payer, -100, funding, 50, another, 50))); - } - - @Test - void updatesAsExpectedIfRecordSoFar() { - // setup: - subject.recordSoFar = mock(TransactionRecord.Builder.class); - subject.hasComputedRecordSoFar = true; - // and: - var expected = mock(TransactionRecord.class); - - // given: - given(itemizableFeeCharging.totalFeesChargedToPayer()).willReturn(123L); - var xfers = withAdjustments(payer, -100, funding, 50, another, 50); - // and: - given(subject.recordSoFar.build()).willReturn(expected); - given(subject.recordSoFar.setTransferList(xfers)).willReturn(subject.recordSoFar); - - // when: - var actual = subject.updatedRecordGiven(xfers); - - // then: - verify(subject.recordSoFar).setTransferList(xfers); - verify(subject.recordSoFar).setTransactionFee(123L); - // and: - assertSame(expected, actual); - } - @Test void throwsIseIfNoPayerActive() { // expect: @@ -324,7 +288,7 @@ record = subject.recordSoFar(); assertEquals(anotherNodeAccount, subject.submittingNodeAccount()); assertEquals(anotherMemberId, subject.submittingSwirldsMember()); // and: - verify(itemizableFeeCharging).resetFor(accessor, anotherNodeAccount); + verify(narratedCharging).resetForTxn(accessor, memberId); } @Test @@ -342,35 +306,11 @@ void effectivePayerIsActiveIfVerified() { assertEquals(payer, subject.effectivePayer()); } - @Test - void getsItemizedRepr() { - // setup: - TransferList canonicalAdjustments = - withAdjustments(payer, -2100, node, 100, funding, 1000, another, 1000); - TransferList itemizedFees = - withAdjustments(funding, 900, payer, -900, node, 100, payer, -100); - // and: - TransferList desiredRepr = itemizedFees.toBuilder() - .addAccountAmounts(AccountAmount.newBuilder().setAccountID(payer).setAmount(-1100)) - .addAccountAmounts(AccountAmount.newBuilder().setAccountID(funding).setAmount(100)) - .addAccountAmounts(AccountAmount.newBuilder().setAccountID(another).setAmount(1000)) - .build(); - - given(ledger.netTransfersInTxn()).willReturn(canonicalAdjustments); - given(itemizableFeeCharging.itemizedFees()).willReturn(itemizedFees); - - // when: - TransferList repr = subject.itemizedRepresentation(); - - // then: - assertEquals(desiredRepr, repr); - } - @Test void usesChargingToSetTransactionFee() { long std = 1_234L; long other = 4_321L; - given(itemizableFeeCharging.totalFeesChargedToPayer()).willReturn(std); + given(narratedCharging.totalFeesChargedToPayer()).willReturn(std); // when: subject.addNonThresholdFeeChargedToPayer(other); diff --git a/hedera-node/src/test/java/com/hedera/services/context/NodeInfoTest.java b/hedera-node/src/test/java/com/hedera/services/context/NodeInfoTest.java index 013902aa7115..b05ea5c3e189 100644 --- a/hedera-node/src/test/java/com/hedera/services/context/NodeInfoTest.java +++ b/hedera-node/src/test/java/com/hedera/services/context/NodeInfoTest.java @@ -20,6 +20,7 @@ * ‍ */ +import com.hedera.services.state.merkle.MerkleEntityId; import com.hedera.test.extensions.LogCaptor; import com.hedera.test.extensions.LogCaptureExtension; import com.hedera.test.extensions.LoggingSubject; @@ -90,6 +91,7 @@ void understandsAccountIsInMemo() { // setup: final var memo = "0.0.3"; final var expectedAccount = IdUtils.asAccount(memo); + final var expectedAccountKey = new MerkleEntityId(0, 0, 3); givenEntryWithMemoAndStake(nodeId, memo, 1L); @@ -97,6 +99,8 @@ void understandsAccountIsInMemo() { assertEquals(expectedAccount, subject.accountOf(nodeId)); assertEquals(expectedAccount, subject.selfAccount()); assertTrue(subject.hasSelfAccount()); + // and: + assertEquals(expectedAccountKey, subject.accountKeyOf(nodeId)); } @Test diff --git a/hedera-node/src/test/java/com/hedera/services/context/ServicesContextTest.java b/hedera-node/src/test/java/com/hedera/services/context/ServicesContextTest.java index 1a5cbafa3aca..91756024e104 100644 --- a/hedera-node/src/test/java/com/hedera/services/context/ServicesContextTest.java +++ b/hedera-node/src/test/java/com/hedera/services/context/ServicesContextTest.java @@ -43,8 +43,9 @@ import com.hedera.services.fees.TxnRateFeeMultiplierSource; import com.hedera.services.fees.calculation.AwareFcfsUsagePrices; import com.hedera.services.fees.calculation.UsageBasedFeeCalculator; -import com.hedera.services.fees.charging.ItemizableFeeCharging; -import com.hedera.services.fees.charging.TxnFeeChargingPolicy; +import com.hedera.services.fees.charging.NarratedLedgerCharging; +import com.hedera.services.fees.charging.FeeChargingPolicy; +import com.hedera.services.fees.charging.TxnChargingPolicyAgent; import com.hedera.services.files.HFileMeta; import com.hedera.services.files.SysFileCallbacks; import com.hedera.services.files.TieredHederaFs; @@ -114,7 +115,6 @@ import com.hedera.services.state.merkle.MerkleTopic; import com.hedera.services.state.migration.StdStateMigrations; import com.hedera.services.state.submerkle.ExchangeRates; -import com.hedera.services.state.submerkle.RichInstant; import com.hedera.services.state.submerkle.SequenceNumber; import com.hedera.services.state.validation.BasedLedgerValidator; import com.hedera.services.stats.HapiOpCounters; @@ -137,7 +137,6 @@ import com.hedera.services.txns.submission.TxnResponseHelper; import com.hedera.services.txns.validation.ContextOptionValidator; import com.hedera.services.utils.SleepingPause; -import com.hederahashgraph.api.proto.java.AccountID; import com.swirlds.common.Address; import com.swirlds.common.AddressBook; import com.swirlds.common.Console; @@ -464,14 +463,13 @@ void hasExpectedStakedInfrastructure() { assertThat(ctx.applicationPropertiesReloading(), instanceOf(ValidatingCallbackInterceptor.class)); assertThat(ctx.recordsHistorian(), instanceOf(TxnAwareRecordsHistorian.class)); assertThat(ctx.queryableAccounts(), instanceOf(AtomicReference.class)); - assertThat(ctx.txnChargingPolicy(), instanceOf(TxnFeeChargingPolicy.class)); + assertThat(ctx.txnChargingPolicy(), instanceOf(FeeChargingPolicy.class)); assertThat(ctx.txnResponseHelper(), instanceOf(TxnResponseHelper.class)); assertThat(ctx.statusCounts(), instanceOf(ConsensusStatusCounts.class)); assertThat(ctx.queryableStorage(), instanceOf(AtomicReference.class)); assertThat(ctx.systemFilesManager(), instanceOf(HfsSystemFilesManager.class)); assertThat(ctx.queryResponseHelper(), instanceOf(QueryResponseHelper.class)); assertThat(ctx.solidityLifecycle(), instanceOf(SolidityLifecycle.class)); - assertThat(ctx.charging(), instanceOf(ItemizableFeeCharging.class)); assertThat(ctx.repository(), instanceOf(ServicesRepositoryRoot.class)); assertThat(ctx.newPureRepo(), instanceOf(Supplier.class)); assertThat(ctx.exchangeRatesManager(), instanceOf(TxnAwareRatesManager.class)); @@ -523,6 +521,8 @@ void hasExpectedStakedInfrastructure() { assertThat(ctx.entityAutoRenewal(), instanceOf(EntityAutoRenewal.class)); assertThat(ctx.nodeInfo(), instanceOf(NodeInfo.class)); assertThat(ctx.invariants(), instanceOf(InvariantChecks.class)); + assertThat(ctx.narratedCharging(), instanceOf(NarratedLedgerCharging.class)); + assertThat(ctx.chargingPolicyAgent(), instanceOf(TxnChargingPolicyAgent.class)); // and: assertEquals(ServicesNodeType.STAKED_NODE, ctx.nodeType()); // and expect legacy: diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/FeeChargingPolicyTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/FeeChargingPolicyTest.java new file mode 100644 index 000000000000..72d6532ac36c --- /dev/null +++ b/hedera-node/src/test/java/com/hedera/services/fees/charging/FeeChargingPolicyTest.java @@ -0,0 +1,214 @@ +package com.hedera.services.fees.charging; + +/*- + * ‌ + * Hedera Services Node + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + +import com.hederahashgraph.api.proto.java.ResponseCodeEnum; +import com.hederahashgraph.fee.FeeObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + + +@ExtendWith(MockitoExtension.class) +class FeeChargingPolicyTest { + private final FeeObject fees = new FeeObject(1L, 2L, 3L); + private final FeeObject feesForDuplicateTxn = new FeeObject(1L, 2L, 0L); + + @Mock + private NarratedCharging narratedCharging; + + private FeeChargingPolicy subject; + + @BeforeEach + void setUp() { + subject = new FeeChargingPolicy(narratedCharging); + } + + @Test + void chargesNodeUpToNetworkFeeForLackOfDueDiligence() { + // when: + subject.applyForIgnoredDueDiligence(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging).chargeSubmittingNodeUpToNetworkFee(); + } + + @Test + void chargesNonServicePenaltyForUnableToCoverTotal() { + given(narratedCharging.isPayerWillingToCoverNetworkFee()).willReturn(true); + given(narratedCharging.canPayerAffordNetworkFee()).willReturn(true); + given(narratedCharging.isPayerWillingToCoverAllFees()).willReturn(true); + given(narratedCharging.canPayerAffordAllFees()).willReturn(false); + + // when: + ResponseCodeEnum outcome = subject.apply(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging).chargePayerNetworkAndUpToNodeFee(); + // and: + assertEquals(INSUFFICIENT_PAYER_BALANCE, outcome); + } + + @Test + void chargesNonServicePenaltyForUnwillingToCoverTotal() { + given(narratedCharging.isPayerWillingToCoverNetworkFee()).willReturn(true); + given(narratedCharging.canPayerAffordNetworkFee()).willReturn(true); + given(narratedCharging.isPayerWillingToCoverAllFees()).willReturn(false); + + // when: + ResponseCodeEnum outcome = subject.apply(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging).chargePayerNetworkAndUpToNodeFee(); + // and: + assertEquals(INSUFFICIENT_TX_FEE, outcome); + } + + @Test + void chargesDiscountedFeesAsExpectedForDuplicate() { + // setup: + ArgumentCaptor captor = ArgumentCaptor.forClass(FeeObject.class); + + givenPayerWillingAndAbleForAllFees(); + + // when: + ResponseCodeEnum outcome = subject.applyForDuplicate(fees); + + // then: + verify(narratedCharging).setFees(captor.capture()); + // and: + assertEquals(feesForDuplicateTxn.getNodeFee(), captor.getValue().getNodeFee()); + assertEquals(feesForDuplicateTxn.getNetworkFee(), captor.getValue().getNetworkFee()); + assertEquals(feesForDuplicateTxn.getServiceFee(), captor.getValue().getServiceFee()); + // and: + verify(narratedCharging).chargePayerAllFees(); + // and: + assertEquals(OK, outcome); + } + + @Test + void chargesFullFeesAsExpected() { + givenPayerWillingAndAbleForAllFees(); + + // when: + ResponseCodeEnum outcome = subject.apply(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging).chargePayerAllFees(); + // and: + assertEquals(OK, outcome); + } + + @Test + void requiresWillingToPayServiceWhenTriggeredTxn() { + given(narratedCharging.isPayerWillingToCoverServiceFee()).willReturn(false); + + // when: + ResponseCodeEnum outcome = subject.applyForTriggered(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging, never()).chargePayerServiceFee(); + // and: + assertEquals(INSUFFICIENT_TX_FEE, outcome); + } + + @Test + void requiresAbleToPayServiceWhenTriggeredTxn() { + given(narratedCharging.isPayerWillingToCoverServiceFee()).willReturn(true); + given(narratedCharging.canPayerAffordServiceFee()).willReturn(false); + + // when: + ResponseCodeEnum outcome = subject.applyForTriggered(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging, never()).chargePayerServiceFee(); + // and: + assertEquals(INSUFFICIENT_PAYER_BALANCE, outcome); + } + + @Test + void chargesServiceFeeForTriggeredTxn() { + given(narratedCharging.isPayerWillingToCoverServiceFee()).willReturn(true); + given(narratedCharging.canPayerAffordServiceFee()).willReturn(true); + + // when: + ResponseCodeEnum outcome = subject.applyForTriggered(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging).chargePayerServiceFee(); + // and: + assertEquals(OK, outcome); + } + + @Test + void chargesNodePenaltyForPayerUnableToPayNetwork() { + given(narratedCharging.isPayerWillingToCoverNetworkFee()).willReturn(true); + given(narratedCharging.canPayerAffordNetworkFee()).willReturn(false); + + // when: + ResponseCodeEnum outcome = subject.apply(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging).chargeSubmittingNodeUpToNetworkFee(); + // and: + assertEquals(INSUFFICIENT_PAYER_BALANCE, outcome); + } + + @Test + void chargesNodePenaltyForPayerUnwillingToPayNetwork() { + given(narratedCharging.isPayerWillingToCoverNetworkFee()).willReturn(false); + + // when: + ResponseCodeEnum outcome = subject.apply(fees); + + // then: + verify(narratedCharging).setFees(fees); + verify(narratedCharging).chargeSubmittingNodeUpToNetworkFee(); + // and: + assertEquals(INSUFFICIENT_TX_FEE, outcome); + } + + private void givenPayerWillingAndAbleForAllFees() { + given(narratedCharging.isPayerWillingToCoverNetworkFee()).willReturn(true); + given(narratedCharging.canPayerAffordNetworkFee()).willReturn(true); + given(narratedCharging.isPayerWillingToCoverAllFees()).willReturn(true); + given(narratedCharging.canPayerAffordAllFees()).willReturn(true); + } +} diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/FieldSourcedFeeScreeningTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/FieldSourcedFeeScreeningTest.java deleted file mode 100644 index 036e0537b177..000000000000 --- a/hedera-node/src/test/java/com/hedera/services/fees/charging/FieldSourcedFeeScreeningTest.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hedera.services.fees.FeeExemptions; -import com.hedera.services.fees.TxnFeeType; -import com.hedera.services.utils.SignedTxnAccessor; -import com.hedera.test.utils.IdUtils; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.TransactionBody; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.EnumMap; -import java.util.EnumSet; - -import static com.hedera.services.fees.TxnFeeType.NETWORK; -import static com.hedera.services.fees.TxnFeeType.NODE; -import static com.hedera.services.fees.TxnFeeType.SERVICE; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.BDDMockito.anyLong; -import static org.mockito.BDDMockito.argThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.verify; - -class FieldSourcedFeeScreeningTest { - - final long willingness = 1_000L; - final long network = 500L; - final long service = 200L; - final long node = 100L; - final long stateRecord = 150L; - final long cacheRecord = 50L; - - AccountID payer = IdUtils.asAccount("0.0.1001"); - AccountID master = IdUtils.asAccount("0.0.50"); - AccountID participant = IdUtils.asAccount("0.0.2002"); - - BalanceCheck check; - FeeExemptions exemptions; - TransactionBody txn; - TransactionBody systemFileUpdateTxn; - SignedTxnAccessor accessor; - - FieldSourcedFeeScreening subject; - - @BeforeEach - private void setup() { - txn = mock(TransactionBody.class); - check = mock(BalanceCheck.class); - accessor = mock(SignedTxnAccessor.class); - exemptions = mock(FeeExemptions.class); - systemFileUpdateTxn = mock(TransactionBody.class); - - given(txn.getTransactionFee()).willReturn(willingness); - given(accessor.getTxn()).willReturn(txn); - given(accessor.getPayer()).willReturn(payer); - - subject = new FieldSourcedFeeScreening(exemptions); - subject.setBalanceCheck(check); - subject.resetFor(accessor); - } - - @Test - public void exemptsPayerWhenExpected() { - // setup: - EnumSet allPossibleFees = EnumSet.of(NETWORK, NODE, SERVICE); - - givenKnownFeeAmounts(); - given(exemptions.hasExemptPayer(accessor)).willReturn(true); - given(accessor.getTxn()).willReturn(systemFileUpdateTxn); - given(systemFileUpdateTxn.getTransactionFee()).willReturn(Long.MAX_VALUE); - given(check.canAfford(argThat(payer::equals), anyLong())).willReturn(false); - // and: - subject.resetFor(accessor); - - // when: - boolean viability = subject.isPayerWillingnessCredible() && - subject.isPayerWillingToCover(allPossibleFees) && - subject.canPayerAfford(allPossibleFees); - - // then: - assertTrue(viability); - } - - @Test - public void detectsPayerWillingness() { - givenKnownFeeAmounts(); - given(txn.getTransactionFee()).willReturn(network + service); - - // when: - boolean isWillingForEverything = subject.isPayerWillingToCover(EnumSet.of(NETWORK, NODE, SERVICE)); - boolean isWillingForSomething = subject.isPayerWillingToCover(EnumSet.of(NETWORK, SERVICE)); - - // then: - assertFalse(isWillingForEverything); - assertTrue(isWillingForSomething); - } - - @Test - public void detectsParticipantSolvency() { - givenKnownFeeAmounts(); - given(check.canAfford(participant, network + service + node)).willReturn(false); - given(check.canAfford(participant, network + service)).willReturn(true); - - // when: - boolean canAffordEverything = subject.canParticipantAfford(participant, EnumSet.of(NETWORK, NODE, SERVICE)); - boolean canAffordSomething = subject.canParticipantAfford(participant, EnumSet.of(NETWORK, SERVICE)); - - // then: - assertFalse(canAffordEverything); - assertTrue(canAffordSomething); - } - - @Test - public void detectsPayerSolvency() { - givenKnownFeeAmounts(); - given(check.canAfford(payer, network + service + node)).willReturn(false); - given(check.canAfford(payer, network + service)).willReturn(true); - - // when: - boolean canAffordEverything = subject.canPayerAfford(EnumSet.of(NETWORK, NODE, SERVICE)); - boolean canAffordSomething = subject.canPayerAfford(EnumSet.of(NETWORK, SERVICE)); - - // then: - assertFalse(canAffordEverything); - assertTrue(canAffordSomething); - } - - @Test - public void incorporatesFeeAmounts() { - // setup: - EnumMap amounts = mock(EnumMap.class); - - // given: - subject.feeAmounts = amounts; - - // when: - subject.setFor(NODE, node); - - // then: - verify(amounts).put(NODE, node); - } - - @Test - public void approvesCredibleWillingness() { - given(check.canAfford(payer, willingness)).willReturn(true); - - // when: - boolean isCredible = subject.isPayerWillingnessCredible(); - - // then: - assertTrue(isCredible); - verify(check).canAfford(payer, willingness); - } - - @Test - public void participantCantAffordTest() { - // setup: - final BalanceCheck check = (payer, amount) -> amount < (node + network); - - final EnumSet fees = EnumSet.of(NETWORK, NODE); - subject.setFor(NETWORK, network); - subject.setFor(NODE, node); - subject.setBalanceCheck(check); - - // when: - boolean viability = subject.canParticipantAfford(master, fees); - // then: - assertFalse(viability); - } - - private void givenKnownFeeAmounts() { - subject.setFor(NETWORK, network); - subject.setFor(SERVICE, service); - subject.setFor(NODE, node); - } -} diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java deleted file mode 100644 index 1bb95327102f..000000000000 --- a/hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java +++ /dev/null @@ -1,341 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hedera.services.context.properties.GlobalDynamicProperties; -import com.hedera.services.fees.FeeExemptions; -import com.hedera.services.fees.TxnFeeType; -import com.hedera.services.ledger.HederaLedger; -import com.hedera.services.utils.SignedTxnAccessor; -import com.hedera.test.utils.IdUtils; -import com.hederahashgraph.api.proto.java.AccountAmount; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.TransactionBody; -import com.hederahashgraph.api.proto.java.TransferList; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.EnumMap; -import java.util.EnumSet; - -import static com.hedera.services.fees.TxnFeeType.NETWORK; -import static com.hedera.services.fees.TxnFeeType.NODE; -import static com.hedera.services.fees.TxnFeeType.SERVICE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_FEE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_NODE_SERVICE_FEES; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.collection.IsIterableContainingInOrder.contains; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.BDDMockito.any; -import static org.mockito.BDDMockito.anyLong; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.never; -import static org.mockito.BDDMockito.verify; - -class ItemizableFeeChargingTest { - long network = 500L, service = 200L, node = 100L; - - AccountID givenNode = IdUtils.asAccount("0.0.3"); - AccountID submittingNode = IdUtils.asAccount("0.0.4"); - AccountID payer = IdUtils.asAccount("0.0.1001"); - AccountID funding = IdUtils.asAccount("0.0.98"); - AccountID participant = IdUtils.asAccount("0.0.2002"); - - HederaLedger ledger; - FeeExemptions exemptions; - GlobalDynamicProperties properties; - TransactionBody txn; - SignedTxnAccessor accessor; - - ItemizableFeeCharging subject; - - @BeforeEach - private void setup() { - txn = mock(TransactionBody.class); - ledger = mock(HederaLedger.class); - accessor = mock(SignedTxnAccessor.class); - exemptions = mock(FeeExemptions.class); - properties = mock(GlobalDynamicProperties.class); - - given(txn.getNodeAccountID()).willThrow(IllegalStateException.class); - given(accessor.getTxn()).willReturn(txn); - given(accessor.getPayer()).willReturn(payer); - given(properties.fundingAccount()).willReturn(funding); - - subject = new ItemizableFeeCharging(ledger, exemptions, properties); - subject.setLedger(ledger); - - subject.resetFor(accessor, submittingNode); - } - - @Test - public void reportsChargedFeesForSubmittingNode() { - givenKnownFeeAmounts(); - given(ledger.getBalance(submittingNode)).willReturn(Long.MAX_VALUE); - - // when: - subject.chargeSubmittingNodeUpTo(NETWORK_NODE_SERVICE_FEES); - - // then: - assertEquals(network, subject.chargedToSubmittingNode(NETWORK)); - assertEquals(service, subject.chargedToSubmittingNode(SERVICE)); - } - - @Test - public void reportsChargedFeesForPayer() { - givenKnownFeeAmounts(); - - // when: - subject.chargePayer(EnumSet.of(NETWORK, NODE)); - - // then: - assertEquals(network, subject.chargedToPayer(NETWORK)); - assertEquals(node, subject.chargedToPayer(NODE)); - assertEquals(0, subject.chargedToPayer(SERVICE)); - } - - @Test - public void reportsTotalPayerFees() { - givenKnownFeeAmounts(); - - // when: - subject.chargePayer(EnumSet.of(NODE)); - - // then: - assertEquals(node , subject.totalFeesChargedToPayer()); - } - - @Test - public void doesntRecordSelfPayments() { - givenKnownFeeAmounts(); - given(accessor.getPayer()).willReturn(submittingNode); - - // when: - subject.chargePayer(EnumSet.of(NODE)); - subject.chargePayerUpTo(EnumSet.of(NODE)); - - // then: - assertTrue(subject.payerFeesCharged.isEmpty()); - } - - @Test - public void chargesNodeOnlyWhatsAvailableIfNecessary() { - givenKnownFeeAmounts(); - given(ledger.getBalance(submittingNode)).willReturn(network / 2); - - // when: - subject.chargeSubmittingNodeUpTo(EnumSet.of(NETWORK)); - - // then: - verify(ledger).doTransfer(submittingNode, funding, network / 2); - assertEquals(network / 2, subject.submittingNodeFeesCharged.get(NETWORK).longValue()); - } - - @Test - public void chargesNodeSuggestedIfPossible() { - givenKnownFeeAmounts(); - given(ledger.getBalance(submittingNode)).willReturn(network * 2); - - // when: - subject.chargeSubmittingNodeUpTo(EnumSet.of(NETWORK)); - - // then: - verify(ledger).doTransfer(submittingNode, funding, network); - assertEquals(network, subject.submittingNodeFeesCharged.get(NETWORK).longValue()); - } - - @Test - public void ignoresDegenerateFees() { - // given: - subject.setFor(NODE, 0L); - - // when: - subject.chargeParticipant(participant, EnumSet.of(NODE)); - - // then: - verify(ledger, never()).doTransfer(any(), any(), anyLong()); - } - - @Test - public void chargesPayerNothingWhenExempt() { - // setup: - EnumSet allPossibleFees = EnumSet.of(NETWORK, NODE, SERVICE); - - givenKnownFeeAmounts(); - given(ledger.getBalance(payer)).willReturn(Long.MAX_VALUE); - given(exemptions.hasExemptPayer(accessor)).willReturn(true); - // and: - subject.resetFor(accessor); - - // when: - subject.chargePayer(allPossibleFees); - subject.chargePayerUpTo(allPossibleFees); - - // then: - verify(ledger, never()).doTransfer(any(), any(), anyLong()); - // and: - assertTrue(subject.payerFeesCharged.isEmpty()); - } - - @Test - public void itemizesIrresponsibleSubmission() { - givenKnownFeeAmounts(); - given(ledger.getBalance(submittingNode)).willReturn(network - 1); - - // when: - subject.chargeSubmittingNodeUpTo(NETWORK_FEE); - // and: - TransferList itemizedFees = subject.itemizedFees(); - - // then: - assertThat( - itemizedFees.getAccountAmountsList(), - contains( - aa(funding, network - 1), - aa(submittingNode, 1 - network))); - } - - @Test - public void itemizesWhenNodeIsPayer() { - givenKnownFeeAmounts(); - given(ledger.getBalance(any())).willReturn(Long.MAX_VALUE); - given(accessor.getPayer()).willReturn(submittingNode); - - // when: - subject.chargePayer(NETWORK_NODE_SERVICE_FEES); - // and: - TransferList itemizedFees = subject.itemizedFees(); - - // then: - assertThat( - itemizedFees.getAccountAmountsList(), - contains( - aa(funding, network), - aa(submittingNode, -network), - aa(funding, service), - aa(submittingNode, -service))); - } - - @Test - public void itemizesStandardEvents() { - givenKnownFeeAmounts(); - given(ledger.getBalance(any())).willReturn(Long.MAX_VALUE); - - // when: - subject.chargePayer(NETWORK_NODE_SERVICE_FEES); - // and: - TransferList itemizedFees = subject.itemizedFees(); - - // then: - assertThat( - itemizedFees.getAccountAmountsList(), - contains( - aa(funding, network), - aa(payer, -network), - aa(submittingNode, node), - aa(payer, -node), - aa(funding, service), - aa(payer, -service))); - } - - private AccountAmount aa(AccountID who, long what) { - return AccountAmount.newBuilder().setAccountID(who).setAmount(what).build(); - } - - @Test - public void chargesParticipantWithCorrectBeneficiaries() { - givenKnownFeeAmounts(); - - // when: - subject.chargeParticipant(participant, EnumSet.of(NETWORK, NODE, SERVICE)); - - // then: - verify(ledger).doTransfer(participant, funding, network); - verify(ledger).doTransfer(participant, funding, service); - verify(ledger).doTransfer(participant, submittingNode, node); - // and: - assertTrue(subject.submittingNodeFeesCharged.isEmpty()); - assertTrue(subject.payerFeesCharged.isEmpty()); - } - - @Test - public void chargesPayerWithCorrectBeneficiaries() { - givenKnownFeeAmounts(); - - // when: - subject.chargePayer(EnumSet.of(NETWORK, NODE, SERVICE)); - - // then: - verify(ledger).doTransfer(payer, funding, network); - verify(ledger).doTransfer(payer, funding, service); - verify(ledger).doTransfer(payer, submittingNode, node); - // and: - assertEquals(network, subject.payerFeesCharged.get(NETWORK).longValue()); - assertEquals(service, subject.payerFeesCharged.get(SERVICE).longValue()); - assertEquals(node, subject.payerFeesCharged.get(NODE).longValue()); - } - - @Test - public void chargesPayerWithCorrectBeneficiariesUpToAvailable() { - givenKnownFeeAmounts(); - given(ledger.getBalance(payer)) - .willReturn(network + (node / 2)) - .willReturn(node / 2); - - // when: - subject.chargePayerUpTo(EnumSet.of(NETWORK, NODE)); - - // then: - verify(ledger).doTransfer(payer, funding, network); - verify(ledger).doTransfer(payer, submittingNode, node / 2); - // and: - assertEquals(network, subject.payerFeesCharged.get(NETWORK).longValue()); - assertEquals(node / 2, subject.payerFeesCharged.get(NODE).longValue()); - } - - @Test - public void resetsForNewTxn() { - // setup: - EnumMap paidByPayer = mock(EnumMap.class); - EnumMap paidByNode = mock(EnumMap.class); - - // given: - subject.funding = givenNode; - subject.submittingNodeFeesCharged = paidByNode; - subject.payerFeesCharged = paidByPayer; - - // when: - subject.resetFor(accessor, submittingNode); - - // then: - verify(paidByNode).clear(); - verify(paidByPayer).clear(); - assertEquals(funding, subject.funding); - } - - private void givenKnownFeeAmounts() { - subject.setFor(NETWORK, network); - subject.setFor(SERVICE, service); - subject.setFor(NODE, node); - } -} diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/NarratedLedgerChargingTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/NarratedLedgerChargingTest.java new file mode 100644 index 000000000000..095e3e452ed1 --- /dev/null +++ b/hedera-node/src/test/java/com/hedera/services/fees/charging/NarratedLedgerChargingTest.java @@ -0,0 +1,240 @@ +package com.hedera.services.fees.charging; + +/*- + * ‌ + * Hedera Services Node + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + +import com.hedera.services.context.NodeInfo; +import com.hedera.services.context.properties.GlobalDynamicProperties; +import com.hedera.services.fees.FeeExemptions; +import com.hedera.services.ledger.HederaLedger; +import com.hedera.services.state.merkle.MerkleAccount; +import com.hedera.services.state.merkle.MerkleEntityId; +import com.hedera.services.utils.TxnAccessor; +import com.hedera.test.factories.accounts.MerkleAccountFactory; +import com.hedera.test.utils.IdUtils; +import com.hederahashgraph.api.proto.java.AccountID; +import com.hederahashgraph.fee.FeeObject; +import com.swirlds.fcmap.FCMap; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +class NarratedLedgerChargingTest { + private final long submittingNodeId = 0L; + private final long nodeFee = 2L, networkFee = 4L, serviceFee = 6L; + private final FeeObject fees = new FeeObject(nodeFee, networkFee, serviceFee); + private final AccountID grpcNodeId = IdUtils.asAccount("0.0.3"); + private final AccountID grpcPayerId = IdUtils.asAccount("0.0.1234"); + private final AccountID grpcFundingId = IdUtils.asAccount("0.0.98"); + private final MerkleEntityId nodeId = new MerkleEntityId(0, 0, 3L); + private final MerkleEntityId payerId = new MerkleEntityId(0, 0, 1_234L); + + @Mock + private NodeInfo nodeInfo; + @Mock + private TxnAccessor accessor; + @Mock + private HederaLedger ledger; + @Mock + private FeeExemptions feeExemptions; + @Mock + private GlobalDynamicProperties dynamicProperties; + @Mock + private FCMap accounts; + + private NarratedLedgerCharging subject; + + @BeforeEach + void setUp() { + subject = new NarratedLedgerCharging(nodeInfo, ledger, feeExemptions, dynamicProperties, () -> accounts); + } + + @Test + void chargesNoFeesToExemptPayer() { + given(feeExemptions.hasExemptPayer(accessor)).willReturn(true); + given(accessor.getPayer()).willReturn(grpcPayerId); + subject.resetForTxn(accessor, submittingNodeId); + + // when: + subject.chargePayerAllFees(); + subject.chargePayerServiceFee(); + subject.chargePayerNetworkAndUpToNodeFee(); + + // then: + verifyNoInteractions(ledger); + } + + @Test + void chargesAllFeesToPayerAsExpected() { + givenSetupToChargePayer(nodeFee + networkFee + serviceFee, nodeFee + networkFee + serviceFee); + + // expect: + assertTrue(subject.canPayerAffordAllFees()); + assertTrue(subject.isPayerWillingToCoverAllFees()); + + // when: + subject.chargePayerAllFees(); + + // then: + verify(ledger).adjustBalance(grpcPayerId, -(nodeFee + networkFee + serviceFee)); + verify(ledger).adjustBalance(grpcNodeId, +nodeFee); + verify(ledger).adjustBalance(grpcFundingId, +(networkFee + serviceFee)); + assertEquals(nodeFee + networkFee + serviceFee, subject.totalFeesChargedToPayer()); + } + + @Test + void chargesServiceFeeToPayerAsExpected() { + givenSetupToChargePayer(serviceFee, serviceFee); + + // expect: + assertTrue(subject.canPayerAffordServiceFee()); + assertTrue(subject.isPayerWillingToCoverServiceFee()); + + // when: + subject.chargePayerServiceFee(); + + // then: + verify(ledger).adjustBalance(grpcPayerId, -serviceFee); + verify(ledger).adjustBalance(grpcFundingId, +serviceFee); + assertEquals(serviceFee, subject.totalFeesChargedToPayer()); + } + + @Test + void chargesNetworkAndUpToNodeFeeToPayerAsExpected() { + givenSetupToChargePayer(networkFee + nodeFee / 2, nodeFee + networkFee + serviceFee); + + // when: + subject.chargePayerNetworkAndUpToNodeFee(); + + // then: + verify(ledger).adjustBalance(grpcPayerId, -(networkFee + nodeFee / 2)); + verify(ledger).adjustBalance(grpcFundingId, +networkFee); + verify(ledger).adjustBalance(grpcNodeId, nodeFee / 2); + assertEquals(networkFee + nodeFee / 2, subject.totalFeesChargedToPayer()); + } + + @Test + void chargesNodeUpToNetworkFeeAsExpected() { + givenSetupToChargeNode(networkFee - 1); + + // when: + subject.chargeSubmittingNodeUpToNetworkFee(); + + // then: + verify(ledger).adjustBalance(grpcNodeId, -networkFee + 1); + verify(ledger).adjustBalance(grpcFundingId, +networkFee - 1); + assertEquals(0, subject.totalFeesChargedToPayer()); + } + + @Test + void throwsIseIfPayerNotActuallyExtant() { + // expect: + assertThrows(IllegalStateException.class, subject::canPayerAffordAllFees); + assertThrows(IllegalStateException.class, subject::canPayerAffordNetworkFee); + + given(accessor.getPayer()).willReturn(grpcPayerId); + // and given: + subject.resetForTxn(accessor, submittingNodeId); + subject.setFees(fees); + + // still expect: + assertThrows(IllegalStateException.class, subject::canPayerAffordAllFees); + assertThrows(IllegalStateException.class, subject::canPayerAffordNetworkFee); + } + + @Test + void detectsLackOfWillingness() { + given(accessor.getPayer()).willReturn(grpcPayerId); + + subject.resetForTxn(accessor, submittingNodeId); + subject.setFees(fees); + + // expect: + assertFalse(subject.isPayerWillingToCoverAllFees()); + assertFalse(subject.isPayerWillingToCoverNetworkFee()); + assertFalse(subject.isPayerWillingToCoverServiceFee()); + } + + @Test + void exemptPayerNeedsNoAbility() { + given(accessor.getPayer()).willReturn(grpcPayerId); + given(feeExemptions.hasExemptPayer(accessor)).willReturn(true); + + subject.resetForTxn(accessor, submittingNodeId); + subject.setFees(fees); + + // expect: + assertTrue(subject.canPayerAffordAllFees()); + assertTrue(subject.canPayerAffordServiceFee()); + assertTrue(subject.canPayerAffordNetworkFee()); + } + + @Test + void exemptPayerNeedsNoWillingness() { + given(accessor.getPayer()).willReturn(grpcPayerId); + given(feeExemptions.hasExemptPayer(accessor)).willReturn(true); + + subject.resetForTxn(accessor, submittingNodeId); + subject.setFees(fees); + + // expect: + assertTrue(subject.isPayerWillingToCoverAllFees()); + assertTrue(subject.isPayerWillingToCoverNetworkFee()); + assertTrue(subject.isPayerWillingToCoverServiceFee()); + } + + private void givenSetupToChargePayer(long payerBalance, long totalOfferedFee) { + final var payerAccount = MerkleAccountFactory.newAccount().balance(payerBalance).get(); + given(accounts.get(payerId)).willReturn(payerAccount); + + given(dynamicProperties.fundingAccount()).willReturn(grpcFundingId); + given(nodeInfo.accountOf(submittingNodeId)).willReturn(grpcNodeId); + given(nodeInfo.accountKeyOf(submittingNodeId)).willReturn(nodeId); + + given(accessor.getPayer()).willReturn(grpcPayerId); + given(accessor.getOfferedFee()).willReturn(totalOfferedFee); + subject.resetForTxn(accessor, submittingNodeId); + subject.setFees(fees); + } + + private void givenSetupToChargeNode(long nodeBalance) { + final var nodeAccount = MerkleAccountFactory.newAccount().balance(nodeBalance).get(); + given(accounts.get(nodeId)).willReturn(nodeAccount); + + given(dynamicProperties.fundingAccount()).willReturn(grpcFundingId); + given(nodeInfo.accountOf(submittingNodeId)).willReturn(nodeId.toAccountId()); + given(nodeInfo.accountKeyOf(submittingNodeId)).willReturn(nodeId); + + given(accessor.getPayer()).willReturn(grpcPayerId); + subject.resetForTxn(accessor, submittingNodeId); + subject.setFees(fees); + } +} diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnChargingPolicyAgentTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnChargingPolicyAgentTest.java new file mode 100644 index 000000000000..2e6302cffb4d --- /dev/null +++ b/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnChargingPolicyAgentTest.java @@ -0,0 +1,158 @@ +package com.hedera.services.fees.charging; + +/*- + * ‌ + * Hedera Services Node + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + +import com.hedera.services.context.TransactionContext; +import com.hedera.services.context.primitives.StateView; +import com.hedera.services.fees.FeeCalculator; +import com.hedera.services.legacy.core.jproto.JKey; +import com.hedera.services.records.TxnIdRecentHistory; +import com.hedera.services.state.logic.AwareNodeDiligenceScreen; +import com.hedera.services.utils.SignedTxnAccessor; +import com.hedera.services.utils.TxnAccessor; +import com.hedera.test.factories.scenarios.TxnHandlingScenario; +import com.hedera.test.utils.IdUtils; +import com.hederahashgraph.api.proto.java.Timestamp; +import com.hederahashgraph.api.proto.java.Transaction; +import com.hederahashgraph.api.proto.java.TransactionBody; +import com.hederahashgraph.api.proto.java.TransactionID; +import com.hederahashgraph.fee.FeeObject; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Map; + +import static com.hedera.services.txns.diligence.DuplicateClassification.BELIEVED_UNIQUE; +import static com.hedera.services.txns.diligence.DuplicateClassification.DUPLICATE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.DUPLICATE_TRANSACTION; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class TxnChargingPolicyAgentTest { + private final long submittingNode = 1L; + private final JKey payerKey = TxnHandlingScenario.MISC_ACCOUNT_KT.asJKeyUnchecked(); + private final FeeObject mockFees = new FeeObject(1L, 2L, 3L); + private final TxnAccessor accessor = SignedTxnAccessor.uncheckedFrom(Transaction.newBuilder() + .setBodyBytes(TransactionBody.newBuilder() + .setTransactionID(TransactionID.newBuilder() + .setTransactionValidStart(Timestamp.newBuilder() + .setSeconds(1_234_567L) + .build()) + .setAccountID(IdUtils.asAccount("0.0.1234"))) + .build() + .toByteString()) + .build()); + + @Mock + private StateView currentView; + @Mock + private FeeCalculator fees; + @Mock + private TxnIdRecentHistory recentHistory; + @Mock + private FeeChargingPolicy chargingPolicy; + @Mock + private TransactionContext txnCtx; + @Mock + private AwareNodeDiligenceScreen nodeDiligenceScreen; + @Mock + private Map txnHistories; + + private TxnChargingPolicyAgent subject; + + @BeforeEach + void setUp() { + subject = new TxnChargingPolicyAgent( + fees, chargingPolicy, txnCtx, () -> currentView, nodeDiligenceScreen, txnHistories); + } + + @Test + void appliesForLackOfNodeDueDiligence() { + givenBaseCtx(); + given(nodeDiligenceScreen.nodeIgnoredDueDiligence(BELIEVED_UNIQUE)).willReturn(true); + + // when: + final var shouldContinue = subject.applyPolicyFor(accessor); + + // then: + assertFalse(shouldContinue); + verify(chargingPolicy).applyForIgnoredDueDiligence(mockFees); + } + + @Test + void appliesForPayerDuplicate() { + givenBaseCtx(); + given(txnCtx.submittingSwirldsMember()).willReturn(submittingNode); + given(txnHistories.get(accessor.getTxnId())).willReturn(recentHistory); + given(recentHistory.currentDuplicityFor(submittingNode)).willReturn(DUPLICATE); + + // when: + final var shouldContinue = subject.applyPolicyFor(accessor); + + // then: + assertFalse(shouldContinue); + verify(txnCtx).setStatus(DUPLICATE_TRANSACTION); + verify(chargingPolicy).applyForDuplicate(mockFees); + } + + @Test + void appliesForNonOkOutcome() { + givenBaseCtx(); + given(chargingPolicy.apply(mockFees)).willReturn(INSUFFICIENT_PAYER_BALANCE); + + // when: + final var shouldContinue = subject.applyPolicyFor(accessor); + + // then: + assertFalse(shouldContinue); + verify(txnCtx).setStatus(INSUFFICIENT_PAYER_BALANCE); + verify(chargingPolicy).apply(mockFees); + } + + @Test + void appliesForOkOutcome() { + givenBaseCtx(); + given(chargingPolicy.apply(mockFees)).willReturn(OK); + + // when: + final var shouldContinue = subject.applyPolicyFor(accessor); + + // then: + assertTrue(shouldContinue); + verify(txnCtx, never()).setStatus(any()); + verify(chargingPolicy).apply(mockFees); + } + + private void givenBaseCtx() { + given(txnCtx.activePayerKey()).willReturn(payerKey); + given(fees.computeFee(accessor, payerKey, currentView)).willReturn(mockFees); + } +} diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java deleted file mode 100644 index c75d76f4e3eb..000000000000 --- a/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java +++ /dev/null @@ -1,317 +0,0 @@ -package com.hedera.services.fees.charging; - -/*- - * ‌ - * Hedera Services Node - * ​ - * Copyright (C) 2018 - 2021 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. - * ‍ - */ - -import com.hedera.services.context.properties.GlobalDynamicProperties; -import com.hedera.services.fees.FeeExemptions; -import com.hedera.services.ledger.HederaLedger; -import com.hedera.services.utils.SignedTxnAccessor; -import com.hedera.services.utils.TxnAccessor; -import com.hedera.test.utils.IdUtils; -import com.hederahashgraph.api.proto.java.AccountID; -import com.hederahashgraph.api.proto.java.ResponseCodeEnum; -import com.hederahashgraph.api.proto.java.TransactionBody; -import com.hederahashgraph.fee.FeeObject; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static com.hedera.services.fees.TxnFeeType.NETWORK; -import static com.hedera.services.fees.TxnFeeType.NODE; -import static com.hedera.services.fees.TxnFeeType.SERVICE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_FEE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_NODE_SERVICE_FEES; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.NODE_FEE; -import static com.hedera.services.fees.charging.ItemizableFeeCharging.SERVICE_FEE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE; -import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.BDDMockito.any; -import static org.mockito.BDDMockito.argThat; -import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.longThat; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.never; -import static org.mockito.BDDMockito.verify; - -class TxnFeeChargingPolicyTest { - private TxnFeeChargingPolicy subject = new TxnFeeChargingPolicy(); - private final long node = 1, network = 2, service = 3; - FeeObject fee; - - ItemizableFeeCharging charging; - - @BeforeEach - private void setup() { - fee = new FeeObject(node, network, service); - charging = mock(ItemizableFeeCharging.class); - } - - @Test - public void chargesNodePenaltyForSuspectChronology() { - // when: - ResponseCodeEnum outcome = subject.applyForIgnoredDueDiligence(charging, fee); - - // then: - verify(charging).setFor(NETWORK, network); - verify(charging).chargeSubmittingNodeUpTo(NETWORK_FEE); - // and: - assertEquals(OK, outcome); - } - - @Test - public void chargesNonServicePenaltyForUnableToCoverTotal() { - given(charging.isPayerWillingToCover(NETWORK_FEE)).willReturn(true); - given(charging.canPayerAfford(NETWORK_FEE)).willReturn(true); - given(charging.isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES)).willReturn(true); - given(charging.canPayerAfford(NETWORK_NODE_SERVICE_FEES)).willReturn(false); - - // when: - ResponseCodeEnum outcome = subject.apply(charging, fee); - - // then: - verify(charging).setFor(NODE, node); - verify(charging).setFor(NETWORK, network); - verify(charging).setFor(SERVICE, service); - // and: - verify(charging).isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES); - verify(charging).canPayerAfford(NETWORK_NODE_SERVICE_FEES); - verify(charging).chargePayer(NETWORK_FEE); - verify(charging).chargePayerUpTo(NODE_FEE); - // and: - assertEquals(INSUFFICIENT_PAYER_BALANCE, outcome); - } - - @Test - public void liveFireWorksForTriggered() { - // setup: - TransactionBody txn = mock(TransactionBody.class); - AccountID submittingNode = IdUtils.asAccount("0.0.3"); - AccountID payer = IdUtils.asAccount("0.0.1001"); - AccountID funding = IdUtils.asAccount("0.0.98"); - HederaLedger ledger = mock(HederaLedger.class); - GlobalDynamicProperties properties = mock(GlobalDynamicProperties.class); - SignedTxnAccessor accessor = mock(SignedTxnAccessor.class); - charging = new ItemizableFeeCharging(ledger, new NoExemptions(), properties); - - given(ledger.getBalance(any())).willReturn(Long.MAX_VALUE); - given(properties.fundingAccount()).willReturn(funding); - given(txn.getTransactionFee()).willReturn(10L); - given(accessor.getTxn()).willReturn(txn); - - given(accessor.getPayer()).willReturn(payer); - - // when: - charging.resetFor(accessor, submittingNode); - ResponseCodeEnum outcome = subject.applyForTriggered(charging, fee); - - // then: - verify(ledger).doTransfer(payer, funding, service); - verify(ledger, never()).doTransfer( - argThat(payer::equals), - argThat(funding::equals), - longThat(l -> l == network)); - verify(ledger, never()).doTransfer( - argThat(payer::equals), - argThat(submittingNode::equals), - longThat(l -> l == node)); - // and: - assertEquals(OK, outcome); - } - - @Test - public void liveFireDiscountWorksForDuplicate() { - // setup: - TransactionBody txn = mock(TransactionBody.class); - AccountID submittingNode = IdUtils.asAccount("0.0.3"); - AccountID payer = IdUtils.asAccount("0.0.1001"); - AccountID funding = IdUtils.asAccount("0.0.98"); - HederaLedger ledger = mock(HederaLedger.class); - GlobalDynamicProperties properties = mock(GlobalDynamicProperties.class); - SignedTxnAccessor accessor = mock(SignedTxnAccessor.class); - charging = new ItemizableFeeCharging(ledger, new NoExemptions(), properties); - - given(ledger.getBalance(any())).willReturn(Long.MAX_VALUE); - given(properties.fundingAccount()).willReturn(funding); - given(txn.getNodeAccountID()).willReturn(submittingNode); - given(txn.getTransactionFee()).willReturn(10L); - given(accessor.getTxn()).willReturn(txn); - - given(accessor.getPayer()).willReturn(payer); - - // when: - charging.resetFor(accessor, submittingNode); - ResponseCodeEnum outcome = subject.applyForDuplicate(charging, fee); - - // then: - verify(ledger).doTransfer(payer, funding, network); - verify(ledger).doTransfer(payer, submittingNode, node); - verify(ledger, never()).doTransfer( - argThat(payer::equals), - argThat(funding::equals), - longThat(l -> l == service)); - // and: - assertEquals(OK, outcome); - } - - private static class NoExemptions implements FeeExemptions { - @Override - public boolean hasExemptPayer(TxnAccessor accessor) { - return false; - } - } - - @Test - public void chargesDiscountedFeesAsExpectedForDuplicate() { - given(charging.isPayerWillingToCover(NETWORK_FEE)).willReturn(true); - given(charging.canPayerAfford(NETWORK_FEE)).willReturn(true); - given(charging.isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES)).willReturn(true); - given(charging.canPayerAfford(NETWORK_NODE_SERVICE_FEES)).willReturn(true); - - // when: - ResponseCodeEnum outcome = subject.applyForDuplicate(charging, fee); - - // then: - verify(charging).setFor(NODE, node); - verify(charging).setFor(NETWORK, network); - verify(charging).setFor(SERVICE, service); - verify(charging).setFor(SERVICE, 0); - // and: - verify(charging).isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES); - verify(charging).canPayerAfford(NETWORK_NODE_SERVICE_FEES); - verify(charging).chargePayer(NETWORK_NODE_SERVICE_FEES); - // and: - assertEquals(OK, outcome); - } - - @Test - public void chargesFullFeesAsExpected() { - given(charging.isPayerWillingToCover(NETWORK_FEE)).willReturn(true); - given(charging.canPayerAfford(NETWORK_FEE)).willReturn(true); - given(charging.isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES)).willReturn(true); - given(charging.canPayerAfford(NETWORK_NODE_SERVICE_FEES)).willReturn(true); - - // when: - ResponseCodeEnum outcome = subject.apply(charging, fee); - - // then: - verify(charging).setFor(NODE, node); - verify(charging).setFor(NETWORK, network); - verify(charging).setFor(SERVICE, service); - // and: - verify(charging).isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES); - verify(charging).canPayerAfford(NETWORK_NODE_SERVICE_FEES); - verify(charging).chargePayer(NETWORK_NODE_SERVICE_FEES); - // and: - assertEquals(OK, outcome); - } - - @Test - public void chargesNonServicePenaltyForUnwillingToCoverTotal() { - given(charging.isPayerWillingToCover(NETWORK_FEE)).willReturn(true); - given(charging.canPayerAfford(NETWORK_FEE)).willReturn(true); - given(charging.isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES)).willReturn(false); - - // when: - ResponseCodeEnum outcome = subject.apply(charging, fee); - - // then: - verify(charging).setFor(NODE, node); - verify(charging).setFor(NETWORK, network); - verify(charging).setFor(SERVICE, service); - // and: - verify(charging).isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES); - verify(charging).chargePayer(NETWORK_FEE); - verify(charging).chargePayerUpTo(NODE_FEE); - // and: - assertEquals(INSUFFICIENT_TX_FEE, outcome); - } - - @Test - public void requiresWillingToPayServiceWhenTriggeredTxn() { - given(charging.isPayerWillingToCover(SERVICE_FEE)).willReturn(false); - - // when: - ResponseCodeEnum outcome = subject.applyForTriggered(charging, fee); - - // then: - verify(charging).setFor(SERVICE, service); - // and: - verify(charging).isPayerWillingToCover(SERVICE_FEE); - // and: - assertEquals(INSUFFICIENT_TX_FEE, outcome); - } - - @Test - public void requiresAbleToPayServiceWhenTriggeredTxn() { - given(charging.isPayerWillingToCover(SERVICE_FEE)).willReturn(true); - given(charging.canPayerAfford(SERVICE_FEE)).willReturn(false); - - // when: - ResponseCodeEnum outcome = subject.applyForTriggered(charging, fee); - - // then: - verify(charging).setFor(SERVICE, service); - // and: - verify(charging).isPayerWillingToCover(SERVICE_FEE); - verify(charging).canPayerAfford(SERVICE_FEE); - // and: - assertEquals(INSUFFICIENT_PAYER_BALANCE, outcome); - } - - @Test - public void chargesNodePenaltyForPayerUnableToPayNetwork() { - given(charging.isPayerWillingToCover(NETWORK_FEE)).willReturn(true); - given(charging.canPayerAfford(NETWORK_FEE)).willReturn(false); - - // when: - ResponseCodeEnum outcome = subject.apply(charging, fee); - - // then: - verify(charging).setFor(NODE, node); - verify(charging).setFor(NETWORK, network); - verify(charging).setFor(SERVICE, service); - // and: - verify(charging).isPayerWillingToCover(NETWORK_FEE); - verify(charging).canPayerAfford(NETWORK_FEE); - verify(charging).chargeSubmittingNodeUpTo(NETWORK_FEE); - // and: - assertEquals(INSUFFICIENT_PAYER_BALANCE, outcome); - } - - @Test - public void chargesNodePenaltyForPayerUnwillingToPayNetwork() { - given(charging.isPayerWillingToCover(NETWORK_FEE)).willReturn(false); - - // when: - ResponseCodeEnum outcome = subject.apply(charging, fee); - - // then: - verify(charging).setFor(NODE, node); - verify(charging).setFor(NETWORK, network); - verify(charging).setFor(SERVICE, service); - // and: - verify(charging).isPayerWillingToCover(NETWORK_FEE); - verify(charging).chargeSubmittingNodeUpTo(NETWORK_FEE); - // and: - assertEquals(INSUFFICIENT_TX_FEE, outcome); - } -} diff --git a/hedera-node/src/test/java/com/hedera/services/legacy/services/state/AwareProcessLogicTest.java b/hedera-node/src/test/java/com/hedera/services/legacy/services/state/AwareProcessLogicTest.java index 9f4a0a5d2dca..99bdbb25bea9 100644 --- a/hedera-node/src/test/java/com/hedera/services/legacy/services/state/AwareProcessLogicTest.java +++ b/hedera-node/src/test/java/com/hedera/services/legacy/services/state/AwareProcessLogicTest.java @@ -24,9 +24,8 @@ import com.hedera.services.context.ServicesContext; import com.hedera.services.context.TransactionContext; import com.hedera.services.fees.FeeCalculator; -import com.hedera.services.fees.charging.TxnFeeChargingPolicy; +import com.hedera.services.fees.charging.FeeChargingPolicy; import com.hedera.services.ledger.HederaLedger; -import com.hedera.services.legacy.handler.SmartContractRequestHandler; import com.hedera.services.records.AccountRecordsHistorian; import com.hedera.services.records.TxnIdRecentHistory; import com.hedera.services.security.ops.SystemOpAuthorization; @@ -41,7 +40,6 @@ import com.hedera.services.stream.RecordStreamManager; import com.hedera.services.stream.RecordStreamObject; import com.hedera.services.txns.TransitionLogicLookup; -import com.hedera.services.txns.validation.OptionValidator; import com.hedera.services.utils.PlatformTxnAccessor; import com.hedera.services.utils.TxnAccessor; import com.hedera.test.utils.IdUtils; @@ -100,7 +98,7 @@ void setup() { final TxnIdRecentHistory recentHistory = mock(TxnIdRecentHistory.class); final Map histories = mock(Map.class); final AccountID accountID = mock(AccountID.class); - final TxnFeeChargingPolicy policy = mock(TxnFeeChargingPolicy.class); + final FeeChargingPolicy policy = mock(FeeChargingPolicy.class); final SystemOpPolicies policies = mock(SystemOpPolicies.class); final TransitionLogicLookup lookup = mock(TransitionLogicLookup.class); final EntityAutoRenewal entityAutoRenewal = mock(EntityAutoRenewal.class); @@ -150,7 +148,7 @@ void setup() { given(recentHistory.currentDuplicityFor(anyLong())).willReturn(BELIEVED_UNIQUE); given(txnBody.getNodeAccountID()).willReturn(accountID); - given(policy.apply(any(), any())).willReturn(ResponseCodeEnum.OK); + given(policy.apply(any())).willReturn(ResponseCodeEnum.OK); given(policies.check(any())).willReturn(SystemOpAuthorization.AUTHORIZED); given(lookup.lookupFor(any(), any())).willReturn(Optional.empty()); given(ctx.entityAutoRenewal()).willReturn(entityAutoRenewal); @@ -190,6 +188,7 @@ void decrementsParentConsensusTimeIfCanTrigger() { // and: final var triggeredTxn = mock(TxnAccessor.class); + given(triggeredTxn.isTriggeredTxn()).willReturn(true); given(txnCtx.triggeredTxn()).willReturn(triggeredTxn); given(invariantChecks.holdFor(any(), eq(consensusNow.minusNanos(1L)), eq(666L))).willReturn(true); diff --git a/hedera-node/src/test/java/com/hedera/services/legacy/services/state/RecordMgmtTest.java b/hedera-node/src/test/java/com/hedera/services/legacy/services/state/RecordMgmtTest.java index 544231d38eec..f068488aba36 100644 --- a/hedera-node/src/test/java/com/hedera/services/legacy/services/state/RecordMgmtTest.java +++ b/hedera-node/src/test/java/com/hedera/services/legacy/services/state/RecordMgmtTest.java @@ -1,5 +1,25 @@ package com.hedera.services.legacy.services.state; +/*- + * ‌ + * Hedera Services Node + * ​ + * Copyright (C) 2018 - 2021 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. + * ‍ + */ + import com.hedera.services.context.ServicesContext; import com.hedera.services.context.TransactionContext; import com.hedera.services.records.AccountRecordsHistorian; @@ -78,4 +98,4 @@ void doesNothingIfNoLastCreatedRecord() { // then: verifyNoInteractions(recordStreamManager); } -} \ No newline at end of file +} diff --git a/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeHandlerTest.java b/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeHandlerTest.java index 0e7d033573cc..d37775016a13 100644 --- a/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeHandlerTest.java +++ b/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeHandlerTest.java @@ -100,7 +100,7 @@ void setUp() { } @Test - void freezeTest() throws Exception { + void setsInstantInSameDayWhenNatural() throws Exception { // setup: Transaction transaction = FreezeTestHelper.createFreezeTransaction(true, true, null); TransactionBody txBody = CommonUtils.extractTransactionBody(transaction); @@ -117,6 +117,30 @@ void freezeTest() throws Exception { verify(dualState).setFreezeTime(expectedStart); } + @Test + void setsInstantInNextDayWhenNatural() throws Exception { + // setup: + Transaction transaction = FreezeTestHelper.createFreezeTransaction( + true, + true, + null, + null, + new int[] { 10, 0 }, + new int[] { 10, 1 }); + TransactionBody txBody = CommonUtils.extractTransactionBody(transaction); + // and: + final var nominalStartHour = txBody.getFreeze().getStartHour(); + final var nominalStartMin = txBody.getFreeze().getStartMin(); + final var expectedStart = naturalNextInstant(nominalStartHour, nominalStartMin, consensusTime); + + // when: + TransactionRecord record = subject.freeze(txBody, consensusTime); + + // then: + assertEquals(record.getReceipt().getStatus(), ResponseCodeEnum.SUCCESS); + verify(dualState).setFreezeTime(expectedStart); + } + @Test void computesMinsSinceConsensusMidnight() { // given: diff --git a/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeTestHelper.java b/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeTestHelper.java index 844baec33fba..e5aaebb53d10 100644 --- a/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeTestHelper.java +++ b/hedera-node/src/test/java/com/hedera/services/legacy/unit/FreezeTestHelper.java @@ -40,10 +40,21 @@ static Transaction createFreezeTransaction(boolean paidBy58, boolean valid, File } static Transaction createFreezeTransaction(boolean paidBy58, boolean valid, FileID fileID, byte[] fileHash) { + int[] startHourMin = CommonUtilsTest.getUTCHourMinFromMillis(System.currentTimeMillis() + 60000); + int[] endHourMin = CommonUtilsTest.getUTCHourMinFromMillis(System.currentTimeMillis() + 120000); + return createFreezeTransaction(paidBy58, valid, fileID, fileHash, startHourMin, endHourMin); + } + + static Transaction createFreezeTransaction( + boolean paidBy58, + boolean valid, + FileID fileID, + byte[] fileHash, + int[] startHourMin, + int[] endHourMin + ) { FreezeTransactionBody freezeBody; if (valid) { - int[] startHourMin = CommonUtilsTest.getUTCHourMinFromMillis(System.currentTimeMillis() + 60000); - int[] endHourMin = CommonUtilsTest.getUTCHourMinFromMillis(System.currentTimeMillis() + 120000); var builder = getFreezeTranBuilder(startHourMin[0], startHourMin[1], endHourMin[0], endHourMin[1]); if (fileID != null) { builder.setUpdateFile(fileID); @@ -77,6 +88,7 @@ static Transaction createFreezeTransaction(boolean paidBy58, boolean valid, File return Transaction.newBuilder().setBodyBytes(bodyBytes).build(); } + private static FreezeTransactionBody.Builder getFreezeTranBuilder(int startHour, int startMin, int endHour, int endMin){ return FreezeTransactionBody.newBuilder() .setStartHour(startHour).setStartMin(startMin) diff --git a/hedera-node/src/test/java/com/hedera/services/utils/SignedTxnAccessorTest.java b/hedera-node/src/test/java/com/hedera/services/utils/SignedTxnAccessorTest.java index 23f7a3a2e217..ae64cfd6a182 100644 --- a/hedera-node/src/test/java/com/hedera/services/utils/SignedTxnAccessorTest.java +++ b/hedera-node/src/test/java/com/hedera/services/utils/SignedTxnAccessorTest.java @@ -50,9 +50,10 @@ public class SignedTxnAccessorTest { @Test public void parsesLegacyCorrectly() throws Exception { // setup: + final long offeredFee = 100_000_000L; Transaction transaction = RequestBuilder.getCryptoTransferRequest(1234l, 0l, 0l, 3l, 0l, 0l, - 100_000_000l, + offeredFee, Timestamp.getDefaultInstance(), Duration.getDefaultInstance(), false, @@ -75,6 +76,7 @@ public void parsesLegacyCorrectly() throws Exception { assertEquals(body.getTransactionID(), accessor.getTxnId()); assertEquals(1234l, accessor.getPayer().getAccountNum()); assertEquals(HederaFunctionality.CryptoTransfer, accessor.getFunction()); + assertEquals(offeredFee, accessor.getOfferedFee()); assertArrayEquals(CommonUtils.noThrowSha384HashOf(transaction.toByteArray()), accessor.getHash().toByteArray()); assertEquals(expectedMap, accessor.getSigMap()); } diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java index 9c9f3be8268c..2670cbe6a4d1 100644 --- a/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java +++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java @@ -21,6 +21,7 @@ */ import com.hedera.services.bdd.spec.HapiApiSpec; +import com.hedera.services.bdd.spec.HapiPropertySource; import com.hedera.services.bdd.spec.fees.TinyBarTransfers; import com.hedera.services.bdd.spec.transactions.TxnUtils; import com.hederahashgraph.api.proto.java.AccountID; @@ -55,6 +56,10 @@ public static TransferListAsserts includingDeduction(LongSupplier from, long amo return new DeductionAsserts(from, amount); } + public static TransferListAsserts includingDeduction(String from, long amount) { + return new SpecificDeductionAsserts(from, amount); + } + public static TransferListAsserts includingDeduction(String desc, String payer) { return new QualifyingDeductionAssert(desc, payer); } @@ -159,7 +164,7 @@ public NonEmptyTransferAsserts() { class DeductionAsserts extends TransferListAsserts { public DeductionAsserts(LongSupplier from, long amount) { - registerProvider((sepc, o) -> { + registerProvider((spec, o) -> { TransferList transfers = (TransferList) o; long num = from.getAsLong(); Assert.assertTrue( @@ -170,3 +175,17 @@ public DeductionAsserts(LongSupplier from, long amount) { }); } } + +class SpecificDeductionAsserts extends TransferListAsserts { + public SpecificDeductionAsserts(String account, long amount) { + registerProvider((spec, o) -> { + TransferList transfers = (TransferList) o; + AccountID payer = asId(account, spec); + Assert.assertTrue( + String.format("No deduction of -%d tinyBars from %s detected!", amount, account), + transfers.getAccountAmountsList() + .stream() + .anyMatch(aa -> aa.getAmount() == -amount && aa.getAccountID().equals(payer))); + }); + } +} diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/fees/FeeCalculator.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/fees/FeeCalculator.java index ab54fb5bbecf..95ab99ca46c5 100644 --- a/test-clients/src/main/java/com/hedera/services/bdd/spec/fees/FeeCalculator.java +++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/fees/FeeCalculator.java @@ -27,6 +27,7 @@ import com.hederahashgraph.api.proto.java.HederaFunctionality; import com.hederahashgraph.api.proto.java.Transaction; import com.hederahashgraph.api.proto.java.TransactionBody; +import com.hederahashgraph.fee.FeeObject; import com.hederahashgraph.fee.SigValueObj; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -35,7 +36,9 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import static com.hederahashgraph.fee.FeeBuilder.getFeeObject; import static com.hederahashgraph.fee.FeeBuilder.getSignatureCount; import static com.hederahashgraph.fee.FeeBuilder.getSignatureSize; import static com.hederahashgraph.fee.FeeBuilder.getTotalFeeforRequest; @@ -96,6 +99,17 @@ public long forOp(HederaFunctionality op, FeeData knownActivity) { return maxFeeTinyBars(); } + public long forOpWithDetails(HederaFunctionality op, FeeData knownActivity, AtomicReference obs) { + try { + final var activityPrices = opFeeData.get(op); + final var fees = getFeeObject(activityPrices, knownActivity, provider.rates()); + obs.set(fees); + return getTotalFeeforRequest(activityPrices, knownActivity, provider.rates()); + } catch (Throwable t) { + throw new IllegalArgumentException("Calculation not observable!", t); + } + } + @FunctionalInterface public interface ActivityMetrics { FeeData compute(TransactionBody body, SigValueObj sigUsage) throws Throwable; @@ -111,6 +125,17 @@ public long forActivityBasedOp( return forOp(op, activityMetrics); } + public long forActivityBasedOpWithDetails( + HederaFunctionality op, + ActivityMetrics metricsCalculator, + Transaction txn, + int numPayerSigs, + AtomicReference obs + ) throws Throwable { + FeeData activityMetrics = metricsFor(txn, numPayerSigs, metricsCalculator); + return forOpWithDetails(op, activityMetrics, obs); + } + private FeeData metricsFor( Transaction txn, int numPayerSigs, diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoTransfer.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoTransfer.java index 78b039eaddb7..f667cfbe0caa 100644 --- a/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoTransfer.java +++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/crypto/HapiCryptoTransfer.java @@ -39,6 +39,7 @@ import com.hederahashgraph.api.proto.java.TransactionBody; import com.hederahashgraph.api.proto.java.TransactionResponse; import com.hederahashgraph.api.proto.java.TransferList; +import com.hederahashgraph.fee.FeeObject; import com.hederahashgraph.fee.SigValueObj; import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; @@ -52,6 +53,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.BinaryOperator; import java.util.function.Consumer; import java.util.function.Function; @@ -82,6 +84,7 @@ public class HapiCryptoTransfer extends HapiTxnOp { private Function hbarOnlyProvider = MISSING_HBAR_ONLY_PROVIDER; private Optional tokenWithEmptyTransferAmounts = Optional.empty(); private Optional> appendedFromTo = Optional.empty(); + private Optional> feesObserver = Optional.empty(); @Override public HederaFunctionality type() { @@ -98,6 +101,11 @@ public HapiCryptoTransfer breakingNetZeroInvariant() { return this; } + public HapiCryptoTransfer exposingFeesTo(AtomicReference obs) { + feesObserver = Optional.of(obs); + return this; + } + private static Collector transferCollector( BinaryOperator> reducer ) { @@ -175,7 +183,8 @@ public static Function tinyBarsFromTo( }; } - public static Function tinyBarsFromToWithInvalidAmounts(String from, String to, long amount) { + public static Function tinyBarsFromToWithInvalidAmounts(String from, String to, + long amount) { return tinyBarsFromToWithInvalidAmounts(from, to, ignore -> amount); } @@ -258,6 +267,14 @@ private void misconfigureIfRequested(CryptoTransferTransactionBody.Builder b, Ha @Override protected long feeFor(HapiApiSpec spec, Transaction txn, int numPayerKeys) throws Throwable { + if (feesObserver.isPresent()) { + return spec.fees().forActivityBasedOpWithDetails( + HederaFunctionality.CryptoTransfer, + (_txn, _svo) -> usageEstimate(_txn, _svo, spec.fees().tokenTransferUsageMultiplier()), + txn, + numPayerKeys, + feesObserver.get()); + } return spec.fees().forActivityBasedOp( HederaFunctionality.CryptoTransfer, (_txn, _svo) -> usageEstimate(_txn, _svo, spec.fees().tokenTransferUsageMultiplier()), @@ -293,7 +310,8 @@ protected MoreObjects.ToStringHelper toStringHelper() { helper.add( "tokenTransfers", TxnUtils.readableTokenTransfers(txn.getCryptoTransfer().getTokenTransfersList())); - } catch (Exception ignore) { } + } catch (Exception ignore) { + } } return helper; } diff --git a/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java b/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java index 197c8e2fe46d..07c3d0445dcf 100644 --- a/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java +++ b/test-clients/src/main/java/com/hedera/services/bdd/suites/records/RecordCreationSuite.java @@ -24,29 +24,30 @@ import com.hedera.services.bdd.spec.HapiApiSpec; import com.hedera.services.bdd.spec.HapiSpecSetup; import com.hedera.services.bdd.spec.infrastructure.meta.ContractResources; -import com.hedera.services.bdd.spec.persistence.Account; -import com.hedera.services.bdd.spec.utilops.CustomSpecAssert; -import com.hedera.services.bdd.spec.utilops.UtilVerbs; import com.hedera.services.bdd.suites.HapiApiSuite; import com.hederahashgraph.api.proto.java.AccountAmount; import com.hederahashgraph.api.proto.java.AccountID; import com.hederahashgraph.api.proto.java.ServicesConfigurationList; import com.hederahashgraph.api.proto.java.Setting; import com.hederahashgraph.api.proto.java.TransactionRecord; +import com.hederahashgraph.fee.FeeObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.junit.Assert; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; import static com.hedera.services.bdd.spec.HapiApiSpec.defaultHapiSpec; +import static com.hedera.services.bdd.spec.assertions.AccountInfoAsserts.changeFromSnapshot; import static com.hedera.services.bdd.spec.assertions.AssertUtils.inOrder; import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith; +import static com.hedera.services.bdd.spec.assertions.TransferListAsserts.includingDeduction; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountRecords; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getContractRecords; import static com.hedera.services.bdd.spec.queries.QueryVerbs.getFileContents; +import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTxnRecord; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCall; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.contractCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.createTopic; @@ -55,16 +56,22 @@ import static com.hedera.services.bdd.spec.transactions.TxnVerbs.fileCreate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.fileUpdate; import static com.hedera.services.bdd.spec.transactions.TxnVerbs.submitMessageTo; +import static com.hedera.services.bdd.spec.transactions.TxnVerbs.uncheckedSubmit; import static com.hedera.services.bdd.spec.transactions.crypto.HapiCryptoTransfer.tinyBarsFromTo; import static com.hedera.services.bdd.spec.utilops.CustomSpecAssert.allRunFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.assertionsHold; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.balanceSnapshot; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sleepFor; import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing; +import static com.hedera.services.bdd.spec.utilops.UtilVerbs.usableTxnIdNamed; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE; +import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_ZERO_BYTE_IN_STRING; import static org.junit.Assert.assertEquals; public class RecordCreationSuite extends HapiApiSuite { private static final Logger log = LogManager.getLogger(RecordCreationSuite.class); + private static final long SLEEP_MS = 1_000L; private static final String defaultRecordsTtl = HapiSpecSetup.getDefaultNodeProps().get("cache.records.ttl"); public static void main(String... args) { @@ -80,6 +87,9 @@ protected List getSpecsInSuite() { accountsGetPayerRecordsIfSoConfigured(), calledContractNoLongerGetsRecord(), thresholdRecordsDontExistAnymore(), + submittingNodeChargedNetworkFeeForLackOfDueDiligence(), + submittingNodeChargedNetworkFeeForIgnoringPayerUnwillingness(), + submittingNodeStillPaidIfServiceFeesOmitted(), /* This last spec requires sleeping for the default TTL (180s) so that the expiration queue will be purged of all entries for existing records. @@ -94,6 +104,158 @@ changes to record expiration. */ ); } + private HapiApiSpec submittingNodeStillPaidIfServiceFeesOmitted() { + final String comfortingMemo = "This is ok, it's fine, it's whatever."; + final AtomicReference feeObs = new AtomicReference<>(); + + return defaultHapiSpec("submittingNodeStillPaidIfServiceFeesOmitted") + .given( + cryptoTransfer(tinyBarsFromTo(GENESIS, "0.0.3", ONE_HBAR)) + .payingWith(GENESIS), + cryptoCreate("payer"), + cryptoTransfer( + tinyBarsFromTo(GENESIS, FUNDING, 1L) + ) + .memo(comfortingMemo) + .exposingFeesTo(feeObs) + .payingWith("payer") + ).when( + balanceSnapshot("before", "0.0.3"), + balanceSnapshot("fundingBefore", "0.0.98"), + sourcing(() -> + cryptoTransfer( + tinyBarsFromTo(GENESIS, FUNDING, 1L) + ) + .memo(comfortingMemo) + .fee(feeObs.get().getNetworkFee() + feeObs.get().getNodeFee()) + .payingWith("payer") + .via("txnId") + .hasKnownStatus(INSUFFICIENT_TX_FEE) + ) + ).then( + sourcing(() -> + getAccountBalance("0.0.3") + .hasTinyBars( + changeFromSnapshot("before", +feeObs.get().getNodeFee()))), + sourcing(() -> + getAccountBalance("0.0.98") + .hasTinyBars( + changeFromSnapshot("fundingBefore", +feeObs.get().getNetworkFee()))), + sourcing(() -> + getTxnRecord("txnId") + .assertingNothingAboutHashes() + .hasPriority(recordWith() + .transfers(includingDeduction( + "payer", + feeObs.get().getNetworkFee() + feeObs.get().getNodeFee())) + .status(INSUFFICIENT_TX_FEE)) + .logged()) + ); + } + + private HapiApiSpec submittingNodeChargedNetworkFeeForLackOfDueDiligence() { + final String comfortingMemo = "This is ok, it's fine, it's whatever."; + final String disquietingMemo = "\u0000his is ok, it's fine, it's whatever."; + final AtomicReference feeObs = new AtomicReference<>(); + + return defaultHapiSpec("SubmittingNodeChargedNetworkFeeForLackOfDueDiligence") + .given( + cryptoTransfer(tinyBarsFromTo(GENESIS, "0.0.3", ONE_HBAR)) + .payingWith(GENESIS), + cryptoCreate("payer"), + cryptoTransfer( + tinyBarsFromTo(GENESIS, FUNDING, 1L) + ) + .memo(comfortingMemo) + .exposingFeesTo(feeObs) + .payingWith("payer"), + usableTxnIdNamed("txnId") + .payerId("payer") + ).when( + balanceSnapshot("before", "0.0.3"), + balanceSnapshot("fundingBefore", "0.0.98"), + uncheckedSubmit( + cryptoTransfer( + tinyBarsFromTo(GENESIS, FUNDING, 1L) + ) + .memo(disquietingMemo) + .payingWith("payer") + .txnId("txnId") + ) + .payingWith(GENESIS), + sleepFor(SLEEP_MS) + ).then( + sourcing(() -> + getAccountBalance("0.0.3") + .hasTinyBars( + changeFromSnapshot("before", -feeObs.get().getNetworkFee()))), + sourcing(() -> + getAccountBalance("0.0.98") + .hasTinyBars( + changeFromSnapshot("fundingBefore", +feeObs.get().getNetworkFee()))), + sourcing(() -> + getTxnRecord("txnId") + .assertingNothingAboutHashes() + .hasPriority(recordWith() + .transfers(includingDeduction(() -> 3L, feeObs.get().getNetworkFee())) + .status(INVALID_ZERO_BYTE_IN_STRING)) + .logged()) + ); + } + + private HapiApiSpec submittingNodeChargedNetworkFeeForIgnoringPayerUnwillingness() { + final String comfortingMemo = "This is ok, it's fine, it's whatever."; + final AtomicReference feeObs = new AtomicReference<>(); + + return defaultHapiSpec("SubmittingNodeChargedNetworkFeeForIgnoringPayerUnwillingness") + .given( + cryptoTransfer(tinyBarsFromTo(GENESIS, "0.0.3", ONE_HBAR)) + .payingWith(GENESIS), + cryptoCreate("payer"), + cryptoTransfer( + tinyBarsFromTo(GENESIS, FUNDING, 1L) + ) + .memo(comfortingMemo) + .exposingFeesTo(feeObs) + .payingWith("payer"), + usableTxnIdNamed("txnId") + .payerId("payer") + ).when( + balanceSnapshot("before", "0.0.3"), + balanceSnapshot("fundingBefore", "0.0.98"), + sourcing(() -> + uncheckedSubmit( + cryptoTransfer( + tinyBarsFromTo(GENESIS, FUNDING, 1L) + ) + .memo(comfortingMemo) + .fee(feeObs.get().getNetworkFee() - 1L) + .payingWith("payer") + .txnId("txnId") + ) + .payingWith(GENESIS) + ), + sleepFor(SLEEP_MS) + ).then( + sourcing(() -> + getAccountBalance("0.0.3") + .hasTinyBars( + changeFromSnapshot("before", -feeObs.get().getNetworkFee()))), + sourcing(() -> + getAccountBalance("0.0.98") + .hasTinyBars( + changeFromSnapshot("fundingBefore", +feeObs.get().getNetworkFee()))), + sourcing(() -> + getTxnRecord("txnId") + .assertingNothingAboutHashes() + .hasPriority(recordWith() + .transfers(includingDeduction(() -> 3L, feeObs.get().getNetworkFee())) + .status(INSUFFICIENT_TX_FEE)) + .logged()) + ); + } + + private HapiApiSpec payerRecordCreationSanityChecks() { return defaultHapiSpec("PayerRecordCreationSanityChecks") .given(