Skip to content

Commit

Permalink
Charge auto-association fees "inline"
Browse files Browse the repository at this point in the history
Signed-off-by: Michael Tinker <[email protected]>
  • Loading branch information
tinker-michaelj committed Jul 17, 2024
1 parent 2b65890 commit 3433522
Show file tree
Hide file tree
Showing 12 changed files with 86 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ enum PrecedingTransactionCategory {
@NonNull
AccountID payer();

/**
* Attempts to charge the payer in this context the given amount.
* @param amount the amount to charge
* @throws IllegalArgumentException if the payer cannot afford the fee
*/
void chargePayerFee(long amount);

/**
* Returns the current {@link Configuration} for the node.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL;
import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE;
import static com.hedera.hapi.node.base.HederaFunctionality.TOKEN_ASSOCIATE_TO_ACCOUNT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS;
import static com.hedera.hapi.util.HapiUtils.functionOf;
import static java.util.Objects.requireNonNull;
Expand Down Expand Up @@ -71,6 +72,7 @@ public boolean hasThrottleCapacityForChildTransactions() {
final var childRecords = recordListBuilder.childRecordBuilders();
@Nullable List<ThrottleUsageSnapshot> snapshotsIfNeeded = null;

int numAutoAssociations = 0;
for (int i = 0, n = childRecords.size(); i < n && isAllowed; i++) {
final var childRecord = childRecords.get(i);
if (Objects.equals(childRecord.status(), SUCCESS)) {
Expand Down Expand Up @@ -101,8 +103,13 @@ public boolean hasThrottleCapacityForChildTransactions() {
if (shouldThrottleTxn) {
isAllowed = false;
}
numAutoAssociations += childRecord.getNumAutoAssociations();
}
}
if (isAllowed && numAutoAssociations > 0) {
isAllowed = networkUtilizationManager.shouldThrottleNOfUnscaled(
numAutoAssociations, TOKEN_ASSOCIATE_TO_ACCOUNT, consensusNow);
}
if (!isAllowed) {
networkUtilizationManager.resetUsageThrottlesTo(snapshotsIfNeeded);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import com.hedera.hapi.util.UnknownHederaFunctionality;
import com.hedera.node.app.fees.ChildFeeContextImpl;
import com.hedera.node.app.fees.ExchangeRateManager;
import com.hedera.node.app.fees.FeeAccumulator;
import com.hedera.node.app.fees.FeeManager;
import com.hedera.node.app.records.BlockRecordManager;
import com.hedera.node.app.records.RecordBuildersImpl;
Expand Down Expand Up @@ -112,6 +113,7 @@ public class DispatchHandleContext implements HandleContext, FeeContext {
private final DispatchProcessor dispatchProcessor;
private final RecordListBuilder recordListBuilder;
private final ThrottleAdviser throttleAdviser;
private final FeeAccumulator feeAccumulator;
private Map<AccountID, Long> dispatchPaidRewards;

public DispatchHandleContext(
Expand Down Expand Up @@ -139,7 +141,8 @@ public DispatchHandleContext(
@NonNull final ChildDispatchFactory childDispatchLogic,
@NonNull final DispatchProcessor dispatchProcessor,
@NonNull final RecordListBuilder recordListBuilder,
@NonNull final ThrottleAdviser throttleAdviser) {
@NonNull final ThrottleAdviser throttleAdviser,
@NonNull final FeeAccumulator feeAccumulator) {
this.consensusNow = requireNonNull(consensusNow);
this.creatorInfo = requireNonNull(creatorInfo);
this.txnInfo = requireNonNull(transactionInfo);
Expand All @@ -161,6 +164,7 @@ public DispatchHandleContext(
this.dispatchProcessor = requireNonNull(dispatchProcessor);
this.recordListBuilder = requireNonNull(recordListBuilder);
this.throttleAdviser = requireNonNull(throttleAdviser);
this.feeAccumulator = requireNonNull(feeAccumulator);
this.attributeValidator = new AttributeValidatorImpl(this);
this.expiryValidator = new ExpiryValidatorImpl(this);
this.dispatcher = requireNonNull(dispatcher);
Expand All @@ -187,6 +191,11 @@ public AccountID payer() {
return syntheticPayer;
}

@Override
public void chargePayerFee(final long amount) {
feeAccumulator.chargeNetworkFee(syntheticPayer, amount);
}

@NonNull
@Override
public Configuration configuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ private RecordDispatch newChildDispatch(
final var writableStoreFactory = new WritableStoreFactory(
stack, serviceScopeLookup.getServiceName(txnInfo.txBody()), config, storeMetricsService);
final var serviceApiFactory = new ServiceApiFactory(stack, config, storeMetricsService);
final var feeAccumulator = new FeeAccumulator(serviceApiFactory.getApi(TokenServiceApi.class), recordBuilder);
final var dispatchHandleContext = new DispatchHandleContext(
consensusNow,
creatorInfo,
Expand Down Expand Up @@ -260,15 +261,16 @@ private RecordDispatch newChildDispatch(
this,
dispatchProcessor,
recordListBuilder,
throttleAdviser);
throttleAdviser,
feeAccumulator);
return new RecordDispatch(
recordBuilder,
config,
feesFrom(dispatchHandleContext, category, dispatcher, topLevelFunction, txnInfo),
txnInfo,
payerId,
readableStoreFactory,
new FeeAccumulator(serviceApiFactory.getApi(TokenServiceApi.class), recordBuilder),
feeAccumulator,
keyVerifier,
creatorInfo,
consensusNow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,15 @@ public SingleTransactionRecordBuilderImpl addAutomaticTokenAssociation(
return this;
}

/**
* Returns the number of automatic token associations
*
* @return the number of associations
*/
public int getNumAutoAssociations() {
return automaticTokenAssociations.size();
}

/**
* Sets the alias.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ public Dispatch dispatch(
final var writableStoreFactory = new WritableStoreFactory(
stack, serviceScopeLookup.getServiceName(txnInfo.txBody()), config, storeMetricsService);
final var serviceApiFactory = new ServiceApiFactory(stack, config, storeMetricsService);

final var feeAccumulator = new FeeAccumulator(serviceApiFactory.getApi(TokenServiceApi.class), recordBuilder);
final var dispatchHandleContext = new DispatchHandleContext(
consensusNow,
creatorInfo,
Expand Down Expand Up @@ -177,15 +177,16 @@ public Dispatch dispatch(
childDispatchFactory,
dispatchProcessor,
recordListBuilder,
new AppThrottleAdviser(networkUtilizationManager, consensusNow, recordListBuilder, stack));
new AppThrottleAdviser(networkUtilizationManager, consensusNow, recordListBuilder, stack),
feeAccumulator);
return new RecordDispatch(
recordBuilder,
config,
dispatcher.dispatchComputeFees(dispatchHandleContext),
txnInfo,
requireNonNull(txnInfo.payerID()),
readableStoreFactory,
new FeeAccumulator(serviceApiFactory.getApi(TokenServiceApi.class), recordBuilder),
feeAccumulator,
keyVerifier,
creatorInfo,
consensusNow,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
import com.hedera.hapi.util.UnknownHederaFunctionality;
import com.hedera.node.app.fees.ChildFeeContextImpl;
import com.hedera.node.app.fees.ExchangeRateManager;
import com.hedera.node.app.fees.FeeAccumulator;
import com.hedera.node.app.fees.FeeManager;
import com.hedera.node.app.ids.EntityIdService;
import com.hedera.node.app.records.BlockRecordManager;
Expand Down Expand Up @@ -172,6 +173,9 @@ public class DispatchHandleContextTest extends StateTestBase implements Scenario
@Mock
private AppKeyVerifier verifier;

@Mock
private FeeAccumulator feeAccumulator;

@Mock
private NetworkInfo networkInfo;

Expand Down Expand Up @@ -393,7 +397,8 @@ void testConstructorWithInvalidArguments() {
childDispatchFactory,
dispatchProcessor,
recordListBuilder,
throttleAdviser
throttleAdviser,
feeAccumulator
};

final var constructor = DispatchHandleContext.class.getConstructors()[0];
Expand Down Expand Up @@ -782,7 +787,8 @@ private DispatchHandleContext createContext(
childDispatchFactory,
dispatchProcessor,
recordListBuilder,
throttleAdviser);
throttleAdviser,
feeAccumulator);
}

private void mockNeeded() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_FROZEN_FOR_TOKEN;
import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_KYC_NOT_GRANTED_FOR_TOKEN;
import static com.hedera.hapi.node.base.ResponseCodeEnum.AMOUNT_EXCEEDS_ALLOWANCE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_NFT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.NO_REMAINING_AUTOMATIC_ASSOCIATIONS;
Expand Down Expand Up @@ -46,9 +47,9 @@
import com.hedera.node.app.service.token.impl.WritableTokenRelationStore;
import com.hedera.node.app.service.token.impl.WritableTokenStore;
import com.hedera.node.app.service.token.impl.handlers.BaseTokenHandler;
import com.hedera.node.app.spi.workflows.ComputeDispatchFeesAsTopLevel;
import com.hedera.node.app.spi.workflows.HandleContext;
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.node.app.spi.workflows.record.SingleTransactionRecordBuilder;
import com.hedera.node.config.data.EntitiesConfig;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
Expand All @@ -59,6 +60,18 @@
* They are auto-associated only if there are open auto-associations available on the account.
*/
public class AssociateTokenRecipientsStep extends BaseTokenHandler implements TransferStep {
/**
* With unlimited associations enabled, the fee computed by TokenAssociateToAccountHandler depends
* only on the number of tokens associated and nothing else; so a placeholder transaction body works
* fine for us when calling dispatchComputeFees()
*/
private static final TransactionBody PLACEHOLDER_SYNTHETIC_ASSOCIATION = TransactionBody.newBuilder()
.tokenAssociate(TokenAssociateTransactionBody.newBuilder()
.account(AccountID.DEFAULT)
.tokens(TokenID.DEFAULT)
.build())
.build();

private final CryptoTransferTransactionBody op;

/**
Expand Down Expand Up @@ -184,62 +197,27 @@ private TokenAssociation validateAndBuildAutoAssociation(
final var unlimitedAssociationsEnabled =
config.getConfigData(EntitiesConfig.class).unlimitedAutoAssociationsEnabled();
if (unlimitedAssociationsEnabled) {
dispatchAutoAssociation(token, accountStore, tokenRelStore, context, account);
// We still need to return this association to the caller. Since this is used to set in record automatic
// associations.
return asTokenAssociation(tokenId, accountId);
} else {
// Once the unlimitedAssociationsEnabled is enabled, this block of code can be removed and
// all auto-associations will be done through the synthetic transaction and charged.
final var newRelation = autoAssociate(account, token, accountStore, tokenRelStore, config);
return asTokenAssociation(newRelation.tokenId(), newRelation.accountId());
final var autoAssociationFee = associationFeeFor(context, PLACEHOLDER_SYNTHETIC_ASSOCIATION);
try {
context.chargePayerFee(autoAssociationFee);
} catch (IllegalArgumentException ignore) {
throw new HandleException(INSUFFICIENT_PAYER_BALANCE);
}
}
final var newRelation = autoAssociate(account, token, accountStore, tokenRelStore, config);
return asTokenAssociation(newRelation.tokenId(), newRelation.accountId());
} else {
validateTrue(tokenRel != null, TOKEN_NOT_ASSOCIATED_TO_ACCOUNT);
validateFalse(tokenRel.frozen(), ACCOUNT_FROZEN_FOR_TOKEN);
return null;
}
}

/**
* Dispatches a synthetic transaction to associate the token with the account. It will increment the usedAutoAssociations
* count on the account and set the token relation as automaticAssociation.
* This is done only if the unlimitedAutoAssociationsEnabled is enabled.
* @param token The token to associate with the account
* @param accountStore The account store
* @param tokenRelStore The token relation store
* @param context The context
* @param account The account to associate the token with
*/
private void dispatchAutoAssociation(
final @NonNull Token token,
final @NonNull WritableAccountStore accountStore,
final @NonNull WritableTokenRelationStore tokenRelStore,
final @NonNull HandleContext context,
final @NonNull Account account) {
final var accountId = account.accountIdOrThrow();
final var tokenId = token.tokenIdOrThrow();
final var syntheticAssociation = TransactionBody.newBuilder()
.tokenAssociate(TokenAssociateTransactionBody.newBuilder()
.account(account.accountId())
.tokens(token.tokenId())
.build())
.build();
// We don't need to verify signatures for this internal dispatch. So we specify the keyVerifier to null
context.dispatchRemovablePrecedingTransaction(
syntheticAssociation, SingleTransactionRecordBuilder.class, null, context.payer());
// increment the usedAutoAssociations count
final var accountModified = requireNonNull(accountStore.getAliasedAccountById(accountId))
.copyBuilder()
.usedAutoAssociations(account.usedAutoAssociations() + 1)
.build();
accountStore.put(accountModified);
// We need to set this as auto-association
final var newTokenRel = requireNonNull(tokenRelStore.get(accountId, tokenId))
.copyBuilder()
.automaticAssociation(true)
.build();
tokenRelStore.put(newTokenRel);
private long associationFeeFor(@NonNull final HandleContext context, @NonNull final TransactionBody txnBody) {
final var topLevelPayer = context.payer();
// If we get here the payer must exist
final var fees = context.dispatchComputeFees(txnBody, topLevelPayer, ComputeDispatchFeesAsTopLevel.NO);
return fees.serviceFee() + fees.networkFee() + fees.nodeFee();
}

private void validateFungibleAllowance(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ void doesTokenBalanceChangesWithoutAllowances() {
assertThat(senderAccountBefore.numberPositiveBalances()).isEqualTo(2);
assertThat(receiverAccountBefore.numberPositiveBalances()).isEqualTo(2);
assertThat(senderRelBefore.balance()).isEqualTo(1000L);
assertThat(receiverRelBefore.balance()).isEqualTo(1);

adjustFungibleTokenChangesStep.doIn(transferContext);

Expand All @@ -110,10 +109,8 @@ void doesTokenBalanceChangesWithoutAllowances() {
// numPositiveBalancesChanged since all 1000 token Rel balance is transferred and new balance is 0
assertThat(senderAccountAfter.numberPositiveBalances())
.isEqualTo(senderAccountBefore.numberPositiveBalances() - 1);
// It is not changed since the original balance before the method is called is > 0.
// So no need to increment the positive balances
assertThat(receiverAccountAfter.numberPositiveBalances())
.isEqualTo(receiverAccountBefore.numberPositiveBalances());
.isEqualTo(receiverAccountBefore.numberPositiveBalances() + 1);
assertThat(senderRelAfter.balance()).isEqualTo(senderRelBefore.balance() - 1000);
assertThat(receiverRelAfter.balance()).isEqualTo(receiverRelBefore.balance() + 1000);
}
Expand Down Expand Up @@ -146,7 +143,7 @@ void doesTokenBalanceChangesWithAllowances() {
assertThat(receiverAccountBefore.numberPositiveBalances()).isEqualTo(2);
assertThat(senderRelBefore.balance()).isEqualTo(1000L);
// There is an association happening during the transfer for auto creation
assertThat(receiverRelBefore.balance()).isEqualTo(1);
assertThat(receiverRelBefore.balance()).isEqualTo(0);

assertThat(senderAccountBefore.tokenAllowances()).hasSize(1);

Expand All @@ -161,10 +158,8 @@ void doesTokenBalanceChangesWithAllowances() {
// numPositiveBalancesChanged since all 1000 token Rel balance is transferred and new balance is 0
assertThat(senderAccountAfter.numberPositiveBalances())
.isEqualTo(senderAccountBefore.numberPositiveBalances() - 1);
// It is not changed since the original balance before the method is called is > 0.
// So no need to increment the positive balances
assertThat(receiverAccountAfter.numberPositiveBalances())
.isEqualTo(receiverAccountBefore.numberPositiveBalances());
.isEqualTo(receiverAccountBefore.numberPositiveBalances() + 1);
assertThat(senderRelAfter.balance()).isEqualTo(senderRelBefore.balance() - 1000);
assertThat(receiverRelAfter.balance()).isEqualTo(receiverRelBefore.balance() + 1000);

Expand Down
Loading

0 comments on commit 3433522

Please sign in to comment.