All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.hedera.node.app.service.token.impl.handlers.CryptoUpdateHandler Maven / Gradle / Ivy

There is a newer version: 0.57.3
Show newest version
/*
 * Copyright (C) 2022-2024 Hedera Hashgraph, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hedera.node.app.service.token.impl.handlers;

import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_EXPIRED_AND_PENDING_REMOVAL;
import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST;
import static com.hedera.hapi.node.base.ResponseCodeEnum.EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ADMIN_KEY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_MAX_AUTO_ASSOCIATIONS;
import static com.hedera.hapi.node.base.ResponseCodeEnum.PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT;
import static com.hedera.node.app.hapi.fees.pricing.BaseOperationUsage.THREE_MONTHS_IN_SECONDS;
import static com.hedera.node.app.hapi.fees.usage.SingletonEstimatorUtils.ESTIMATOR_UTILS;
import static com.hedera.node.app.hapi.fees.usage.crypto.CryptoOpsUsage.UPDATE_SLOT_MULTIPLIER;
import static com.hedera.node.app.hapi.fees.usage.crypto.entities.CryptoEntitySizes.CRYPTO_ENTITY_SIZES;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.BASIC_ENTITY_ID_SIZE;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.INT_SIZE;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.LONG_SIZE;
import static com.hedera.node.app.hapi.utils.fee.FeeBuilder.getAccountKeyStorageSize;
import static com.hedera.node.app.service.token.api.AccountSummariesApi.SENTINEL_ACCOUNT_ID;
import static com.hedera.node.app.service.token.api.AccountSummariesApi.SENTINEL_NODE_ID;
import static com.hedera.node.app.spi.validation.AttributeValidator.isImmutableKey;
import static com.hedera.node.app.spi.validation.ExpiryMeta.NA;
import static com.hedera.node.app.spi.workflows.HandleException.validateFalse;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
import static com.hedera.node.app.spi.workflows.PreCheckException.validateFalsePreCheck;
import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.HederaFunctionality;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.SubType;
import com.hedera.hapi.node.base.Timestamp;
import com.hedera.hapi.node.state.token.Account;
import com.hedera.hapi.node.token.CryptoUpdateTransactionBody;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.hapi.utils.CommonPbjConverters;
import com.hedera.node.app.service.token.CryptoSignatureWaivers;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.service.token.impl.WritableAccountStore;
import com.hedera.node.app.service.token.impl.validators.StakingValidator;
import com.hedera.node.app.spi.fees.FeeCalculator;
import com.hedera.node.app.spi.fees.FeeContext;
import com.hedera.node.app.spi.fees.Fees;
import com.hedera.node.app.spi.validation.EntityType;
import com.hedera.node.app.spi.validation.ExpiryMeta;
import com.hedera.node.app.spi.workflows.HandleContext;
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.node.app.spi.workflows.PreCheckException;
import com.hedera.node.app.spi.workflows.PreHandleContext;
import com.hedera.node.app.spi.workflows.TransactionHandler;
import com.hedera.node.config.data.AutoRenewConfig;
import com.hedera.node.config.data.EntitiesConfig;
import com.hedera.node.config.data.LedgerConfig;
import com.hedera.node.config.data.StakingConfig;
import com.hedera.node.config.data.TokensConfig;
import com.swirlds.config.api.Configuration;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.nio.charset.StandardCharsets;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding {@link HederaFunctionality#CRYPTO_UPDATE}.
 */
@Singleton
public class CryptoUpdateHandler extends BaseCryptoHandler implements TransactionHandler {
    private final CryptoSignatureWaivers waivers;
    private final StakingValidator stakingValidator;

    /**
     * Default constructor for injection.
     * @param waivers the {@link CryptoSignatureWaivers} to use for checking signature waivers
     * @param stakingValidator the {@link StakingValidator} to use for staking validation
     */
    @Inject
    public CryptoUpdateHandler(
            @NonNull final CryptoSignatureWaivers waivers, @NonNull final StakingValidator stakingValidator) {
        this.waivers = requireNonNull(waivers, "The supplied argument 'waivers' must not be null");
        this.stakingValidator =
                requireNonNull(stakingValidator, "The supplied argument 'stakingValidator' must not be null");
    }

    @Override
    public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException {
        final var op = txn.cryptoUpdateAccountOrThrow();
        validateTruePreCheck(op.hasAccountIDToUpdate(), ACCOUNT_ID_DOES_NOT_EXIST);
        validateFalsePreCheck(
                op.hasProxyAccountID() && !op.proxyAccountID().equals(AccountID.DEFAULT),
                PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED);
    }

    @Override
    public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {
        requireNonNull(context);
        requireNonNull(waivers);
        final var txn = context.body();
        pureChecks(txn);

        final var payer = context.payer();
        final var op = txn.cryptoUpdateAccountOrThrow();

        // update account must exist. Validated in pureChecks
        final var updateAccountId = op.accountIDToUpdateOrThrow();

        // 1. When updating the treasury account 0.0.2 key, the target account must also sign the transaction
        // 2. When updating treasury account 0.0.2, even if system admin is the payer for the transaction,
        // the target account must sign the transaction
        // 3. If the payer for the transaction is 0.0.2 or system admin, then no signatures are needed for the update
        final var targetAccountKeyMustSign = !waivers.isTargetAccountSignatureWaived(txn, payer);

        // 4. Including above 3 conditions, if the target account is 0.0.2, new key must sign the transaction
        final var newAccountKeyMustSign = !waivers.isNewKeySignatureWaived(txn, payer);
        if (targetAccountKeyMustSign) {
            context.requireKeyOrThrow(updateAccountId, INVALID_ACCOUNT_ID);
        }
        if (newAccountKeyMustSign && op.hasKey()) {
            context.requireKeyOrThrow(op.key(), INVALID_ADMIN_KEY);
        }
    }

    /**
     * This method is called during the handle workflow. It executes the actual transaction.
     * @param context the {@link HandleContext} which collects all information
     * @throws HandleException if any of the checks fail
     * @throws NullPointerException if any of the arguments are null
     */
    @Override
    public void handle(@NonNull final HandleContext context) {
        final var txn = requireNonNull(context).body();
        final var op = txn.cryptoUpdateAccountOrThrow();
        final var target = op.accountIDToUpdateOrThrow();

        // validate update account exists
        final var accountStore = context.storeFactory().writableStore(WritableAccountStore.class);
        final var targetAccount = accountStore.get(target);
        validateTrue(targetAccount != null, INVALID_ACCOUNT_ID);
        context.attributeValidator().validateMemo(op.memo());

        // Customize the account based on fields set in transaction body
        final var builder = updateBuilder(op, targetAccount);

        // validate all checks that involve config and state
        validateSemantics(context, targetAccount, op, builder, accountStore);

        // If an account is detached and expired, validateSemantics would have thrown an exception
        // To reach here, this update transaction has extended the expiration of the account.
        // So, we need to set the expiredAndPendingRemoval flag to false
        if (targetAccount.expiredAndPendingRemoval()) {
            builder.expiredAndPendingRemoval(false);
        }

        // Add account to the modifications in state
        accountStore.put(builder.build());
    }

    /**
     * Add a builder from {@link CryptoUpdateTransactionBody} to create {@link Account.Builder} object
     * @param op Crypto update transaction body
     * @return builder
     */
    private Account.Builder updateBuilder(
            @NonNull final CryptoUpdateTransactionBody op, @NonNull final Account currentAccount) {
        final var builder = currentAccount.copyBuilder();
        if (op.hasKey()) {
            /* Note that {@code this.validateSemantics} will have rejected any txn with an invalid key. */
            builder.key(op.key());
        }
        if (op.hasExpirationTime()) {
            builder.expirationSecond(op.expirationTime().seconds());
        }
        if (op.hasReceiverSigRequiredWrapper()) {
            builder.receiverSigRequired(op.receiverSigRequiredWrapper().booleanValue());
        } else if (op.hasReceiverSigRequired()) {
            builder.receiverSigRequired(op.receiverSigRequired());
        }
        if (op.hasAutoRenewPeriod()) {
            builder.autoRenewSeconds(op.autoRenewPeriod().seconds());
        }
        if (op.hasMemo()) {
            builder.memo(op.memo());
        }
        if (op.hasMaxAutomaticTokenAssociations()) {
            builder.maxAutoAssociations(op.maxAutomaticTokenAssociations());
        }
        if (op.hasDeclineReward()) {
            builder.declineReward(op.declineReward().booleanValue());
        }
        if (op.hasStakedAccountId()) {
            // 0.0.0 is used a sentinel value for removing staked account id
            // Once https://github.com/hashgraph/pbj/issues/160 this is closed, reset stakedId to UNSET
            if (SENTINEL_ACCOUNT_ID.equals(op.stakedAccountId())) {
                builder.stakedAccountId((AccountID) null);
            } else {
                builder.stakedAccountId(op.stakedAccountId());
            }
        } else if (op.hasStakedNodeId()) {
            // -1 is used a sentinel value for removing staked node id
            // Once https://github.com/hashgraph/pbj/issues/160 this is closed, reset stakedId to UNSET
            if (SENTINEL_NODE_ID == op.stakedNodeId()) {
                builder.stakedNodeId(SENTINEL_NODE_ID);
            } else {
                builder.stakedNodeId(op.stakedNodeId());
            }
        }
        return builder;
    }

    /**
     * Validate semantics of the transaction. This method is called during the handle workflow.
     * It validates any fields of the transaction that involves state or config.
     * @param context handle context
     * @param updateAccount account to be updated
     * @param op crypto update transaction body
     * @param builder account update builder
     * @param accountStore account store
     */
    private void validateSemantics(
            @NonNull final HandleContext context,
            @NonNull final Account updateAccount,
            @NonNull final CryptoUpdateTransactionBody op,
            @NonNull Account.Builder builder,
            @NonNull final ReadableAccountStore accountStore) {
        final var expiryValidator = context.expiryValidator();
        final var builderAccount = builder.build();
        validateFields(op, context, accountStore);

        validateTrue(!updateAccount.smartContract(), INVALID_ACCOUNT_ID);

        // get needed configs for validation
        final var tokensConfig = context.configuration().getConfigData(TokensConfig.class);
        final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class);
        final var entitiesConfig = context.configuration().getConfigData(EntitiesConfig.class);

        // validate expiry metadata
        final var currentMetadata = new ExpiryMeta(
                updateAccount.expirationSecond(), updateAccount.autoRenewSeconds(), updateAccount.autoRenewAccountId());
        final var updateMeta = new ExpiryMeta(
                op.hasExpirationTime() ? op.expirationTime().seconds() : NA,
                op.hasAutoRenewPeriod() ? op.autoRenewPeriod().seconds() : NA,
                null);
        context.expiryValidator().resolveUpdateAttempt(currentMetadata, updateMeta, false);

        // If an account is detached and pending removal, it cannot be updated
        // It can only be updated to extend expiration time
        if (expiryValidator.isDetached(
                EntityType.ACCOUNT, updateAccount.expiredAndPendingRemoval(), updateAccount.tinybarBalance())) {
            validateTrue(builderAccount.expirationSecond() != 0, ACCOUNT_EXPIRED_AND_PENDING_REMOVAL);
        }

        // validate auto associations
        if (op.hasMaxAutomaticTokenAssociations()) {
            final long newMax = builderAccount.maxAutoAssociations();
            // first validate if the associations are limited and the new max is within
            validateFalse(
                    entitiesConfig.limitTokenAssociations() && newMax > tokensConfig.maxPerAccount(),
                    REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT);
            validateFalse(
                    newMax < -1 && entitiesConfig.unlimitedAutoAssociationsEnabled()
                            || newMax < 0 && !entitiesConfig.unlimitedAutoAssociationsEnabled(),
                    INVALID_MAX_AUTO_ASSOCIATIONS);
            validateFalse(
                    newMax < updateAccount.usedAutoAssociations() && newMax != -1,
                    EXISTING_AUTOMATIC_ASSOCIATIONS_EXCEED_GIVEN_LIMIT);
            validateFalse(
                    newMax > ledgerConfig.maxAutoAssociations(),
                    REQUESTED_NUM_AUTOMATIC_ASSOCIATIONS_EXCEEDS_ASSOCIATION_LIMIT);
        }

        // validate if account is not deleted
        validateFalse(updateAccount.deleted(), ACCOUNT_DELETED);
    }

    /**
     * Validate basic fields of the transaction that involves state or config.
     * @param op crypto update transaction body
     * @param context handle context
     * @param accountStore account store
     */
    private void validateFields(
            @NonNull final CryptoUpdateTransactionBody op,
            @NonNull final HandleContext context,
            @NonNull final ReadableAccountStore accountStore) {
        if (op.hasMemo()) {
            context.attributeValidator().validateMemo(op.memo());
        }
        // Empty key list is allowed and is used for immutable entities (e.g. system accounts)
        if (op.hasKey() && !isImmutableKey(op.key())) {
            context.attributeValidator().validateKey(op.key());
        }

        if (op.hasAutoRenewPeriod()) {
            context.attributeValidator()
                    .validateAutoRenewPeriod(op.autoRenewPeriod().seconds());
        }

        StakingValidator.validateStakedIdForUpdate(
                context.configuration().getConfigData(StakingConfig.class).isEnabled(),
                op.hasDeclineReward(),
                op.stakedId().kind().name(),
                op.stakedAccountId(),
                op.stakedNodeId(),
                accountStore,
                context.networkInfo());
    }

    /**
     * This method calculates the fees for the CryptoUpdate transaction.
     * Currently, it just duplicates all the logic from mono-service
     * @param feeContext the {@link FeeContext} with all information needed for the calculation
     * @return the calculated fees
     */
    @NonNull
    @Override
    public Fees calculateFees(@NonNull final FeeContext feeContext) {
        // Variable bytes plus two additional longs for balance and auto-renew period; plus a boolean for receiver sig
        // required.
        final var body = feeContext.body();
        final var accountStore = feeContext.readableStore(ReadableAccountStore.class);
        return cryptoUpdateFees(
                body,
                feeContext.feeCalculatorFactory().feeCalculator(SubType.DEFAULT),
                accountStore,
                feeContext.configuration());
    }

    /**
     * This method calculates the base size of the cryptoUpdate transaction.
     * This is the duplicated code as in mono-service
     * @param op the {@link CryptoUpdateTransactionBody}
     * @param keySize the size of the key
     * @return the calculated base size
     */
    private static long baseSizeOf(
            final CryptoUpdateTransactionBody op, final long keySize, final boolean unlimitedAutoAssociationsEnabled) {
        return BASIC_ENTITY_ID_SIZE
                + op.memoOrElse("").getBytes(StandardCharsets.UTF_8).length
                + (op.hasExpirationTime() ? LONG_SIZE : 0L)
                + (op.hasAutoRenewPeriod() ? LONG_SIZE : 0L)
                + (op.hasProxyAccountID() ? BASIC_ENTITY_ID_SIZE : 0L)
                + (op.hasMaxAutomaticTokenAssociations() ? INT_SIZE : 0L)
                + keySize;
    }

    /**
     * This method calculates the bytes for the CryptoUpdate transaction auto-renew information.
     * This is the duplicated code as in mono-service
     * @param account the {@link Account} to be updated
     * @return the calculated bytes
     */
    private static long cryptoAutoRenewRb(@Nullable final Account account) {
        final var fixedBytes = CRYPTO_ENTITY_SIZES.fixedBytesInAccountRepr();
        if (account == null) {
            return fixedBytes;
        }
        return fixedBytes
                + currentNonBaseBytes(account)
                + (account.numberAssociations() * CRYPTO_ENTITY_SIZES.bytesInTokenAssocRepr());
    }

    /**
     * This method calculates the bytes for the CryptoUpdate transaction related to memo and keys.
     * This is the duplicated code as in mono-service
     * @param account the {@link Account} to be updated
     * @return the calculated bytes
     */
    private static int currentNonBaseBytes(final Account account) {
        return account.memo().getBytes(StandardCharsets.UTF_8).length
                + getAccountKeyStorageSize(CommonPbjConverters.fromPbj(account.keyOrElse(Key.DEFAULT)))
                + (account.maxAutoAssociations() == 0 ? 0 : INT_SIZE);
    }

    /**
     * This method calculates the fees for the CryptoUpdate transaction.
     * This can also be used for lazy account creation logic in AutoAccountCreator class in future PRs
     * @param body the {@link TransactionBody}
     * @param feeCalculator the {@link FeeCalculator}
     * @param accountStore the {@link ReadableAccountStore}
     * @param configuration the {@link Configuration}
     * @return the calculated fees
     */
    private Fees cryptoUpdateFees(
            final TransactionBody body,
            final FeeCalculator feeCalculator,
            final ReadableAccountStore accountStore,
            final Configuration configuration) {
        final var op = body.cryptoUpdateAccountOrThrow();
        // When dispatching transaction body for hollow account we don't have update account set
        final var account = accountStore.getAccountById(op.accountIDToUpdateOrElse(AccountID.DEFAULT));
        final var autoRenewconfig = configuration.getConfigData(AutoRenewConfig.class);
        final var entityConfig = configuration.getConfigData(EntitiesConfig.class);
        final var unlimitedAutoAssoc = entityConfig.unlimitedAutoAssociationsEnabled();
        final var explicitAutoAssocSlotLifetime = autoRenewconfig.expireAccounts() ? 0L : THREE_MONTHS_IN_SECONDS;

        final var keySize = op.hasKey() ? getAccountKeyStorageSize(CommonPbjConverters.fromPbj(op.keyOrThrow())) : 0L;
        final var baseSize = baseSizeOf(op, keySize, unlimitedAutoAssoc);
        final var newMemoSize = op.memoOrElse("").getBytes(StandardCharsets.UTF_8).length;

        final var accountMemoSize = (account == null || account.memo() == null)
                ? 0L
                : account.memo().getBytes(StandardCharsets.UTF_8).length;
        final long newVariableBytes = (newMemoSize != 0L ? newMemoSize : accountMemoSize)
                + (keySize == 0L && account != null
                        ? getAccountKeyStorageSize(CommonPbjConverters.fromPbj(account.keyOrElse(Key.DEFAULT)))
                        : keySize);

        final long tokenRelBytes =
                (account == null ? 0 : account.numberAssociations()) * CRYPTO_ENTITY_SIZES.bytesInTokenAssocRepr();
        final long sharedFixedBytes = CRYPTO_ENTITY_SIZES.fixedBytesInAccountRepr() + tokenRelBytes;
        final var effectiveNow =
                body.transactionIDOrThrow().transactionValidStartOrThrow().seconds();
        final long newLifetime = ESTIMATOR_UTILS.relativeLifetime(
                effectiveNow, op.expirationTimeOrElse(Timestamp.DEFAULT).seconds());
        final long oldLifetime =
                ESTIMATOR_UTILS.relativeLifetime(effectiveNow, (account == null ? 0 : account.expirationSecond()));
        final long rbsDelta = ESTIMATOR_UTILS.changeInBsUsage(
                cryptoAutoRenewRb(account), oldLifetime, sharedFixedBytes + newVariableBytes, newLifetime);

        final var oldSlotsUsage = (account == null ? 0 : account.maxAutoAssociations()) * UPDATE_SLOT_MULTIPLIER;
        final var newSlotsUsage = op.hasMaxAutomaticTokenAssociations() && !unlimitedAutoAssoc
                ? op.maxAutomaticTokenAssociations().longValue() * UPDATE_SLOT_MULTIPLIER
                : oldSlotsUsage;
        // If given an explicit auto-assoc slot lifetime, we use it as a lower bound for both old and new lifetimes
        final long slotRbsDelta = ESTIMATOR_UTILS.changeInBsUsage(
                oldSlotsUsage,
                Math.max(explicitAutoAssocSlotLifetime, oldLifetime),
                newSlotsUsage,
                Math.max(explicitAutoAssocSlotLifetime, newLifetime));
        return feeCalculator
                .addBytesPerTransaction(baseSize)
                .addRamByteSeconds(rbsDelta > 0 ? rbsDelta : 0)
                .addRamByteSeconds(slotRbsDelta > 0 ? slotRbsDelta : 0)
                .calculate();
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy