Skip to content

Commit

Permalink
feat: implement signSchedule(address, bytes) HSS function (#17260)
Browse files Browse the repository at this point in the history
Signed-off-by: Luke Lee <[email protected]>
  • Loading branch information
lukelee-sl authored Jan 9, 2025
1 parent fe5bd5c commit 5a5b7c6
Show file tree
Hide file tree
Showing 16 changed files with 856 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
* Copyright (C) 2024-2025 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.
Expand Down Expand Up @@ -83,6 +83,11 @@ public record ContractsConfig(
@ConfigProperty(value = "systemContract.scheduleService.signSchedule.enabled", defaultValue = "true")
@NetworkProperty
boolean systemContractSignScheduleEnabled,
@ConfigProperty(
value = "systemContract.scheduleService.signSchedule.from.contract.enabled",
defaultValue = "true")
@NetworkProperty
boolean systemContractSignScheduleFromContractEnabled,
@ConfigProperty(value = "systemContract.scheduleService.authorizeSchedule.enabled", defaultValue = "true")
@NetworkProperty
boolean systemContractAuthorizeScheduleEnabled,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
* Copyright (C) 2023-2025 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.
Expand Down Expand Up @@ -35,6 +35,7 @@
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.common.CallTranslator;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter;
import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater;
import com.hedera.node.app.spi.signatures.SignatureVerifier;
import com.swirlds.config.api.Configuration;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
Expand All @@ -55,6 +56,9 @@ public class HssCallAttempt extends AbstractCallAttempt<HssCallAttempt> {
@Nullable
private final Schedule redirectScheduleTxn;

@NonNull
private final SignatureVerifier signatureVerifier;

// too many parameters
@SuppressWarnings("java:S107")
public HssCallAttempt(
Expand All @@ -65,6 +69,7 @@ public HssCallAttempt(
@NonNull final Configuration configuration,
@NonNull final AddressIdConverter addressIdConverter,
@NonNull final VerificationStrategies verificationStrategies,
@NonNull final SignatureVerifier signatureVerifier,
@NonNull final SystemContractGasCalculator gasCalculator,
@NonNull final List<CallTranslator<HssCallAttempt>> callTranslators,
final boolean isStaticCall) {
Expand All @@ -86,6 +91,7 @@ public HssCallAttempt(
} else {
this.redirectScheduleTxn = null;
}
this.signatureVerifier = signatureVerifier;
}

@Override
Expand Down Expand Up @@ -195,4 +201,13 @@ public Set<Key> getKeysForContractSender() {
.build());
}
}

/*
* Returns the {@link SignatureVerifier} used for this call.
*
* @return the {@link SignatureVerifier} used for this call
*/
public @NonNull SignatureVerifier signatureVerifier() {
return signatureVerifier;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2023-2024 Hedera Hashgraph, LLC
* Copyright (C) 2023-2025 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.
Expand Down Expand Up @@ -45,6 +45,7 @@ public class HssCallFactory implements CallFactory<HssCallAttempt> {
private final CallAddressChecks addressChecks;
private final VerificationStrategies verificationStrategies;
private final List<CallTranslator<HssCallAttempt>> callTranslators;
private final SignatureVerifier signatureVerifier;

@Inject
public HssCallFactory(
Expand All @@ -56,6 +57,7 @@ public HssCallFactory(
this.syntheticIds = requireNonNull(syntheticIds);
this.addressChecks = requireNonNull(addressChecks);
this.verificationStrategies = requireNonNull(verificationStrategies);
this.signatureVerifier = requireNonNull(signatureVerifier);
this.callTranslators = requireNonNull(callTranslators);
}

Expand Down Expand Up @@ -83,6 +85,7 @@ public HssCallFactory(
configOf(frame),
syntheticIds.converterFor(enhancement.nativeOperations()),
verificationStrategies,
signatureVerifier,
systemContractGasCalculatorOf(frame),
callTranslators,
frame.isStatic());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 Hedera Hashgraph, LLC
* Copyright (C) 2024-2025 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.
Expand All @@ -17,15 +17,23 @@
package com.hedera.node.app.service.contract.impl.exec.systemcontracts.hss.signschedule;

import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SCHEDULE_ID;
import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSACTION_BODY;
import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.explicitFromHeadlong;
import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.numberOfLongZero;
import static com.hedera.node.app.service.contract.impl.utils.SignatureMapUtils.preprocessEcdsaSignatures;
import static com.hedera.node.app.service.contract.impl.utils.SystemContractUtils.messageFromScheduleId;
import static com.hedera.node.app.spi.workflows.HandleException.validateTrue;
import static com.hedera.pbj.runtime.io.buffer.Bytes.wrap;
import static java.util.Objects.requireNonNull;

import com.esaulpaugh.headlong.abi.Address;
import com.esaulpaugh.headlong.abi.Function;
import com.esaulpaugh.headlong.abi.Tuple;
import com.google.common.annotations.VisibleForTesting;
import com.hedera.hapi.node.base.AccountID;
import com.hedera.hapi.node.base.Key;
import com.hedera.hapi.node.base.ScheduleID;
import com.hedera.hapi.node.base.SignatureMap;
import com.hedera.hapi.node.scheduled.ScheduleSignTransactionBody;
import com.hedera.hapi.node.transaction.TransactionBody;
import com.hedera.node.app.service.contract.impl.exec.gas.DispatchType;
Expand All @@ -36,8 +44,16 @@
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hss.HssCallAttempt;
import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes;
import com.hedera.node.app.service.contract.impl.hevm.HederaWorldUpdater;
import com.hedera.node.app.spi.signatures.SignatureVerifier.MessageType;
import com.hedera.node.app.spi.signatures.SignatureVerifier.SimpleKeyStatus;
import com.hedera.node.app.spi.workflows.HandleException;
import com.hedera.node.config.data.ContractsConfig;
import com.hedera.pbj.runtime.ParseException;
import com.hedera.pbj.runtime.io.buffer.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;

Expand All @@ -49,6 +65,8 @@ public class SignScheduleTranslator extends AbstractCallTranslator<HssCallAttemp
public static final Function SIGN_SCHEDULE = new Function("signSchedule(address,bytes)", ReturnTypes.INT_64);
public static final Function SIGN_SCHEDULE_PROXY = new Function("signSchedule()", ReturnTypes.INT_64);
public static final Function AUTHORIZE_SCHEDULE = new Function("authorizeSchedule(address)", ReturnTypes.INT_64);
private static final int SCHEDULE_ID_INDEX = 0;
private static final int SIGNATURE_MAP_INDEX = 1;

@Inject
public SignScheduleTranslator() {
Expand All @@ -60,17 +78,21 @@ public boolean matches(@NonNull final HssCallAttempt attempt) {
requireNonNull(attempt);
final var signScheduleEnabled =
attempt.configuration().getConfigData(ContractsConfig.class).systemContractSignScheduleEnabled();
final var signScheduleFromContractEnabled = attempt.configuration()
.getConfigData(ContractsConfig.class)
.systemContractSignScheduleFromContractEnabled();
final var authorizeScheduleEnabled =
attempt.configuration().getConfigData(ContractsConfig.class).systemContractAuthorizeScheduleEnabled();
return attempt.isSelectorIfConfigEnabled(signScheduleEnabled, SIGN_SCHEDULE_PROXY)
|| attempt.isSelectorIfConfigEnabled(signScheduleFromContractEnabled, SIGN_SCHEDULE)
|| attempt.isSelectorIfConfigEnabled(authorizeScheduleEnabled, AUTHORIZE_SCHEDULE);
}

@Override
public Call callFrom(@NonNull HssCallAttempt attempt) {
final var body = bodyFor(scheduleIdFor(attempt));
return new DispatchForResponseCodeHssCall(
attempt, body, SignScheduleTranslator::gasRequirement, attempt.keySetFor());
attempt, body, SignScheduleTranslator::gasRequirement, keySetFor(attempt));
}

/**
Expand All @@ -96,7 +118,8 @@ public static long gasRequirement(
* @param scheduleID the schedule ID
* @return the transaction body
*/
private TransactionBody bodyFor(@NonNull ScheduleID scheduleID) {
@VisibleForTesting
public TransactionBody bodyFor(@NonNull ScheduleID scheduleID) {
requireNonNull(scheduleID);
return TransactionBody.newBuilder()
.scheduleSign(ScheduleSignTransactionBody.newBuilder()
Expand All @@ -112,9 +135,12 @@ private TransactionBody bodyFor(@NonNull ScheduleID scheduleID) {
* @param attempt the call attempt
* @return the schedule ID
*/
private ScheduleID scheduleIdFor(@NonNull HssCallAttempt attempt) {
@VisibleForTesting
public ScheduleID scheduleIdFor(@NonNull HssCallAttempt attempt) {
requireNonNull(attempt);
if (attempt.isSelector(SIGN_SCHEDULE_PROXY)) {
return getScheduleIDForSignScheduleProxy(attempt);
} else if (attempt.isSelector(SIGN_SCHEDULE)) {
return getScheduleIDForSignSchedule(attempt);
} else if (attempt.isSelector(AUTHORIZE_SCHEDULE)) {
return getScheduleIDForAuthorizeSchedule(attempt);
Expand All @@ -123,17 +149,96 @@ private ScheduleID scheduleIdFor(@NonNull HssCallAttempt attempt) {
}

private static ScheduleID getScheduleIDForSignSchedule(@NonNull HssCallAttempt attempt) {
final var scheduleID = attempt.redirectScheduleId();
validateTrue(scheduleID != null, INVALID_SCHEDULE_ID);
return attempt.redirectScheduleId();
final var call = SIGN_SCHEDULE.decodeCall(attempt.inputBytes());
return getScheduleIDFromCall(attempt, call);
}

private static ScheduleID getScheduleIDForAuthorizeSchedule(@NonNull HssCallAttempt attempt) {
final var call = AUTHORIZE_SCHEDULE.decodeCall(attempt.inputBytes());
final Address scheduleAddress = call.get(0);
return getScheduleIDFromCall(attempt, call);
}

private static @Nullable ScheduleID getScheduleIDFromCall(@NonNull HssCallAttempt attempt, Tuple call) {
final Address scheduleAddress = call.get(SCHEDULE_ID_INDEX);
final var number = numberOfLongZero(explicitFromHeadlong(scheduleAddress));
final var schedule = attempt.enhancement().nativeOperations().getSchedule(number);
validateTrue(schedule != null, INVALID_SCHEDULE_ID);
return schedule.scheduleId();
}

private static ScheduleID getScheduleIDForSignScheduleProxy(@NonNull HssCallAttempt attempt) {
final var scheduleID = attempt.redirectScheduleId();
validateTrue(scheduleID != null, INVALID_SCHEDULE_ID);
return attempt.redirectScheduleId();
}

/**
* Extracts the key set for a {@code signSchedule(address, bytes)} call. Otherwise, delegates to the call attempt.
*
* @param attempt the call attempt
* @return the key set
*/
private Set<Key> keySetFor(@NonNull HssCallAttempt attempt) {
requireNonNull(attempt);

// Check for the signSchedule(address, bytes) call. This form of key set extraction will never be used
// for the HIP 756 calls and thus we treat it separately.
if (attempt.isSelector(SIGN_SCHEDULE)) {
return getKeyForSignSchedule(attempt);
}
return attempt.keySetFor();
}

@VisibleForTesting
@NonNull
public static Set<Key> getKeyForSignSchedule(@NonNull HssCallAttempt attempt) {
requireNonNull(attempt);
final Set<Key> keys = new HashSet<>();
final var call = SIGN_SCHEDULE.decodeCall(attempt.inputBytes());
final var scheduleId = requireNonNull(getScheduleIDFromCall(attempt, call));

final var message = messageFromScheduleId(scheduleId);

final var signatureBlob = (byte[]) call.get(SIGNATURE_MAP_INDEX);
try {
final var chainId =
attempt.configuration().getConfigData(ContractsConfig.class).chainId();
final var sigMap = preprocessEcdsaSignatures(
requireNonNull(SignatureMap.PROTOBUF.parse(wrap(signatureBlob))), chainId);
for (var sigPair : sigMap.sigPair()) {
// For ED25519 and ECDSA keys, verify the key and add it to the key set if verified
if (sigPair.hasEd25519()) {
var key = Key.newBuilder().ed25519(sigPair.pubKeyPrefix()).build();
if (isVerifiedSignature(attempt, key, message, sigMap)) {
keys.add(key);
}
}
if (sigPair.hasEcdsaSecp256k1()) {
var key = Key.newBuilder()
.ecdsaSecp256k1(sigPair.pubKeyPrefix())
.build();
if (isVerifiedSignature(attempt, key, message, sigMap)) {
keys.add(key);
}
}
}
} catch (@NonNull final ParseException | NullPointerException | IllegalArgumentException ex) {
throw new HandleException(INVALID_TRANSACTION_BODY);
}
return keys;
}

/**
* Verifies the signature for a given key.
* @param attempt the call attempt
* @param key the key to verify
* @param message the message to verify - concatenation of realm, shard, and schedule numbers
* @param sigMap the signature map used for verification
* @return true if the signature is verified, false otherwise
*/
private static boolean isVerifiedSignature(
@NonNull HssCallAttempt attempt, Key key, Bytes message, SignatureMap sigMap) {
return attempt.signatureVerifier()
.verifySignature(key, message, MessageType.RAW, sigMap, ky -> SimpleKeyStatus.ONLY_IF_CRYPTO_SIG_VALID);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2025 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.utils;

import static java.util.Objects.requireNonNull;

import com.hedera.hapi.node.base.SignatureMap;
import com.hedera.hapi.node.base.SignaturePair;
import com.hedera.hapi.node.base.SignaturePair.SignatureOneOfType;
import com.hedera.node.app.spi.signatures.SignatureVerifier;
import com.hedera.pbj.runtime.OneOf;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.List;

/**
* Some utility methods that are useful for processing system contracts.
*/
public class SignatureMapUtils {
private SignatureMapUtils() {
// Utility class
throw new UnsupportedOperationException("Utility Class");
}

/**
* When a signature map is passed to a system contract function and contains ECDSA signatures, before the signatures
* can be verified with a {@link SignatureVerifier}, they must be preprocessed in the following way:
* (1) If the v value is greater than 35, it must be checked to see if it matches the chain ID per EIP 155
* (2) Strip the v value from the public key as it is not needed for verification
* @param sigMap
* @return a new SignatureMap with the ECDSA signatures preprocessed
*/
public static SignatureMap preprocessEcdsaSignatures(@NonNull final SignatureMap sigMap, final int chainId) {
final List<SignaturePair> newPairs = new ArrayList<>();
for (var spair : sigMap.sigPair()) {
if (spair.hasEcdsaSecp256k1()) {
final var ecSig = requireNonNull(spair.ecdsaSecp256k1());
if (ecSig.length() > 64) {
if (!validChainId(ecSig.toByteArray(), chainId)) {
throw new IllegalArgumentException("v value in ECDSA signature does not match chain ID");
}
spair = new SignaturePair(
spair.pubKeyPrefix(), new OneOf<>(SignatureOneOfType.ECDSA_SECP256K1, ecSig.slice(0, 64)));
}
}
newPairs.add(spair);
}
return new SignatureMap(newPairs);
}

/**
* Check that the v value in an ECDSA signature matches the chain ID if it is greater than 35 per EIP 155
* @param ecSig
* @param chainId
* @return true if the v value matches the chain ID or is not relevant, false otherwise
*/
public static boolean validChainId(final byte[] ecSig, final int chainId) {
int v = 0;
for (int i = 64; i < ecSig.length; i++) {
v <<= 8;
v |= (ecSig[i] & 0xFF);
}
if (v >= 35) {
// See EIP 155 - https://eips.ethereum.org/EIPS/eip-155
final var chainIdParityZero = 35 + (chainId * 2);
return v == chainIdParityZero || v == chainIdParityZero + 1;
}
return true;
}
}
Loading

0 comments on commit 5a5b7c6

Please sign in to comment.