Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement hss sign schedule function #16789

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.PRE_HANDLE_FAILURE;
import static com.hedera.node.app.workflows.prehandle.PreHandleResult.Status.SO_FAR_SO_GOOD;
import static java.util.Collections.emptyMap;
import static java.util.Collections.emptySet;
import static java.util.Collections.emptySortedSet;
import static java.util.Collections.unmodifiableSortedSet;
import static java.util.Objects.requireNonNull;
Expand Down Expand Up @@ -169,7 +168,7 @@ public Dispatch createChildDispatch(
requireNonNull(options);

final var preHandleResult = preHandleChild(options.body(), options.payerId(), config, readableStoreFactory);
final var childVerifier = getKeyVerifier(options.effectiveKeyVerifier(), config, emptySet());
final var childVerifier = getKeyVerifier(options.effectiveKeyVerifier(), config, options.authorizingKeys());
final var childTxnInfo = getTxnInfoFrom(options.payerId(), options.body());
final var streamMode = config.getConfigData(BlockStreamConfig.class).streamMode();
final var childStack = SavepointStackImpl.newChildStack(
Expand Down Expand Up @@ -438,8 +437,7 @@ public SignatureVerification verificationFor(@NonNull final Key key) {
@Override
public SignatureVerification verificationFor(
@NonNull final Key key, @NonNull final VerificationAssistant callback) {
// We do not yet support signing scheduled transactions from within the EVM
throw new UnsupportedOperationException();
return verifier.verificationFor(key, callback);
}

@NonNull
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,28 @@ void keyVerifierWithNullCallbackAndAuthorizingKeysAsExpected() {
assertThat(derivedVerifier.authorizingSimpleKeys()).containsExactly(A_CONTRACT_ID_KEY);
}

@Test
void keyVerifierWithCallbackAndAuthorizingKeysAsExpected() {
final var derivedVerifier =
ChildDispatchFactory.getKeyVerifier(a -> true, DEFAULT_CONFIG, Set.of(A_CONTRACT_ID_KEY));
assertThat(derivedVerifier.verificationFor(Key.DEFAULT).passed()).isTrue();
assertThat(derivedVerifier.verificationFor(Key.DEFAULT, (k, v) -> true).passed())
.isTrue();
assertThatThrownBy(() -> derivedVerifier.verificationFor(Bytes.EMPTY))
.isInstanceOf(UnsupportedOperationException.class);
assertThat(derivedVerifier.numSignaturesVerified()).isZero();
assertThat(derivedVerifier.authorizingSimpleKeys()).containsExactly(A_CONTRACT_ID_KEY);
assertThat(derivedVerifier.authorizingSimpleKeys()).isNotEmpty();
Comment on lines +197 to +198
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, assertThat has a "fluid" API, you can write

        assertThat(derivedVerifier.authorizingSimpleKeys()).isNotEmpty().containsExactly(A_CONTRACT_ID_KEY);

(though in fact that's redundant in this particular case, don't need the isNotEmpty() here)
(in general should be containsOnly as even if this test had more then one element to be contained the order doesn't matter)
(just spitting out AssertJ stuff for posterity, don't even think about it.)

}

@Test
void keyVerifierOnlySupportsKeyVerification() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this test what does this test's name mean? (I know you didn't name it, but you could fix the name anyway, if I'm not just confused.)

final var derivedVerifier = ChildDispatchFactory.getKeyVerifier(verifierCallback, DEFAULT_CONFIG, emptySet());
assertThatThrownBy(() -> derivedVerifier.verificationFor(Key.DEFAULT, assistant))
.isInstanceOf(UnsupportedOperationException.class);
assertThat(derivedVerifier.verificationFor(Key.DEFAULT, assistant).passed())
.isFalse();
assertThatThrownBy(() -> derivedVerifier.verificationFor(Bytes.EMPTY))
.isInstanceOf(UnsupportedOperationException.class);
assertThat(derivedVerifier.numSignaturesVerified()).isEqualTo(0L);
assertThat(derivedVerifier.numSignaturesVerified()).isZero();
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,14 @@ public record ContractsConfig(
boolean precompileHrcFacadeAssociateEnabled,
@ConfigProperty(value = "systemContract.accountService.enabled", defaultValue = "true") @NetworkProperty
boolean systemContractAccountServiceEnabled,
@ConfigProperty(value = "systemContract.scheduleService.enabled", defaultValue = "false") @NetworkProperty
@ConfigProperty(value = "systemContract.scheduleService.enabled", defaultValue = "true") @NetworkProperty
boolean systemContractScheduleServiceEnabled,
@ConfigProperty(value = "systemContract.scheduleService.signSchedule.enabled", defaultValue = "true")
@NetworkProperty
boolean systemContractSignScheduleEnabled,
@ConfigProperty(value = "systemContract.scheduleService.authorizeSchedule.enabled", defaultValue = "false")
stoyanov-st marked this conversation as resolved.
Show resolved Hide resolved
@NetworkProperty
boolean systemContractAuthorizeScheduleEnabled,
@ConfigProperty(value = "systemContract.accountService.isAuthorizedRawEnabled", defaultValue = "true")
@NetworkProperty
boolean systemContractAccountServiceIsAuthorizedRawEnabled,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,9 @@ private InvolvedParties computeInvolvedParties(
}

private boolean contractNotRequired(@Nullable final HederaEvmAccount to, @NonNull final Configuration config) {
david-bakin-sl marked this conversation as resolved.
Show resolved Hide resolved
final var maybeGrandfatheredNumber =
(to == null) ? null : to.isTokenFacade() ? null : to.hederaId().accountNumOrThrow();
final var maybeGrandfatheredNumber = (to == null || to.isTokenFacade() || to.isScheduleTxnFacade())
david-bakin-sl marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

@david-bakin-sl david-bakin-sl Nov 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I was curious why this change didn't have a corresponding test to modify in TransactionProcessorTest but I see it's basically not unit-testable: It's a private method nested 3 levels deep in calls from other private methods. You basically can only test this little method by starting at the top-level method in this class (processTransaction) and maneuvering to drive private method computeInvolvedParties to the exact required state and inspecting the resulting HederaEvmTransactionResult. After mocking all dependencies of actually doing the transaction you're submitting and also for computing gas.

private methods make classes untestable, most often, in my experience. And it doesn't add much in the way of "safety" in terms of not using things that weren't meant to be public. If the original dev didn't want the stuff to be public he should have isolated it behind an interface for the public methods, leaving the formerly-private methods available publicly in the ...Impl so they could be tested.

? null
: to.hederaId().accountNumOrThrow();

return featureFlags.isAllowCallsToNonContractAccountsEnabled(
config.getConfigData(ContractsConfig.class), maybeGrandfatheredNumber);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,11 @@ public enum DispatchType {
/**
* Dispatch for Hedera token reject functionality with resource prices on a non-fungible token.
*/
TOKEN_REJECT_NFT(HederaFunctionality.TOKEN_REJECT, TOKEN_NON_FUNGIBLE_UNIQUE);
TOKEN_REJECT_NFT(HederaFunctionality.TOKEN_REJECT, TOKEN_NON_FUNGIBLE_UNIQUE),
/**
* Dispatch for Hedera schedule sign functionality with default resource prices.
*/
SCHEDULE_SIGN(HederaFunctionality.SCHEDULE_SIGN, DEFAULT);

private final HederaFunctionality functionality;
private final SubType subtype;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,10 @@
public ExchangeRate currentExchangeRate() {
return context.exchangeRateInfo().activeRate(context.consensusNow());
}

@Override
@Nullable
public Key maybeEthSenderKey() {
return maybeEthSenderKey;

Check warning on line 156 in hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/scope/HandleSystemContractOperations.java#L156

Added line #L156 was not covered by tests
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,10 @@ public Transaction syntheticTransactionForNativeCall(
public ExchangeRate currentExchangeRate() {
return context.exchangeRateInfo().activeRate(instantSource.instant());
}

@Override
@Nullable
public Key maybeEthSenderKey() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import com.hedera.node.app.service.contract.impl.records.ContractCallStreamBuilder;
import com.hedera.node.app.spi.workflows.record.StreamBuilder;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.tuweni.bytes.Bytes;
Expand Down Expand Up @@ -154,4 +155,10 @@ Transaction syntheticTransactionForNativeCall(
*/
@NonNull
ExchangeRate currentExchangeRate();

/**
* Returns the ecdsa eth key for the sender of current transaction if one exists.
*/
@Nullable
Key maybeEthSenderKey();
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@

import com.hedera.hapi.node.base.ContractID;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.AbstractNativeSystemContract;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.has.HasCallFactory;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hss.HssCallFactory;
import com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils;
import com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.EntityType;
import edu.umd.cs.findbugs.annotations.NonNull;
Expand All @@ -45,7 +45,7 @@ public class HssSystemContract extends AbstractNativeSystemContract implements H
public static final ContractID HSS_CONTRACT_ID = asNumberedContractId(Address.fromHexString(HSS_EVM_ADDRESS));

@Inject
public HssSystemContract(@NonNull final GasCalculator gasCalculator, @NonNull final HasCallFactory callFactory) {
public HssSystemContract(@NonNull final GasCalculator gasCalculator, @NonNull final HssCallFactory callFactory) {
super(HSS_SYSTEM_CONTRACT_NAME, callFactory, HSS_CONTRACT_ID, gasCalculator);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/*
* Copyright (C) 2023-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.contract.impl.exec.systemcontracts.hss;

import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY;
import static com.hedera.hapi.node.base.ResponseCodeEnum.SUCCESS;
import static com.hedera.node.app.service.contract.impl.exec.failure.CustomExceptionalHaltReason.ERROR_DECODING_PRECOMPILE_INPUT;
import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.FullResult.haltResult;
import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.Call.PricedResult.gasOnly;
import static com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes.encodedRc;
import static com.hedera.node.app.service.contract.impl.exec.utils.FrameUtils.contractsConfigOf;

import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.ResponseCodeEnum;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.contract.impl.exec.gas.DispatchGasCalculator;
import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator;
import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategy;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.AbstractCall;
import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater;
import com.hedera.node.app.service.contract.impl.records.ContractCallStreamBuilder;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.Objects;
import java.util.Set;
import org.hyperledger.besu.evm.frame.MessageFrame;

/**
* An HSS call that simply dispatches a synthetic transaction body and returns a result that is
* an encoded {@link ResponseCodeEnum}.
*/
public class DispatchForResponseCodeHssCall extends AbstractCall {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So close to DispatchForResponseCodeHtsCall and yet not close enough to derive from same abstract class. (Or is it close enough?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want all of the clutter if possible. Maybe I'll create an abstract class if it turns out the addition updaters are needed here.


private final AccountID senderId;

@Nullable
private final TransactionBody syntheticBody;

private final VerificationStrategy verificationStrategy;
private final DispatchGasCalculator dispatchGasCalculator;
private final Set<Key> authorizingKeys;

/**
* Convenience overload that slightly eases construction for the most common case.
*
* @param attempt the attempt to translate to a dispatching
* @param syntheticBody the synthetic body to dispatch
* @param dispatchGasCalculator the dispatch gas calculator to use
*/
public DispatchForResponseCodeHssCall(
@NonNull final HssCallAttempt attempt,
@Nullable final TransactionBody syntheticBody,
@NonNull final DispatchGasCalculator dispatchGasCalculator,
@NonNull final Set<Key> authorizingKeys) {
this(
attempt.enhancement(),
attempt.systemContractGasCalculator(),
attempt.addressIdConverter().convertSender(attempt.senderAddress()),
syntheticBody,
attempt.defaultVerificationStrategy(),
dispatchGasCalculator,
authorizingKeys);
}

/**
* More general constructor, for cases where perhaps a custom {@link VerificationStrategy} is needed.
*
* @param enhancement the enhancement to use
* @param senderId the id of the spender
* @param syntheticBody the synthetic body to dispatch
* @param verificationStrategy the verification strategy to use
* @param dispatchGasCalculator the dispatch gas calculator to use
*/
// too many parameters
@SuppressWarnings("java:S107")
public DispatchForResponseCodeHssCall(
@NonNull final HederaWorldUpdater.Enhancement enhancement,
@NonNull final SystemContractGasCalculator gasCalculator,
@NonNull final AccountID senderId,
@Nullable final TransactionBody syntheticBody,
@NonNull final VerificationStrategy verificationStrategy,
@NonNull final DispatchGasCalculator dispatchGasCalculator,
@NonNull final Set<Key> authorizingKeys) {
super(gasCalculator, enhancement, false);
this.senderId = Objects.requireNonNull(senderId);
this.syntheticBody = syntheticBody;
this.verificationStrategy = Objects.requireNonNull(verificationStrategy);
this.dispatchGasCalculator = Objects.requireNonNull(dispatchGasCalculator);
this.authorizingKeys = authorizingKeys;
}

@Override
public @NonNull PricedResult execute(@NonNull final MessageFrame frame) {
if (syntheticBody == null) {
return gasOnly(
haltResult(
ERROR_DECODING_PRECOMPILE_INPUT,
contractsConfigOf(frame).precompileHtsDefaultGasCost()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a config for precompileHssDefaultGasGost? (Hss)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is worth it to have a different gas cost for this for each system contract.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe should be renamed?

INVALID_TRANSACTION_BODY,
false);
}
final var recordBuilder = systemContractOperations()
.dispatch(
syntheticBody,
verificationStrategy,
senderId,
ContractCallStreamBuilder.class,
authorizingKeys);
final var gasRequirement =
dispatchGasCalculator.gasRequirement(syntheticBody, gasCalculator, enhancement, senderId);
var status = recordBuilder.status();
if (status != SUCCESS) {
recordBuilder.status(status);

Check warning on line 128 in hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hss/DispatchForResponseCodeHssCall.java

View check run for this annotation

Codecov / codecov/patch

hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hss/DispatchForResponseCodeHssCall.java#L128

Added line #L128 was not covered by tests
}
return completionWith(gasRequirement, recordBuilder, encodedRc(recordBuilder.status()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@
package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hss;

import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.isLongZeroAddress;
import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.maybeMissingNumberOf;
import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.numberOfLongZero;
import static java.util.Objects.requireNonNull;

import com.esaulpaugh.headlong.abi.Function;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.ScheduleID;
import com.hedera.hapi.node.state.schedule.Schedule;
import com.hedera.node.app.service.contract.impl.exec.gas.SystemContractGasCalculator;
Expand Down Expand Up @@ -51,6 +53,9 @@ public class HssCallAttempt extends AbstractCallAttempt<HssCallAttempt> {
@Nullable
private final Schedule redirectScheduleTxn;

@NonNull
private final Address originatorAddress;

// too many parameters
@SuppressWarnings("java:S107")
public HssCallAttempt(
Expand All @@ -63,7 +68,8 @@ public HssCallAttempt(
@NonNull final VerificationStrategies verificationStrategies,
@NonNull final SystemContractGasCalculator gasCalculator,
@NonNull final List<CallTranslator<HssCallAttempt>> callTranslators,
final boolean isStaticCall) {
final boolean isStaticCall,
@NonNull final Address originatorAddress) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be moved up, perhaps under senderAddress?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Michael, convinced me that originatorAddress is not really needed. I will remove it.

super(
input,
senderAddress,
Expand All @@ -82,6 +88,7 @@ public HssCallAttempt(
} else {
this.redirectScheduleTxn = null;
}
this.originatorAddress = requireNonNull(originatorAddress);
}

@Override
Expand Down Expand Up @@ -149,4 +156,14 @@ public boolean isScheduleRedirect() {
}
return null;
}

/**
* Returns the AccountID of the originator of this call.
*
* @return the AccountID of the originator of this call
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth explaining here how the originator (*) is different from the sender? Term "originator" doesn't appear in HIP-755 and of course there is no HIP for the schedule service itself.

(*) Using me myself as a proxy for the typical reader: I don't know the difference.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing - see above comment

*/
public @NonNull AccountID originatorAccount() {
final var number = maybeMissingNumberOf(originatorAddress, this.enhancement.nativeOperations());
return AccountID.newBuilder().accountNum(number).build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public HssCallFactory(
verificationStrategies,
systemContractGasCalculatorOf(frame),
callTranslators,
frame.isStatic());
frame.isStatic(),
frame.getOriginatorAddress());
}
}

This file was deleted.

Loading
Loading