com.hedera.node.app.service.schedule.impl.handlers.ScheduleCreateHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of app-service-schedule-impl Show documentation
Show all versions of app-service-schedule-impl Show documentation
Default Hedera Schedule Service Implementation
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.schedule.impl.handlers;
import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST;
import static com.hedera.hapi.node.base.ResponseCodeEnum.IDENTICAL_SCHEDULE_ALREADY_CREATED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ADMIN_KEY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED;
import static com.hedera.hapi.node.base.ResponseCodeEnum.MEMO_TOO_LONG;
import static com.hedera.hapi.node.base.ResponseCodeEnum.MISSING_EXPIRY_TIME;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULED_TRANSACTION_NOT_IN_WHITELIST;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SCHEDULE_EXPIRY_IS_BUSY;
import static com.hedera.hapi.node.base.SubType.DEFAULT;
import static com.hedera.hapi.node.base.SubType.SCHEDULE_CREATE_CONTRACT_CALL;
import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj;
import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.childAsOrdinary;
import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.createProvisionalSchedule;
import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.functionalityForType;
import static com.hedera.node.app.service.schedule.impl.handlers.HandlerUtility.transactionIdForScheduled;
import static com.hedera.node.app.spi.validation.Validations.mustExist;
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.HederaFunctionality;
import com.hedera.hapi.node.base.ScheduleID;
import com.hedera.hapi.node.scheduled.SchedulableTransactionBody;
import com.hedera.hapi.node.scheduled.ScheduleCreateTransactionBody;
import com.hedera.hapi.node.state.schedule.Schedule;
import com.hedera.hapi.node.state.schedule.ScheduledOrder;
import com.hedera.hapi.node.state.throttles.ThrottleUsageSnapshots;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.hapi.fees.usage.SigUsage;
import com.hedera.node.app.hapi.fees.usage.schedule.ScheduleOpsUsage;
import com.hedera.node.app.hapi.utils.fee.SigValueObj;
import com.hedera.node.app.service.schedule.ScheduleStreamBuilder;
import com.hedera.node.app.service.schedule.WritableScheduleStore;
import com.hedera.node.app.service.token.ReadableAccountStore;
import com.hedera.node.app.spi.fees.FeeContext;
import com.hedera.node.app.spi.fees.Fees;
import com.hedera.node.app.spi.throttle.Throttle;
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.HederaConfig;
import com.hedera.node.config.data.LedgerConfig;
import com.hedera.node.config.data.SchedulingConfig;
import com.hederahashgraph.api.proto.java.FeeData;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.time.Instant;
import java.time.InstantSource;
import java.util.Collections;
import java.util.Objects;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
/**
* This class contains all workflow-related functionality regarding {@link HederaFunctionality#SCHEDULE_CREATE}.
*/
@Singleton
public class ScheduleCreateHandler extends AbstractScheduleHandler implements TransactionHandler {
private static final Logger log = LogManager.getLogger(ScheduleCreateHandler.class);
private final ScheduleOpsUsage scheduleOpsUsage = new ScheduleOpsUsage();
private final InstantSource instantSource;
private final Throttle.Factory throttleFactory;
@Inject
public ScheduleCreateHandler(
@NonNull final InstantSource instantSource, @NonNull final Throttle.Factory throttleFactory) {
this.instantSource = requireNonNull(instantSource);
this.throttleFactory = requireNonNull(throttleFactory);
}
@Override
public void pureChecks(@NonNull final TransactionBody body) throws PreCheckException {
requireNonNull(body);
validateTruePreCheck(body.hasScheduleCreate(), INVALID_TRANSACTION_BODY);
final var op = body.scheduleCreateOrThrow();
validateTruePreCheck(op.hasScheduledTransactionBody(), INVALID_TRANSACTION);
// (FUTURE) Add a dedicated response code for an op waiting for an unspecified expiration time
validateFalsePreCheck(op.waitForExpiry() && !op.hasExpirationTime(), MISSING_EXPIRY_TIME);
}
@Override
public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException {
requireNonNull(context);
final var body = context.body();
// We ensure this exists in pureChecks()
final var op = body.scheduleCreateOrThrow();
final var config = context.configuration();
final var hederaConfig = config.getConfigData(HederaConfig.class);
validateTruePreCheck(op.memo().length() <= hederaConfig.transactionMaxMemoUtf8Bytes(), MEMO_TOO_LONG);
// For backward compatibility, use ACCOUNT_ID_DOES_NOT_EXIST for a nonexistent designated payer
if (op.hasPayerAccountID()) {
final var accountStore = context.createStore(ReadableAccountStore.class);
final var payer = accountStore.getAccountById(op.payerAccountIDOrThrow());
mustExist(payer, ACCOUNT_ID_DOES_NOT_EXIST);
}
final var schedulingConfig = config.getConfigData(SchedulingConfig.class);
validateTruePreCheck(
isAllowedFunction(op.scheduledTransactionBodyOrThrow(), schedulingConfig),
SCHEDULED_TRANSACTION_NOT_IN_WHITELIST);
// If an admin key is present, it must sign
if (op.hasAdminKey()) {
context.requireKey(op.adminKeyOrThrow());
}
final var ledgerConfig = config.getConfigData(LedgerConfig.class);
final long maxLifetime = schedulingConfig.longTermEnabled()
? schedulingConfig.maxExpirationFutureSeconds()
: ledgerConfig.scheduleTxExpiryTimeSecs();
final var schedule = createProvisionalSchedule(
body, instantSource.instant(), maxLifetime, schedulingConfig.longTermEnabled());
final var transactionKeys = getRequiredKeys(schedule, context::allKeysForTransaction);
// If the schedule payer inherits from the ScheduleCreate, it is already in the required keys
if (op.hasPayerAccountID()) {
context.optionalKey(transactionKeys.payerKey());
}
// Any required non-payer key may optionally provide its signature with the ScheduleCreate
context.optionalKeys(transactionKeys.requiredNonPayerKeys());
}
@Override
public void handle(@NonNull final HandleContext context) throws HandleException {
requireNonNull(context);
final var schedulingConfig = context.configuration().getConfigData(SchedulingConfig.class);
final boolean isLongTermEnabled = schedulingConfig.longTermEnabled();
final var ledgerConfig = context.configuration().getConfigData(LedgerConfig.class);
final var maxLifetime = isLongTermEnabled
? schedulingConfig.maxExpirationFutureSeconds()
: ledgerConfig.scheduleTxExpiryTimeSecs();
final var consensusNow = context.consensusNow();
final var provisionalSchedule =
createProvisionalSchedule(context.body(), consensusNow, maxLifetime, isLongTermEnabled);
final var now = consensusNow.getEpochSecond();
final var then = provisionalSchedule.calculatedExpirationSecond();
validateTrue(then > now, SCHEDULE_EXPIRATION_TIME_MUST_BE_HIGHER_THAN_CONSENSUS_TIME);
validateTrue(then <= now + maxLifetime, SCHEDULE_EXPIRATION_TIME_TOO_FAR_IN_FUTURE);
validateTrue(
isAllowedFunction(provisionalSchedule.scheduledTransactionOrThrow(), schedulingConfig),
SCHEDULED_TRANSACTION_NOT_IN_WHITELIST);
context.attributeValidator().validateMemo(provisionalSchedule.memo());
context.attributeValidator()
.validateMemo(provisionalSchedule.scheduledTransactionOrThrow().memo());
if (provisionalSchedule.hasAdminKey()) {
try {
context.attributeValidator().validateKey(provisionalSchedule.adminKeyOrThrow());
} catch (HandleException e) {
throw new HandleException(INVALID_ADMIN_KEY);
}
}
final var validationResult = validate(provisionalSchedule, consensusNow, isLongTermEnabled);
validateTrue(isMaybeExecutable(validationResult), validationResult);
// Note that we must store the original ScheduleCreate transaction body in the Schedule so
// we can compare those bytes to any new ScheduleCreate transaction for detecting duplicate
// ScheduleCreate transactions. SchedulesByEquality is the virtual map for that task.
final var scheduleStore = context.storeFactory().writableStore(WritableScheduleStore.class);
final var possibleDuplicateId = scheduleStore.getByEquality(provisionalSchedule);
final var possibleDuplicate = possibleDuplicateId == null ? null : scheduleStore.get(possibleDuplicateId);
final var duplicate = maybeDuplicate(provisionalSchedule, possibleDuplicate);
if (duplicate != null) {
final var scheduledTxnId = duplicate
.originalCreateTransactionOrThrow()
.transactionIDOrThrow()
.copyBuilder()
.scheduled(true)
.build();
context.savepointStack()
.getBaseBuilder(ScheduleStreamBuilder.class)
.scheduleID(duplicate.scheduleId())
.scheduledTransactionID(scheduledTxnId);
throw new HandleException(IDENTICAL_SCHEDULE_ALREADY_CREATED);
}
validateTrue(
scheduleStore.numSchedulesInState() + 1 <= schedulingConfig.maxNumber(),
MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED);
final var capacityFraction = schedulingConfig.schedulableCapacityFraction();
final var usageSnapshots = scheduleStore.usageSnapshotsForScheduled(then);
final var throttle =
upToDateThrottle(then, capacityFraction.asApproxCapacitySplit(), usageSnapshots, scheduleStore);
validateTrue(
throttle.allow(
provisionalSchedule.payerAccountIdOrThrow(),
childAsOrdinary(provisionalSchedule),
functionOf(provisionalSchedule),
Instant.ofEpochSecond(then)),
SCHEDULE_EXPIRY_IS_BUSY);
scheduleStore.trackUsage(then, throttle.usageSnapshots());
// With all validations done, we check if the new schedule is already executable
final var transactionKeys = getTransactionKeysOrThrow(provisionalSchedule, context::allKeysForTransaction);
final var requiredKeys = allRequiredKeys(transactionKeys);
final var signatories =
newSignatories(context.keyVerifier().authorizingSimpleKeys(), Collections.emptyList(), requiredKeys);
final var schedulingTxnId =
provisionalSchedule.originalCreateTransactionOrThrow().transactionIDOrThrow();
final var schedulerId = schedulingTxnId.accountIDOrThrow();
final var scheduleId = ScheduleID.newBuilder()
.shardNum(schedulerId.shardNum())
.realmNum(schedulerId.realmNum())
.scheduleNum(context.entityNumGenerator().newEntityNum())
.build();
var schedule = provisionalSchedule
.copyBuilder()
.scheduleId(scheduleId)
.schedulerAccountId(schedulerId)
.signatories(signatories)
.build();
if (tryToExecuteSchedule(context, schedule, requiredKeys, validationResult, isLongTermEnabled)) {
schedule = markedExecuted(schedule, consensusNow);
}
scheduleStore.put(schedule);
context.savepointStack()
.getBaseBuilder(ScheduleStreamBuilder.class)
.scheduleID(schedule.scheduleId())
.scheduledTransactionID(transactionIdForScheduled(schedule));
}
@NonNull
@Override
public Fees calculateFees(@NonNull final FeeContext feeContext) {
requireNonNull(feeContext);
final var body = feeContext.body();
final var config = feeContext.configuration();
final var ledgerConfig = config.getConfigData(LedgerConfig.class);
final var schedulingConfig = config.getConfigData(SchedulingConfig.class);
final var subType = body.scheduleCreateOrElse(ScheduleCreateTransactionBody.DEFAULT)
.scheduledTransactionBodyOrElse(SchedulableTransactionBody.DEFAULT)
.hasContractCall()
? SCHEDULE_CREATE_CONTRACT_CALL
: DEFAULT;
return feeContext
.feeCalculatorFactory()
.feeCalculator(subType)
.legacyCalculate(sigValueObj -> usageGiven(
fromPbj(body),
sigValueObj,
schedulingConfig.longTermEnabled(),
ledgerConfig.scheduleTxExpiryTimeSecs()));
}
private @NonNull FeeData usageGiven(
@NonNull final com.hederahashgraph.api.proto.java.TransactionBody txn,
@NonNull final SigValueObj svo,
final boolean longTermEnabled,
final long scheduledTxExpiryTimeSecs) {
final var op = txn.getScheduleCreate();
final var sigUsage = new SigUsage(svo.getTotalSigCount(), svo.getSignatureSize(), svo.getPayerAcctSigCount());
final long lifetimeSecs;
if (op.hasExpirationTime() && longTermEnabled) {
lifetimeSecs = Math.max(
0L,
op.getExpirationTime().getSeconds()
- txn.getTransactionID().getTransactionValidStart().getSeconds());
} else {
lifetimeSecs = scheduledTxExpiryTimeSecs;
}
return scheduleOpsUsage.scheduleCreateUsage(txn, sigUsage, lifetimeSecs);
}
private @Nullable Schedule maybeDuplicate(@NonNull final Schedule schedule, @Nullable final Schedule duplicate) {
if (duplicate == null) {
return null;
}
if (areIdentical(duplicate, schedule)) {
return duplicate;
}
return null;
}
private boolean areIdentical(@NonNull final Schedule candidate, @NonNull final Schedule requested) {
return candidate.waitForExpiry() == requested.waitForExpiry()
&& candidate.providedExpirationSecond() == requested.providedExpirationSecond()
&& Objects.equals(candidate.memo(), requested.memo())
&& Objects.equals(candidate.adminKey(), requested.adminKey())
// @note We should check scheduler here, but mono doesn't, so we cannot either, yet.
&& Objects.equals(candidate.scheduledTransaction(), requested.scheduledTransaction());
}
private boolean isAllowedFunction(
@NonNull final SchedulableTransactionBody body, @NonNull final SchedulingConfig config) {
final var scheduledFunctionality = functionalityForType(body.data().kind());
return config.whitelist().functionalitySet().contains(scheduledFunctionality);
}
private HederaFunctionality functionOf(@NonNull final Schedule schedule) {
return functionalityForType(
schedule.scheduledTransactionOrThrow().data().kind());
}
/**
* Attempts to recover a throttle from the given usage snapshots, or creates a new throttle if the recovery fails.
* (This edge case can occur if the network throttle definitions changed since a transaction was last scheduled
* in the given second and snapshots were taken.)
* @param then the second for which the throttle is being recovered
* @param capacitySplit the capacity split for the throttle
* @param usageSnapshots the usage snapshots to recover from
* @return the throttle
*/
private Throttle upToDateThrottle(
final long then,
final int capacitySplit,
@Nullable final ThrottleUsageSnapshots usageSnapshots,
@NonNull final WritableScheduleStore scheduleStore) {
requireNonNull(scheduleStore);
try {
return throttleFactory.newThrottle(capacitySplit, usageSnapshots);
} catch (Exception e) {
final var instantThen = Instant.ofEpochSecond(then);
log.info(
"Could not recreate throttle at {} from {} ({}), rebuilding with up-to-date throttle",
instantThen,
usageSnapshots,
e.getMessage());
final var throttle = throttleFactory.newThrottle(capacitySplit, null);
final var counts = requireNonNull(scheduleStore.scheduledCountsAt(then));
final int n = counts.numberScheduled();
for (int i = 0; i < n; i++) {
final var scheduleId = requireNonNull(scheduleStore.getByOrder(new ScheduledOrder(then, i)));
final var schedule = requireNonNull(scheduleStore.get(scheduleId));
// Consume capacity from every already-scheduled transaction in the new throttle
throttle.allow(
schedule.payerAccountIdOrThrow(),
childAsOrdinary(schedule),
functionOf(schedule),
Instant.ofEpochSecond(then));
}
log.info("Rebuilt throttle at {} from {} scheduled transactions", instantThen, n);
return throttle;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy