From ad53b0cbe3de30708da20018a063fbd1f019b169 Mon Sep 17 00:00:00 2001 From: Michael Tinker Date: Tue, 14 Jan 2025 10:03:47 -0600 Subject: [PATCH] chore: (0.58) Support custom `Operation`s in standalone executor (#17354) Signed-off-by: Michael Tinker --- hedera-node/hedera-app/build.gradle.kts | 18 ++- .../standalone/TransactionExecutors.java | 139 ++++++++++++++++- .../standalone/TransactionExecutorsTest.java | 143 ++++++++++++++++-- .../resources/initcode/EmitBlockTimestamp.bin | 1 + .../impl/ContractServiceComponent.java | 9 +- .../contract/impl/ContractServiceImpl.java | 14 +- .../contract/impl/annotations/CustomOps.java | 38 +++++ .../contract/impl/exec/v030/V030Module.java | 7 +- .../contract/impl/exec/v034/V034Module.java | 7 +- .../contract/impl/exec/v038/V038Module.java | 7 +- .../contract/impl/exec/v046/V046Module.java | 7 +- .../contract/impl/exec/v050/V050Module.java | 7 +- .../contract/impl/exec/v051/V051Module.java | 7 +- 13 files changed, 372 insertions(+), 32 deletions(-) create mode 100644 hedera-node/hedera-app/src/test/resources/initcode/EmitBlockTimestamp.bin create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/annotations/CustomOps.java diff --git a/hedera-node/hedera-app/build.gradle.kts b/hedera-node/hedera-app/build.gradle.kts index ebcf3e45a546..970a194f595d 100644 --- a/hedera-node/hedera-app/build.gradle.kts +++ b/hedera-node/hedera-app/build.gradle.kts @@ -1,4 +1,19 @@ -// SPDX-License-Identifier: Apache-2.0 +/* + * 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. + */ + plugins { id("org.hiero.gradle.module.library") id("org.hiero.gradle.feature.benchmark") @@ -39,6 +54,7 @@ testModuleInfo { requires("org.junit.jupiter.params") requires("org.mockito") requires("org.mockito.junit.jupiter") + requires("tuweni.bytes") requires("uk.org.webcompere.systemstubs.core") requires("uk.org.webcompere.systemstubs.jupiter") requiresStatic("com.github.spotbugs.annotations") diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java index 0977fa2af22a..eefc89a5354d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/standalone/TransactionExecutors.java @@ -19,6 +19,7 @@ import static com.hedera.node.app.service.token.impl.handlers.BaseCryptoHandler.asAccount; import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; import static com.hedera.node.app.workflows.standalone.impl.NoopVerificationStrategies.NOOP_VERIFICATION_STRATEGIES; +import static java.util.Objects.requireNonNull; import com.hedera.node.app.Hedera; import com.hedera.node.app.config.BootstrapConfigProviderImpl; @@ -46,12 +47,16 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import java.time.InstantSource; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; +import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.tracing.OperationTracer; /** @@ -70,21 +75,147 @@ public interface TracerBinding extends Supplier> { void runWhere(@NonNull List tracers, @NonNull Runnable runnable); } + /** + * The properties to use when creating a new {@link TransactionExecutor}. + * @param state the {@link State} to use + * @param appProperties the properties to use + * @param customTracerBinding the custom tracer binding to use + * @param customOps the custom operations to use + */ + public record Properties( + @NonNull State state, + @NonNull Map appProperties, + @Nullable TracerBinding customTracerBinding, + @NonNull Set customOps) { + /** + * Create a new {@link Builder} instance. + * @return a new {@link Builder} instance + */ + public static Builder newBuilder() { + return new Builder(); + } + + /** + * Builder for {@link Properties}. + */ + public static class Builder { + private State state; + private TracerBinding customTracerBinding; + private final Map appProperties = new HashMap<>(); + private final Set customOps = new HashSet<>(); + + /** + * Set the required {@link State} field. + */ + public Builder state(@NonNull final State state) { + this.state = requireNonNull(state); + return this; + } + + /** + * Add or override a single property. + */ + public Builder appProperty(@NonNull final String key, @NonNull final String value) { + requireNonNull(key); + requireNonNull(value); + this.appProperties.put(key, value); + return this; + } + + /** + * Add/override multiple properties at once. + */ + public Builder appProperties(@NonNull final Map properties) { + requireNonNull(properties); + this.appProperties.putAll(properties); + return this; + } + + /** + * Set the optional {@link TracerBinding}. + */ + public Builder customTracerBinding(@Nullable final TracerBinding customTracerBinding) { + this.customTracerBinding = customTracerBinding; + return this; + } + + /** + * Set the custom operations in bulk. + */ + public Builder customOps(@NonNull final Set customOps) { + requireNonNull(customOps); + this.customOps.addAll(customOps); + return this; + } + + /** + * Add a single custom operation. + */ + public Builder addCustomOp(@NonNull final Operation customOp) { + requireNonNull(customOp); + this.customOps.add(customOp); + return this; + } + + /** + * Build and return the immutable {@link Properties} record. + */ + public Properties build() { + if (state == null) { + throw new IllegalStateException("State must not be null"); + } + return new Properties(state, Map.copyOf(appProperties), customTracerBinding, Set.copyOf(customOps)); + } + } + } + /** * Creates a new {@link TransactionExecutor} based on the given {@link State} and properties. + * @param properties the properties to use for the executor + * @return a new {@link TransactionExecutor} + */ + public TransactionExecutor newExecutor(@NonNull final Properties properties) { + requireNonNull(properties); + return newExecutor( + properties.state(), + properties.appProperties(), + properties.customTracerBinding(), + properties.customOps()); + } + + /** + * Creates a new {@link TransactionExecutor} based on the given {@link State} and properties. + * Prefer * * @param state the {@link State} to create the executor from * @param properties the properties to use for the executor * @param customTracerBinding if not null, the tracer binding to use * @return a new {@link TransactionExecutor} */ + @Deprecated(since = "0.58") public TransactionExecutor newExecutor( @NonNull final State state, @NonNull final Map properties, @Nullable final TracerBinding customTracerBinding) { + return newExecutor(state, properties, customTracerBinding, Set.of()); + } + + /** + * Creates a new {@link TransactionExecutor}. + * @param state the {@link State} to use + * @param properties the properties to use + * @param customTracerBinding the custom tracer binding to use + * @param customOps the custom operations to use + * @return a new {@link TransactionExecutor} + */ + private TransactionExecutor newExecutor( + @NonNull final State state, + @NonNull final Map properties, + @Nullable final TracerBinding customTracerBinding, + @NonNull final Set customOps) { final var tracerBinding = customTracerBinding != null ? customTracerBinding : DefaultTracerBinding.DEFAULT_TRACER_BINDING; - final var executor = newExecutorComponent(state, properties, tracerBinding); + final var executor = newExecutorComponent(state, properties, tracerBinding, customOps); executor.initializer().accept(state); executor.stateNetworkInfo().initFrom(state); final var exchangeRateManager = executor.exchangeRateManager(); @@ -102,7 +233,8 @@ public TransactionExecutor newExecutor( private ExecutorComponent newExecutorComponent( @NonNull final State state, @NonNull final Map properties, - @NonNull final TracerBinding tracerBinding) { + @NonNull final TracerBinding tracerBinding, + @NonNull final Set customOps) { final var bootstrapConfigProvider = new BootstrapConfigProviderImpl(); final var configProvider = new ConfigProviderImpl(false, null, properties); final AtomicReference componentRef = new AtomicReference<>(); @@ -128,7 +260,8 @@ private ExecutorComponent newExecutorComponent( new TssLibraryImpl(appContext), ForkJoinPool.commonPool(), NO_OP_METRICS); - final var contractService = new ContractServiceImpl(appContext, NOOP_VERIFICATION_STRATEGIES, tracerBinding); + final var contractService = + new ContractServiceImpl(appContext, NOOP_VERIFICATION_STRATEGIES, tracerBinding, customOps); final var fileService = new FileServiceImpl(); final var scheduleService = new ScheduleServiceImpl(); final var component = DaggerExecutorComponent.builder() diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java index f0a22cbf8739..6456994f07a7 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/standalone/TransactionExecutorsTest.java @@ -17,12 +17,14 @@ package com.hedera.node.app.workflows.standalone; import static com.hedera.node.app.fixtures.AppTestBase.DEFAULT_CONFIG; +import static com.hedera.node.app.records.schemas.V0490BlockRecordSchema.BLOCK_INFO_STATE_KEY; import static com.hedera.node.app.spi.AppContext.Gossip.UNAVAILABLE_GOSSIP; import static com.hedera.node.app.spi.key.KeyUtils.IMMUTABILITY_SENTINEL_KEY; import static com.hedera.node.app.util.FileUtilities.createFileID; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.DEFAULT_NODE_INFO; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.MAX_SIGNED_TXN_SIZE_PROPERTY; import static com.hedera.node.app.workflows.standalone.TransactionExecutors.TRANSACTION_EXECUTORS; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -38,6 +40,7 @@ import com.hedera.hapi.node.contract.ContractCallTransactionBody; import com.hedera.hapi.node.contract.ContractCreateTransactionBody; import com.hedera.hapi.node.file.FileCreateTransactionBody; +import com.hedera.hapi.node.state.blockrecords.BlockInfo; import com.hedera.hapi.node.state.file.File; import com.hedera.hapi.node.transaction.ThrottleDefinitions; import com.hedera.hapi.node.transaction.TransactionBody; @@ -104,12 +107,17 @@ import java.time.InstantSource; import java.util.List; import java.util.Map; -import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.Spliterators; import java.util.function.Function; import java.util.stream.StreamSupport; +import org.apache.tuweni.bytes.Bytes32; +import org.hyperledger.besu.evm.EVM; +import org.hyperledger.besu.evm.frame.MessageFrame; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; +import org.hyperledger.besu.evm.operation.AbstractOperation; +import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.tracing.StandardJsonTracer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -142,6 +150,8 @@ public class TransactionExecutorsTest { ContractID.newBuilder().contractNum(1002).build(); private static final com.esaulpaugh.headlong.abi.Function PICK_FUNCTION = new com.esaulpaugh.headlong.abi.Function("pick()", "(uint32)"); + private static final com.esaulpaugh.headlong.abi.Function GET_LAST_BLOCKHASH_FUNCTION = + new com.esaulpaugh.headlong.abi.Function("getLastBlockHash()", "(bytes32)"); private static final String EXPECTED_TRACE_START = "{\"pc\":0,\"op\":96,\"gas\":\"0x13458\",\"gasCost\":\"0x3\",\"memSize\":0,\"depth\":1,\"refund\":0,\"opName\":\"PUSH1\"}"; @@ -154,6 +164,15 @@ public class TransactionExecutorsTest { @Mock private StartupNetworks startupNetworks; + @Mock + private TransactionExecutors.TracerBinding tracerBinding; + + @Mock + private GasCalculator gasCalculator; + + @Mock + private State state; + @Test void executesTransactionsAsExpected() { final var overrides = Map.of("hedera.transaction.maxMemoUtf8Bytes", "101"); @@ -161,7 +180,10 @@ void executesTransactionsAsExpected() { final var state = genesisState(overrides); // Get a standalone executor based on this state, with an override to allow slightly longer memos - final var executor = TRANSACTION_EXECUTORS.newExecutor(state, overrides, null); + final var executor = TRANSACTION_EXECUTORS.newExecutor(TransactionExecutors.Properties.newBuilder() + .state(state) + .appProperties(overrides) + .build()); // Execute a FileCreate that uploads the initcode for the Multipurpose.sol contract final var uploadOutput = executor.execute(uploadMultipurposeInitcode(), Instant.EPOCH); @@ -169,7 +191,7 @@ void executesTransactionsAsExpected() { assertThat(uploadReceipt.fileIDOrThrow()).isEqualTo(EXPECTED_INITCODE_ID); // Execute a ContractCreate that creates a Multipurpose contract instance - final var creationOutput = executor.execute(createMultipurposeContract(), Instant.EPOCH); + final var creationOutput = executor.execute(createContract(), Instant.EPOCH); final var creationReceipt = creationOutput.getFirst().transactionRecord().receiptOrThrow(); assertThat(creationReceipt.contractIDOrThrow()).isEqualTo(EXPECTED_CONTRACT_ID); @@ -189,6 +211,44 @@ void executesTransactionsAsExpected() { assertThat(stringWriter.toString()).startsWith(EXPECTED_TRACE_START); } + @Test + void usesOverrideBlockhashOpAsExpected() { + final var state = genesisState(Map.of()); + final var writableStates = state.getWritableStates(BlockRecordService.NAME); + final var blockInfoSingleton = writableStates.getSingleton(BLOCK_INFO_STATE_KEY); + blockInfoSingleton.put(requireNonNull(blockInfoSingleton.get()) + .copyBuilder() + .lastBlockNumber(666L) + .build()); + ((CommittableWritableStates) writableStates).commit(); + + // Use a custom operation that overrides the BLOCKHASH operation + final var customOp = new CustomBlockhashOperation(); + final var executor = TRANSACTION_EXECUTORS.newExecutor(TransactionExecutors.Properties.newBuilder() + .state(state) + .addCustomOp(customOp) + .appProperty("hedera.transaction.maxMemoUtf8Bytes", "101") + .build()); + + final var uploadOutput = executor.execute(uploadEmitBlockTimestampInitcode(), Instant.EPOCH); + final var uploadReceipt = uploadOutput.getFirst().transactionRecord().receiptOrThrow(); + assertThat(uploadReceipt.fileIDOrThrow()).isEqualTo(EXPECTED_INITCODE_ID); + + final var creationOutput = executor.execute(createContract(), Instant.EPOCH); + final var creationReceipt = + creationOutput.getFirst().transactionRecord().receiptOrThrow(); + assertThat(creationReceipt.contractIDOrThrow()).isEqualTo(EXPECTED_CONTRACT_ID); + + final var callOutput = executor.execute(contractCallGetLastBlockHashFunction(), Instant.EPOCH); + final var callRecord = callOutput.getFirst().transactionRecord(); + final var callResult = callRecord.contractCallResultOrThrow().contractCallResult(); + final byte[] blockHash = GET_LAST_BLOCKHASH_FUNCTION + .getOutputs() + .decode(callResult.toByteArray()) + .get(0); + assertThat(Bytes32.wrap(blockHash)).isEqualTo(CustomBlockhashOperation.FAKE_BLOCK_HASH); + } + @Test void respectsOverrideMaxSignedTxnSize() { final var overrides = Map.of(MAX_SIGNED_TXN_SIZE_PROPERTY, "42"); @@ -196,13 +256,38 @@ void respectsOverrideMaxSignedTxnSize() { final var state = genesisState(overrides); // Get a standalone executor based on this state, with an override to allow slightly longer memos - final var executor = TRANSACTION_EXECUTORS.newExecutor(state, overrides, null); + final var executor = TRANSACTION_EXECUTORS.newExecutor(TransactionExecutors.Properties.newBuilder() + .state(state) + .appProperties(overrides) + .build()); // With just 42 bytes allowed for signed transactions, the executor will not be able to construct // a dispatch for the transaction and throw an exception assertThrows(NullPointerException.class, () -> executor.execute(uploadMultipurposeInitcode(), Instant.EPOCH)); } + @Test + void propertiesBuilderRequiresNonNullState() { + assertThrows(IllegalStateException.class, () -> TransactionExecutors.Properties.newBuilder() + .build()); + } + + @Test + void propertiesBuilderBulkOptionsAsExpected() { + final var customOps = Set.of(new CustomBlockhashOperation()); + final var appProperties = Map.of("hedera.transaction.maxMemoUtf8Bytes", "101"); + final var properties = TransactionExecutors.Properties.newBuilder() + .customOps(customOps) + .appProperties(appProperties) + .customTracerBinding(tracerBinding) + .state(state) + .build(); + + assertThat(properties.customOps()).isEqualTo(customOps); + assertThat(properties.appProperties()).isEqualTo(appProperties); + assertThat(properties.customTracerBinding()).isEqualTo(tracerBinding); + } + private TransactionBody contractCallMultipurposePickFunction() { final var callData = PICK_FUNCTION.encodeCallWithArgs(); return newBodyBuilder() @@ -214,7 +299,18 @@ private TransactionBody contractCallMultipurposePickFunction() { .build(); } - private TransactionBody createMultipurposeContract() { + private TransactionBody contractCallGetLastBlockHashFunction() { + final var callData = GET_LAST_BLOCKHASH_FUNCTION.encodeCallWithArgs(); + return newBodyBuilder() + .contractCall(ContractCallTransactionBody.newBuilder() + .contractID(EXPECTED_CONTRACT_ID) + .functionParameters(Bytes.wrap(callData.array())) + .gas(GAS) + .build()) + .build(); + } + + private TransactionBody createContract() { final var maxLifetime = DEFAULT_CONFIG.getConfigData(EntitiesConfig.class).maxLifetime(); return newBodyBuilder() @@ -238,6 +334,18 @@ private TransactionBody uploadMultipurposeInitcode() { .build(); } + private TransactionBody uploadEmitBlockTimestampInitcode() { + final var maxLifetime = + DEFAULT_CONFIG.getConfigData(EntitiesConfig.class).maxLifetime(); + return newBodyBuilder() + .fileCreate(FileCreateTransactionBody.newBuilder() + .contents(resourceAsBytes("initcode/EmitBlockTimestamp.bin")) + .keys(IMMUTABILITY_SENTINEL_KEY.keyListOrThrow()) + .expirationTime(new Timestamp(maxLifetime, 0)) + .build()) + .build(); + } + private TransactionBody.Builder newBodyBuilder() { final var minValidDuration = DEFAULT_CONFIG.getConfigData(HederaConfig.class).transactionMinValidDuration(); @@ -268,7 +376,7 @@ private State genesisState(@NonNull final Map overrides) { () -> NO_OP_METRICS, new AppThrottleFactory( () -> config, () -> state, () -> ThrottleDefinitions.DEFAULT, ThrottleAccumulator::new)); - registerServices(appContext, config, servicesRegistry); + registerServices(appContext, servicesRegistry); final var migrator = new FakeServiceMigrator(); final var bootstrapConfig = new BootstrapConfigProviderImpl().getConfiguration(); migrator.doMigrations( @@ -313,9 +421,7 @@ private Map> genesisContentProviders( } private void registerServices( - @NonNull final AppContext appContext, - @NonNull final Configuration config, - @NonNull final ServicesRegistry servicesRegistry) { + @NonNull final AppContext appContext, @NonNull final ServicesRegistry servicesRegistry) { // Register all service schema RuntimeConstructable factories before platform init Set.of( new EntityIdService(), @@ -399,7 +505,7 @@ public void updateFrom(final State state) { private Bytes resourceAsBytes(@NonNull final String loc) { try { try (final var in = TransactionExecutorsTest.class.getClassLoader().getResourceAsStream(loc)) { - final var bytes = Objects.requireNonNull(in).readAllBytes(); + final var bytes = requireNonNull(in).readAllBytes(); return Bytes.wrap(bytes); } } catch (IOException e) { @@ -429,4 +535,21 @@ public static Bytes getCertBytes(X509Certificate certificate) { throw new RuntimeException(e); } } + + private class CustomBlockhashOperation extends AbstractOperation { + private static final OperationResult ONLY_RESULT = new Operation.OperationResult(0L, null); + private static final Bytes32 FAKE_BLOCK_HASH = Bytes32.fromHexString("0x1234567890"); + + protected CustomBlockhashOperation() { + super(64, "BLOCKHASH", 1, 1, gasCalculator); + } + + @Override + public OperationResult execute(@NonNull final MessageFrame frame, @NonNull final EVM evm) { + // This stack item has the requested block number, ignore it + frame.popStackItem(); + frame.pushStackItem(FAKE_BLOCK_HASH); + return ONLY_RESULT; + } + } } diff --git a/hedera-node/hedera-app/src/test/resources/initcode/EmitBlockTimestamp.bin b/hedera-node/hedera-app/src/test/resources/initcode/EmitBlockTimestamp.bin new file mode 100644 index 000000000000..dbc109bf669e --- /dev/null +++ b/hedera-node/hedera-app/src/test/resources/initcode/EmitBlockTimestamp.bin @@ -0,0 +1 @@ +608060405234801561001057600080fd5b506104be806100206000396000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c806327e86d6e146100515780632e4c1d021461006f578063672829a614610079578063e7dc4e2214610098575b600080fd5b6100596100b6565b6040516100669190610227565b60405180910390f35b6100776100cb565b005b61008161014a565b60405161008f92919061025b565b60405180910390f35b6100a0610164565b6040516100ad9190610342565b60405180910390f35b60006001436100c59190610393565b40905090565b7fdb7bcd08f1cb9a081d4a97aa8df2caca5caf3948df72cf63559bd93e2f44ffed426040516100fa91906103c7565b60405180910390a160004390506000814090507f4977e6b2d0d386b4fd3201b04b268b23d9e2ddcf6cab18a51ce2cc2978adc131828260405161013e92919061025b565b60405180910390a15050565b60008060014361015a9190610393565b9150814090509091565b6060600060ff9050600061010067ffffffffffffffff81111561018a576101896103e2565b5b6040519080825280602002602001820160405280156101b85781602001602082028036833780820191505090505b50905060005b828110156102055780436101d29190610393565b408282815181106101e6576101e5610411565b5b60200260200101818152505080806101fd90610440565b9150506101be565b50809250505090565b6000819050919050565b6102218161020e565b82525050565b600060208201905061023c6000830184610218565b92915050565b6000819050919050565b61025581610242565b82525050565b6000604082019050610270600083018561024c565b61027d6020830184610218565b9392505050565b600081519050919050565b600082825260208201905092915050565b6000819050602082019050919050565b6102b98161020e565b82525050565b60006102cb83836102b0565b60208301905092915050565b6000602082019050919050565b60006102ef82610284565b6102f9818561028f565b9350610304836102a0565b8060005b8381101561033557815161031c88826102bf565b9750610327836102d7565b925050600181019050610308565b5085935050505092915050565b6000602082019050818103600083015261035c81846102e4565b905092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061039e82610242565b91506103a983610242565b92508282039050818111156103c1576103c0610364565b5b92915050565b60006020820190506103dc600083018461024c565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600061044b82610242565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361047d5761047c610364565b5b60018201905091905056fea264697066735822122022024da7048e20c190d12620cf12562191616f7e8a0376df13f3f6b709e054ca64736f6c63430008120033 \ No newline at end of file diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java index 16ebea137121..2d6b2eaa9aa4 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceComponent.java @@ -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. @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl; +import com.hedera.node.app.service.contract.impl.annotations.CustomOps; import com.hedera.node.app.service.contract.impl.exec.metrics.ContractMetrics; import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategies; import com.hedera.node.app.service.contract.impl.handlers.ContractHandlers; @@ -25,8 +26,10 @@ import edu.umd.cs.findbugs.annotations.Nullable; import java.time.InstantSource; import java.util.List; +import java.util.Set; import java.util.function.Supplier; import javax.inject.Singleton; +import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.tracing.OperationTracer; /** @@ -45,6 +48,7 @@ interface Factory { * @param signatureVerifier the verifier used for signature verification * @param verificationStrategies the current verification strategy to use * @param addOnTracers all operation tracer callbacks + * @param customOps any additional custom operations to use when constructing the EVM * @return the contract service component */ ContractServiceComponent create( @@ -52,7 +56,8 @@ ContractServiceComponent create( @BindsInstance SignatureVerifier signatureVerifier, @BindsInstance VerificationStrategies verificationStrategies, @BindsInstance @Nullable Supplier> addOnTracers, - @BindsInstance ContractMetrics contractMetrics); + @BindsInstance ContractMetrics contractMetrics, + @BindsInstance @CustomOps Set customOps); } /** diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java index 1e0b8e14e094..4c9205c97cd1 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/ContractServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020-2024 Hedera Hashgraph, LLC + * Copyright (C) 2020-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. @@ -32,7 +32,9 @@ import edu.umd.cs.findbugs.annotations.Nullable; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; +import org.hyperledger.besu.evm.operation.Operation; import org.hyperledger.besu.evm.tracing.OperationTracer; /** @@ -50,19 +52,22 @@ public class ContractServiceImpl implements ContractService { * @param appContext the current application context */ public ContractServiceImpl(@NonNull final AppContext appContext) { - this(appContext, null, null); + this(appContext, null, null, Set.of()); } /** * @param appContext the current application context * @param verificationStrategies the current verification strategy used * @param addOnTracers all operation tracer callbacks + * @param customOps any additional custom operations to use when constructing the EVM */ public ContractServiceImpl( @NonNull final AppContext appContext, @Nullable final VerificationStrategies verificationStrategies, - @Nullable final Supplier> addOnTracers) { + @Nullable final Supplier> addOnTracers, + @NonNull final Set customOps) { requireNonNull(appContext); + requireNonNull(customOps); final var metricsSupplier = requireNonNull(appContext.metricsSupplier()); final Supplier contractsConfigSupplier = () -> appContext.configSupplier().get().getConfigData(ContractsConfig.class); @@ -75,7 +80,8 @@ public ContractServiceImpl( appContext.signatureVerifier(), Optional.ofNullable(verificationStrategies).orElseGet(DefaultVerificationStrategies::new), addOnTracers, - contractMetrics); + contractMetrics, + customOps); } @Override diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/annotations/CustomOps.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/annotations/CustomOps.java new file mode 100644 index 000000000000..c108945c1ada --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/annotations/CustomOps.java @@ -0,0 +1,38 @@ +/* + * 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.annotations; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import com.hedera.node.app.service.contract.impl.exec.operations.CustomCallOperation; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.inject.Qualifier; + +/** + * Qualifies a set of {@link org.hyperledger.besu.evm.operation.Operation}s to use to further customize + * EVM behavior, beyond the standard Hedera customizations like {@link CustomCallOperation}. + */ +@Target({METHOD, PARAMETER, TYPE}) +@Retention(RUNTIME) +@Documented +@Qualifier +public @interface CustomOps {} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v030/V030Module.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v030/V030Module.java index 911f323a5db9..25adae593e22 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v030/V030Module.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v030/V030Module.java @@ -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. @@ -21,6 +21,7 @@ import static org.hyperledger.besu.evm.MainnetEVMs.registerLondonOperations; import static org.hyperledger.besu.evm.operation.SStoreOperation.FRONTIER_MINIMUM; +import com.hedera.node.app.service.contract.impl.annotations.CustomOps; import com.hedera.node.app.service.contract.impl.annotations.ServicesV030; import com.hedera.node.app.service.contract.impl.exec.AddressChecks; import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; @@ -126,11 +127,13 @@ static CustomMessageCallProcessor provideMessageCallProcessor( static EVM provideEVM( @ServicesV030 @NonNull final Set customOperations, @NonNull final EvmConfiguration evmConfiguration, - @NonNull final GasCalculator gasCalculator) { + @NonNull final GasCalculator gasCalculator, + @CustomOps @NonNull final Set customOps) { // Use London EVM with 0.30 custom operations and 0x00 chain id (set at runtime) final var operationRegistry = new OperationRegistry(); registerLondonOperations(operationRegistry, gasCalculator, BigInteger.ZERO); customOperations.forEach(operationRegistry::put); + customOps.forEach(operationRegistry::put); return new EVM(operationRegistry, gasCalculator, evmConfiguration, EvmSpecVersion.LONDON); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v034/V034Module.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v034/V034Module.java index ace31e36119a..3848d11edb68 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v034/V034Module.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v034/V034Module.java @@ -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. @@ -21,6 +21,7 @@ import static org.hyperledger.besu.evm.MainnetEVMs.registerLondonOperations; import static org.hyperledger.besu.evm.operation.SStoreOperation.FRONTIER_MINIMUM; +import com.hedera.node.app.service.contract.impl.annotations.CustomOps; import com.hedera.node.app.service.contract.impl.annotations.ServicesV034; import com.hedera.node.app.service.contract.impl.exec.AddressChecks; import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; @@ -128,11 +129,13 @@ static CustomMessageCallProcessor provideMessageCallProcessor( static EVM provideEVM( @ServicesV034 @NonNull final Set customOperations, @NonNull final EvmConfiguration evmConfiguration, - @NonNull final GasCalculator gasCalculator) { + @NonNull final GasCalculator gasCalculator, + @CustomOps @NonNull final Set customOps) { // Use Paris EVM with 0.34 custom operations and 0x00 chain id (set at runtime) final var operationRegistry = new OperationRegistry(); registerLondonOperations(operationRegistry, gasCalculator, BigInteger.ZERO); customOperations.forEach(operationRegistry::put); + customOps.forEach(operationRegistry::put); return new EVM(operationRegistry, gasCalculator, evmConfiguration, EvmSpecVersion.PARIS); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v038/V038Module.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v038/V038Module.java index 43f6061304aa..bca4cc2565f7 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v038/V038Module.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v038/V038Module.java @@ -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. @@ -21,6 +21,7 @@ import static org.hyperledger.besu.evm.MainnetEVMs.registerShanghaiOperations; import static org.hyperledger.besu.evm.operation.SStoreOperation.FRONTIER_MINIMUM; +import com.hedera.node.app.service.contract.impl.annotations.CustomOps; import com.hedera.node.app.service.contract.impl.annotations.ServicesV038; import com.hedera.node.app.service.contract.impl.exec.AddressChecks; import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; @@ -128,11 +129,13 @@ static CustomMessageCallProcessor provideMessageCallProcessor( static EVM provideEVM( @ServicesV038 @NonNull final Set customOperations, @NonNull final EvmConfiguration evmConfiguration, - @NonNull final GasCalculator gasCalculator) { + @NonNull final GasCalculator gasCalculator, + @CustomOps @NonNull final Set customOps) { // Use Paris EVM with 0.38 custom operations and 0x00 chain id (set at runtime) final var operationRegistry = new OperationRegistry(); registerShanghaiOperations(operationRegistry, gasCalculator, BigInteger.ZERO); customOperations.forEach(operationRegistry::put); + customOps.forEach(operationRegistry::put); return new EVM(operationRegistry, gasCalculator, evmConfiguration, EvmSpecVersion.SHANGHAI); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/V046Module.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/V046Module.java index 32617b4760e5..d4e2835cf4fb 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/V046Module.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v046/V046Module.java @@ -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. @@ -21,6 +21,7 @@ import static org.hyperledger.besu.evm.MainnetEVMs.registerShanghaiOperations; import static org.hyperledger.besu.evm.operation.SStoreOperation.FRONTIER_MINIMUM; +import com.hedera.node.app.service.contract.impl.annotations.CustomOps; import com.hedera.node.app.service.contract.impl.annotations.ServicesV046; import com.hedera.node.app.service.contract.impl.exec.AddressChecks; import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; @@ -128,11 +129,13 @@ static CustomMessageCallProcessor provideMessageCallProcessor( static EVM provideEVM( @ServicesV046 @NonNull final Set customOperations, @NonNull final EvmConfiguration evmConfiguration, - @NonNull final GasCalculator gasCalculator) { + @NonNull final GasCalculator gasCalculator, + @CustomOps @NonNull final Set customOps) { // Use Shanghai EVM with 0.46 custom operations and 0x00 chain id (set at runtime) final var operationRegistry = new OperationRegistry(); registerShanghaiOperations(operationRegistry, gasCalculator, BigInteger.ZERO); customOperations.forEach(operationRegistry::put); + customOps.forEach(operationRegistry::put); return new EVM(operationRegistry, gasCalculator, evmConfiguration, EvmSpecVersion.SHANGHAI); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v050/V050Module.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v050/V050Module.java index 3e5746fa19b8..102a8cea01fb 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v050/V050Module.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v050/V050Module.java @@ -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. @@ -21,6 +21,7 @@ import static org.hyperledger.besu.evm.MainnetEVMs.registerCancunOperations; import static org.hyperledger.besu.evm.operation.SStoreOperation.FRONTIER_MINIMUM; +import com.hedera.node.app.service.contract.impl.annotations.CustomOps; import com.hedera.node.app.service.contract.impl.annotations.ServicesV050; import com.hedera.node.app.service.contract.impl.exec.AddressChecks; import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; @@ -138,7 +139,8 @@ static CustomMessageCallProcessor provideMessageCallProcessor( static EVM provideEVM( @ServicesV050 @NonNull final Set customOperations, @NonNull final EvmConfiguration evmConfiguration, - @NonNull final GasCalculator gasCalculator) { + @NonNull final GasCalculator gasCalculator, + @CustomOps @NonNull final Set customOps) { oneTimeEVMModuleInitialization(); @@ -146,6 +148,7 @@ static EVM provideEVM( final var operationRegistry = new OperationRegistry(); registerCancunOperations(operationRegistry, gasCalculator, BigInteger.ZERO); customOperations.forEach(operationRegistry::put); + customOps.forEach(operationRegistry::put); return new EVM(operationRegistry, gasCalculator, evmConfiguration, EvmSpecVersion.CANCUN); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v051/V051Module.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v051/V051Module.java index 5b5dcce962a1..884c5e306aaf 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v051/V051Module.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/v051/V051Module.java @@ -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. @@ -21,6 +21,7 @@ import static org.hyperledger.besu.evm.MainnetEVMs.registerCancunOperations; import static org.hyperledger.besu.evm.operation.SStoreOperation.FRONTIER_MINIMUM; +import com.hedera.node.app.service.contract.impl.annotations.CustomOps; import com.hedera.node.app.service.contract.impl.annotations.ServicesV051; import com.hedera.node.app.service.contract.impl.exec.AddressChecks; import com.hedera.node.app.service.contract.impl.exec.FeatureFlags; @@ -138,7 +139,8 @@ static CustomMessageCallProcessor provideMessageCallProcessor( static EVM provideEVM( @ServicesV051 @NonNull final Set customOperations, @NonNull final EvmConfiguration evmConfiguration, - @NonNull final GasCalculator gasCalculator) { + @NonNull final GasCalculator gasCalculator, + @CustomOps @NonNull final Set customOps) { oneTimeEVMModuleInitialization(); @@ -146,6 +148,7 @@ static EVM provideEVM( final var operationRegistry = new OperationRegistry(); registerCancunOperations(operationRegistry, gasCalculator, BigInteger.ZERO); customOperations.forEach(operationRegistry::put); + customOps.forEach(operationRegistry::put); return new EVM(operationRegistry, gasCalculator, evmConfiguration, EvmSpecVersion.CANCUN); }