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

com.hedera.node.app.service.file.impl.handlers.FileUpdateHandler Maven / Gradle / Ivy

The 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.file.impl.handlers;

import static com.hedera.hapi.node.base.ResponseCodeEnum.AUTORENEW_DURATION_NOT_IN_RANGE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.FILE_DELETED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_FILE_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_FILE_SIZE_EXCEEDED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.UNAUTHORIZED;
import static com.hedera.node.app.service.file.impl.FileServiceImpl.DEFAULT_MEMO;
import static com.hedera.node.app.service.file.impl.utils.FileServiceUtils.preValidate;
import static com.hedera.node.app.service.file.impl.utils.FileServiceUtils.validateAndAddRequiredKeys;
import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.CHILD;
import static com.hedera.node.app.spi.workflows.HandleContext.TransactionCategory.PRECEDING;
import static com.hedera.node.app.spi.workflows.HandleException.validateFalse;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.FileID;
import com.hedera.hapi.node.base.HederaFunctionality;
import com.hedera.hapi.node.base.SubType;
import com.hedera.hapi.node.base.Timestamp;
import com.hedera.hapi.node.file.FileUpdateTransactionBody;
import com.hedera.hapi.node.state.file.File;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.hapi.fees.usage.SigUsage;
import com.hedera.node.app.hapi.fees.usage.file.ExtantFileContext;
import com.hedera.node.app.hapi.fees.usage.file.FileOpsUsage;
import com.hedera.node.app.hapi.utils.CommonPbjConverters;
import com.hedera.node.app.hapi.utils.fee.SigValueObj;
import com.hedera.node.app.service.file.FileSignatureWaivers;
import com.hedera.node.app.service.file.ReadableFileStore;
import com.hedera.node.app.service.file.impl.WritableFileStore;
import com.hedera.node.app.service.file.impl.WritableUpgradeFileStore;
import com.hedera.node.app.spi.authorization.SystemPrivilege;
import com.hedera.node.app.spi.fees.FeeContext;
import com.hedera.node.app.spi.fees.Fees;
import com.hedera.node.app.spi.validation.AttributeValidator;
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.app.spi.workflows.record.StreamBuilder;
import com.hedera.node.config.data.AccountsConfig;
import com.hedera.node.config.data.FilesConfig;
import com.hedera.node.config.data.LedgerConfig;
import com.hedera.node.config.types.LongPair;
import com.hederahashgraph.api.proto.java.FeeData;
import com.hederahashgraph.api.proto.java.KeyList;
import edu.umd.cs.findbugs.annotations.NonNull;
import javax.inject.Inject;
import javax.inject.Singleton;

/**
 * This class contains all workflow-related functionality regarding {@link HederaFunctionality#FILE_UPDATE}.
 */
@Singleton
public class FileUpdateHandler implements TransactionHandler {
    private static final Timestamp EXPIRE_NEVER =
            Timestamp.newBuilder().seconds(Long.MAX_VALUE - 1).build();
    private final FileOpsUsage fileOpsUsage;
    private final FileSignatureWaivers fileSignatureWaivers;

    /**
     * Constructs a {@link FileUpdateHandler} with the given {@link FileOpsUsage} and {@link FileSignatureWaivers}.
     * @param fileOpsUsage the file operation usage calculator
     * @param fileSignatureWaivers the file signature waivers
     */
    @Inject
    public FileUpdateHandler(final FileOpsUsage fileOpsUsage, final FileSignatureWaivers fileSignatureWaivers) {
        this.fileOpsUsage = fileOpsUsage;
        this.fileSignatureWaivers = fileSignatureWaivers;
    }

    /**
     * Performs checks independent of state or context.
     * @param txn the transaction to check
     */
    @Override
    public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException {
        final var transactionBody = txn.fileUpdateOrThrow();

        if (transactionBody.fileID() == null) {
            throw new PreCheckException(INVALID_FILE_ID);
        }
    }

    /**
     * This method is called during the pre-handle workflow.
     *
     * 

Determines signatures needed for update a file * * @param context the {@link PreHandleContext} which collects all information that will be * passed to {@code #handle()} * @throws PreCheckException if any issue happens on the pre handle level */ @Override public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException { requireNonNull(context); final var body = context.body(); final var op = body.fileUpdateOrThrow(); final var fileStore = context.createStore(ReadableFileStore.class); final var transactionFileId = requireNonNull(op.fileID()); preValidate(transactionFileId, fileStore, context); final var areSignaturesWaived = fileSignatureWaivers.areFileUpdateSignaturesWaived(body, context.payer()); if (areSignaturesWaived) { return; } var file = fileStore.getFileLeaf(transactionFileId); if (wantsToMutateNonExpiryField(op)) { validateAndAddRequiredKeys(file, op.keys(), context); } } @Override public void handle(@NonNull final HandleContext handleContext) throws HandleException { requireNonNull(handleContext); final var fileStore = handleContext.storeFactory().writableStore(WritableFileStore.class); final var fileUpdate = handleContext.body().fileUpdateOrThrow(); final var fileServiceConfig = handleContext.configuration().getConfigData(FilesConfig.class); if (fileUpdate.fileID() == null) { throw new HandleException(INVALID_FILE_ID); } // the update file always will be for the node, not a particular ledger that's why we just compare the fileNum // and ignore shard and realm FileID fileID = fileUpdate.fileIDOrThrow(); LongPair upgradeFileRange = fileServiceConfig.softwareUpdateRange(); if (fileID.fileNum() >= upgradeFileRange.left() && fileID.fileNum() <= upgradeFileRange.right()) { handleUpdateUpgradeFile(fileUpdate, handleContext); return; } final var maybeFile = fileStore.get(fileUpdate.fileIDOrElse(FileID.DEFAULT)); if (maybeFile.isEmpty()) { throw new HandleException(INVALID_FILE_ID); } final var file = maybeFile.get(); validateFalse(file.deleted(), FILE_DELETED); // First validate this file is mutable; and the pending mutations are allowed if (wantsToMutateNonExpiryField(fileUpdate)) { if (handleContext.hasPrivilegedAuthorization() != SystemPrivilege.AUTHORIZED) { validateTrue(file.hasKeys() && !file.keys().keys().isEmpty(), UNAUTHORIZED); } validateMaybeNewMemo(handleContext.attributeValidator(), fileUpdate); } validateAutoRenew(fileUpdate, handleContext); // Now we apply the mutations to a builder final var builder = new File.Builder(); // But first copy over the immutable topic attributes to the builder builder.fileId(file.fileId()); builder.deleted(file.deleted()); // And then resolve mutable attributes, and put the new topic back final var accountsConfig = handleContext.configuration().getConfigData(AccountsConfig.class); resolveMutableBuilderAttributes( fileUpdate, builder, fileServiceConfig, file, fileID, accountsConfig, handleContext.payer()); fileStore.put(builder.build()); } @NonNull @Override public Fees calculateFees(@NonNull FeeContext feeContext) { final var op = feeContext.body(); final var file = feeContext .readableStore(ReadableFileStore.class) .getFileLeaf(op.fileUpdateOrThrow().fileIDOrThrow()); final AccountID payerId = op.transactionID().accountID(); final SystemPrivilege privilege = feeContext.authorizer().hasPrivilegedAuthorization(payerId, HederaFunctionality.FILE_UPDATE, op); // Even if the privilege is UNAUTHORIZED or IMPERMISSIBLE continue with a free fee // The appropriate error is thrown at a later stage of the workflow if (privilege != SystemPrivilege.UNNECESSARY) { return Fees.FREE; } return feeContext .feeCalculatorFactory() .feeCalculator(SubType.DEFAULT) .legacyCalculate(sigValueObj -> usageGiven(CommonPbjConverters.fromPbj(op), sigValueObj, CommonPbjConverters.fromPbj(file))); } private void handleUpdateUpgradeFile(FileUpdateTransactionBody fileUpdate, HandleContext handleContext) { final var fileStore = handleContext.storeFactory().writableStore(WritableUpgradeFileStore.class); // empty old upgrade file FileID fileId = fileUpdate.fileIDOrThrow(); if (fileUpdate.contents() != null && fileUpdate.contents().length() > 0) { fileStore.resetFileContents(fileId); fileStore.addUpgradeContent(fileId, fileUpdate.contents()); } // Note that upgrade file memos are generated programmatically // as the SHA-384 hash of their contents final var file = new File.Builder() .fileId(fileId) .deleted(false) .expirationSecond(fileUpdate.expirationTimeOrElse(EXPIRE_NEVER).seconds()) .build(); fileStore.add(file); } private void resolveMutableBuilderAttributes( @NonNull final FileUpdateTransactionBody op, @NonNull final File.Builder builder, @NonNull final FilesConfig filesConfig, @NonNull final File file, @NonNull final FileID fileId, @NonNull final AccountsConfig accountsConfig, @NonNull final AccountID payerId) { if (op.hasKeys()) { builder.keys(op.keys()); } else { builder.keys(file.keys()); } final var contentLength = op.contents().length(); final var zeroLengthShouldClearTarget = accountsConfig.isSuperuser(payerId) && filesConfig.isOverrideFile(fileId); if (contentLength > 0 || zeroLengthShouldClearTarget) { if (contentLength > filesConfig.maxSizeKb() * 1024L) { throw new HandleException(MAX_FILE_SIZE_EXCEEDED); } builder.contents(op.contents()); } else { builder.contents(file.contents()); } if (op.hasMemo()) { builder.memo(op.memo()); } else { builder.memo(file.memo()); } if (op.hasExpirationTime() && op.expirationTime().seconds() > file.expirationSecond()) { builder.expirationSecond(op.expirationTime().seconds()); } else { builder.expirationSecond(file.expirationSecond()); } } private void validateAutoRenew(FileUpdateTransactionBody op, HandleContext handleContext) { if (op.hasExpirationTime()) { final var category = handleContext .savepointStack() .getBaseBuilder(StreamBuilder.class) .category(); final var isInternalDispatch = category == CHILD || category == PRECEDING; final long startSeconds = isInternalDispatch ? handleContext.consensusNow().getEpochSecond() : handleContext .body() .transactionID() .transactionValidStart() .seconds(); final long effectiveDuration = op.expirationTime().seconds() - startSeconds; final var ledgerConfig = handleContext.configuration().getConfigData(LedgerConfig.class); final long maxEntityLifetime = ledgerConfig.autoRenewPeriodMaxDuration(); final long minEntityLifetime = ledgerConfig.autoRenewPeriodMinDuration(); validateTrue( effectiveDuration >= minEntityLifetime && effectiveDuration <= maxEntityLifetime, AUTORENEW_DURATION_NOT_IN_RANGE); } } /** * Determines if the update operation wants to mutate non-expiry fields. * * @param op the update operation transaction body * @return {@code true} if the operation wants to mutate non-expiry fields, {@code false} otherwise */ public static boolean wantsToMutateNonExpiryField(@NonNull final FileUpdateTransactionBody op) { return op.hasMemo() || op.hasKeys() || op.contents().length() > 0; } private void validateMaybeNewMemo( @NonNull final AttributeValidator attributeValidator, @NonNull final FileUpdateTransactionBody op) { if (op.hasMemo()) { attributeValidator.validateMemo(op.memo()); } } private FeeData usageGiven( final com.hederahashgraph.api.proto.java.TransactionBody txn, final SigValueObj svo, final com.hederahashgraph.api.proto.java.File file) { final var sigUsage = new SigUsage(svo.getTotalSigCount(), svo.getSignatureSize(), svo.getPayerAcctSigCount()); if (file != null) { final var contents = file.getContents(); final var ctx = ExtantFileContext.newBuilder() .setCurrentSize(contents == null ? 0 : contents.size()) .setCurrentWacl(file.getKeys()) .setCurrentMemo(file.getMemo()) .setCurrentExpiry(file.getExpirationSecond()) .build(); return fileOpsUsage.fileUpdateUsage(txn, sigUsage, ctx); } else { final long now = txn.getTransactionID().getTransactionValidStart().getSeconds(); return fileOpsUsage.fileUpdateUsage(txn, sigUsage, missingCtx(now)); } } static ExtantFileContext missingCtx(final long now) { return ExtantFileContext.newBuilder() .setCurrentExpiry(now) .setCurrentMemo(DEFAULT_MEMO) .setCurrentWacl(KeyList.getDefaultInstance()) .setCurrentSize(0) .build(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy