From 6009a602980c54b39a92e80ce5b48ef36025fa04 Mon Sep 17 00:00:00 2001 From: David S Bakin <117694041+david-bakin-sl@users.noreply.github.com> Date: Tue, 3 Dec 2024 08:20:30 -0800 Subject: [PATCH] feat: SCSC metrics for #TXs that fail pureCheck (P1 level) - counts for CONTRACT_CREATE / CONTRACT_CALL / ETHEREUM_TRANSACTION that fail pureCheck for any reason - and for those that fail pureCheck because they don't even have the intrinsic gas amount - and for Type 3 format (BLOB) Ethereum transactions (that we don't support) By: - Change plumbing for metrics into smart contract service - Metrics are controlled by feature flags in `ContractsConfig` - Added `AbstractContractTransactionHandler` abstract class to hold a growing amount of commonality in smart contract transaction handler classes. Signed-off-by: David S Bakin <117694041+david-bakin-sl@users.noreply.github.com> --- .../com/hedera/node/app/spi/AppContext.java | 7 + .../src/main/java/module-info.java | 1 + .../blocks/BlockStreamManagerBenchmark.java | 3 + .../app/blocks/StandaloneRoundManagement.java | 3 + .../main/java/com/hedera/node/app/Hedera.java | 3 + .../node/app/services/AppContextImpl.java | 2 + .../standalone/TransactionExecutors.java | 8 +- .../app/components/IngestComponentTest.java | 3 + .../standalone/TransactionExecutorsTest.java | 1 + .../node/config/data/ContractsConfig.java | 6 +- .../impl/ContractServiceComponent.java | 9 +- .../contract/impl/ContractServiceImpl.java | 17 +- .../impl/exec/metrics/ContractMetrics.java | 237 ++++++++++++++++++ .../AbstractContractTransactionHandler.java | 107 ++++++++ .../impl/handlers/ContractCallHandler.java | 50 ++-- .../impl/handlers/ContractCreateHandler.java | 44 ++-- .../handlers/EthereumTransactionHandler.java | 43 ++-- .../src/main/java/module-info.java | 2 + .../impl/test/ContractServiceImplTest.java | 1 + .../exec/metrics/ContractMetricsTest.java | 137 ++++++++++ .../handlers/ContractCallHandlerTest.java | 19 +- .../handlers/ContractCreateHandlerTest.java | 19 +- .../EthereumTransactionHandlerTest.java | 16 +- 23 files changed, 659 insertions(+), 79 deletions(-) create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/metrics/ContractMetrics.java create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/AbstractContractTransactionHandler.java create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/metrics/ContractMetricsTest.java diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java index 0248cc876d7c..2712503cd48e 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/AppContext.java @@ -22,6 +22,7 @@ import com.hedera.node.app.spi.throttle.Throttle; import com.swirlds.common.crypto.Signature; import com.swirlds.config.api.Configuration; +import com.swirlds.metrics.api.Metrics; import com.swirlds.state.lifecycle.Service; import com.swirlds.state.lifecycle.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; @@ -105,6 +106,12 @@ public Signature sign(final byte[] ledgerId) { */ Supplier selfNodeInfoSupplier(); + /** + * The supplier of (platform) metrics + * @return the supplier + */ + Supplier metricsSupplier(); + /** * The application's strategy for creating {@link Throttle} instances. * @return the throttle factory diff --git a/hedera-node/hedera-app-spi/src/main/java/module-info.java b/hedera-node/hedera-app-spi/src/main/java/module-info.java index 09f560d9b626..2a24fb583ebf 100644 --- a/hedera-node/hedera-app-spi/src/main/java/module-info.java +++ b/hedera-node/hedera-app-spi/src/main/java/module-info.java @@ -3,6 +3,7 @@ requires transitive com.hedera.node.hapi; requires transitive com.swirlds.common; requires transitive com.swirlds.config.api; + requires transitive com.swirlds.metrics.api; requires transitive com.swirlds.state.api; requires transitive com.hedera.pbj.runtime; requires static com.github.spotbugs.annotations; diff --git a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/BlockStreamManagerBenchmark.java b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/BlockStreamManagerBenchmark.java index 66aeda46fda1..471acf95e398 100644 --- a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/BlockStreamManagerBenchmark.java +++ b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/BlockStreamManagerBenchmark.java @@ -55,6 +55,7 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.Hash; import com.swirlds.common.metrics.noop.NoOpMetrics; +import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; import com.swirlds.platform.system.Round; @@ -104,6 +105,7 @@ public class BlockStreamManagerBenchmark { private static final String SAMPLE_BLOCK = "sample.blk.gz"; private static final Instant FAKE_CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); private static final Timestamp FAKE_CONSENSUS_TIME = new Timestamp(1_234_567L, 890); + private static final Metrics NO_OP_METRICS = new NoOpMetrics(); private static final SemanticVersion VERSION = new SemanticVersion(0, 56, 0, "", ""); public static void main(String... args) throws Exception { @@ -124,6 +126,7 @@ public static void main(String... args) throws Exception { UNAVAILABLE_GOSSIP, configProvider::getConfiguration, () -> DEFAULT_NODE_INFO, + () -> NO_OP_METRICS, (split, snapshots) -> { throw new UnsupportedOperationException(); }); diff --git a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/StandaloneRoundManagement.java b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/StandaloneRoundManagement.java index 58231f0f0fc9..a7b0e0fddb3e 100644 --- a/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/StandaloneRoundManagement.java +++ b/hedera-node/hedera-app/src/jmh/java/com/hedera/node/app/blocks/StandaloneRoundManagement.java @@ -56,6 +56,7 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.Hash; import com.swirlds.common.metrics.noop.NoOpMetrics; +import com.swirlds.metrics.api.Metrics; import com.swirlds.platform.state.service.PlatformStateService; import com.swirlds.platform.state.service.schemas.V0540PlatformStateSchema; import com.swirlds.platform.system.Round; @@ -84,6 +85,7 @@ public class StandaloneRoundManagement { private static final String SAMPLE_BLOCK = "sample.blk.gz"; private static final Instant FAKE_CONSENSUS_NOW = Instant.ofEpochSecond(1_234_567L, 890); private static final Timestamp FAKE_CONSENSUS_TIME = new Timestamp(1_234_567L, 890); + private static final Metrics NO_OP_METRICS = new NoOpMetrics(); private static final SemanticVersion VERSION = new SemanticVersion(0, 56, 0, "", ""); private static final int NUM_ROUNDS = 10000; @@ -100,6 +102,7 @@ public class StandaloneRoundManagement { UNAVAILABLE_GOSSIP, configProvider::getConfiguration, () -> DEFAULT_NODE_INFO, + () -> NO_OP_METRICS, (split, snapshots) -> { throw new UnsupportedOperationException(); }); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java index 5b6d54bc9670..8eb512776b59 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/Hedera.java @@ -429,6 +429,7 @@ public Hedera( this, configSupplier, () -> daggerApp.networkInfo().selfNodeInfo(), + () -> metrics, new AppThrottleFactory( configSupplier, () -> daggerApp.workingStateAccessor().getState(), @@ -602,6 +603,8 @@ public void onStateInitialized( trigger, RosterUtils.buildAddressBook(platform.getRoster()), platform.getContext().getConfiguration()); + + contractServiceImpl.registerMetrics(); } // With the States API grounded in the working state, we can create the object graph from it initializeDagger(state, trigger); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java index 1c9c9c92e6e9..c74b7210bc0d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/services/AppContextImpl.java @@ -20,6 +20,7 @@ import com.hedera.node.app.spi.signatures.SignatureVerifier; import com.hedera.node.app.spi.throttle.Throttle; import com.swirlds.config.api.Configuration; +import com.swirlds.metrics.api.Metrics; import com.swirlds.state.lifecycle.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; import java.time.InstantSource; @@ -39,5 +40,6 @@ public record AppContextImpl( @NonNull Gossip gossip, @NonNull Supplier configSupplier, @NonNull Supplier selfNodeInfoSupplier, + @NonNull Supplier metricsSupplier, @NonNull Throttle.Factory throttleFactory) implements AppContext {} 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 cd68c02bf164..043b8f76929d 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 @@ -39,6 +39,7 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.crypto.CryptographyHolder; import com.swirlds.common.metrics.noop.NoOpMetrics; +import com.swirlds.metrics.api.Metrics; import com.swirlds.state.State; import com.swirlds.state.lifecycle.info.NodeInfo; import edu.umd.cs.findbugs.annotations.NonNull; @@ -111,6 +112,7 @@ private ExecutorComponent newExecutorComponent( UNAVAILABLE_GOSSIP, bootstrapConfigProvider::getConfiguration, () -> DEFAULT_NODE_INFO, + () -> NO_OP_METRICS, new AppThrottleFactory( configProvider::getConfiguration, () -> state, @@ -122,7 +124,7 @@ private ExecutorComponent newExecutorComponent( ForkJoinPool.commonPool(), new TssLibraryImpl(appContext), ForkJoinPool.commonPool(), - new NoOpMetrics()); + NO_OP_METRICS); final var contractService = new ContractServiceImpl(appContext, NOOP_VERIFICATION_STRATEGIES, tracerBinding); final var fileService = new FileServiceImpl(); final var scheduleService = new ScheduleServiceImpl(); @@ -133,7 +135,7 @@ private ExecutorComponent newExecutorComponent( .fileServiceImpl(fileService) .contractServiceImpl(contractService) .scheduleServiceImpl(scheduleService) - .metrics(new NoOpMetrics()) + .metrics(NO_OP_METRICS) .throttleFactory(appContext.throttleFactory()) .build(); componentRef.set(component); @@ -159,4 +161,6 @@ public List get() { return OPERATION_TRACERS.get(); } } + + private static final Metrics NO_OP_METRICS = new NoOpMetrics(); } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java index e2c261bb0f5d..34b298a96c9d 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/components/IngestComponentTest.java @@ -101,6 +101,8 @@ class IngestComponentTest { private HederaInjectionComponent app; + private static final Metrics NO_OP_METRICS = new NoOpMetrics(); + @BeforeEach void setUp() { final Configuration configuration = HederaTestConfigBuilder.createConfig(); @@ -125,6 +127,7 @@ void setUp() { UNAVAILABLE_GOSSIP, () -> configuration, () -> DEFAULT_NODE_INFO, + () -> NO_OP_METRICS, throttleFactory); given(tssBaseService.tssHandlers()) .willReturn(new TssHandlers(tssMessageHandler, tssVoteHandler, tssShareSignatureHandler)); 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 75ade645a7e4..fa945f5cd98c 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 @@ -250,6 +250,7 @@ private State genesisState(@NonNull final Map overrides) { UNAVAILABLE_GOSSIP, () -> config, () -> DEFAULT_NODE_INFO, + () -> NO_OP_METRICS, new AppThrottleFactory( () -> config, () -> state, () -> ThrottleDefinitions.DEFAULT, ThrottleAccumulator::new)); registerServices(appContext, config, servicesRegistry); diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java index 63c62b7a6bf1..9ba2c954a02a 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/ContractsConfig.java @@ -126,4 +126,8 @@ public record ContractsConfig( boolean chargeGasOnEvmHandleException, @ConfigProperty(value = "evm.nonExtantContractsFail", defaultValue = "0") @NetworkProperty Set evmNonExtantContractsFail, - @ConfigProperty(value = "evm.version", defaultValue = "v0.51") @NetworkProperty String evmVersion) {} + @ConfigProperty(value = "evm.version", defaultValue = "v0.51") @NetworkProperty String evmVersion, + @ConfigProperty(value = "metrics.smartContract.primary.enabled", defaultValue = "true") @NetworkProperty + boolean metricsSmartContractPrimaryEnabled, + @ConfigProperty(value = "metrics.smartContract.secondary.enabled", defaultValue = "true") @NetworkProperty + boolean metricsSmartContractSecondaryEnabled) {} 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 546b8da5765b..16ebea137121 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 @@ -16,6 +16,7 @@ package com.hedera.node.app.service.contract.impl; +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; import com.hedera.node.app.spi.signatures.SignatureVerifier; @@ -50,11 +51,17 @@ ContractServiceComponent create( @BindsInstance InstantSource instantSource, @BindsInstance SignatureVerifier signatureVerifier, @BindsInstance VerificationStrategies verificationStrategies, - @BindsInstance @Nullable Supplier> addOnTracers); + @BindsInstance @Nullable Supplier> addOnTracers, + @BindsInstance ContractMetrics contractMetrics); } /** * @return all contract transaction handlers */ ContractHandlers handlers(); + + /** + * @return contract metrics collection, instance + */ + ContractMetrics contractMetrics(); } 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 0f8d403c6003..1e0b8e14e094 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 @@ -19,12 +19,14 @@ import static java.util.Objects.requireNonNull; import com.hedera.node.app.service.contract.ContractService; +import com.hedera.node.app.service.contract.impl.exec.metrics.ContractMetrics; import com.hedera.node.app.service.contract.impl.exec.scope.DefaultVerificationStrategies; import com.hedera.node.app.service.contract.impl.exec.scope.VerificationStrategies; import com.hedera.node.app.service.contract.impl.handlers.ContractHandlers; import com.hedera.node.app.service.contract.impl.schemas.V0490ContractSchema; import com.hedera.node.app.service.contract.impl.schemas.V0500ContractSchema; import com.hedera.node.app.spi.AppContext; +import com.hedera.node.config.data.ContractsConfig; import com.swirlds.state.lifecycle.SchemaRegistry; import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; @@ -61,6 +63,10 @@ public ContractServiceImpl( @Nullable final VerificationStrategies verificationStrategies, @Nullable final Supplier> addOnTracers) { requireNonNull(appContext); + final var metricsSupplier = requireNonNull(appContext.metricsSupplier()); + final Supplier contractsConfigSupplier = + () -> appContext.configSupplier().get().getConfigData(ContractsConfig.class); + final var contractMetrics = new ContractMetrics(metricsSupplier, contractsConfigSupplier); this.component = DaggerContractServiceComponent.factory() .create( appContext.instantSource(), @@ -68,7 +74,8 @@ public ContractServiceImpl( // C.f. https://github.com/hashgraph/hedera-services/issues/14248 appContext.signatureVerifier(), Optional.ofNullable(verificationStrategies).orElseGet(DefaultVerificationStrategies::new), - addOnTracers); + addOnTracers, + contractMetrics); } @Override @@ -77,6 +84,14 @@ public void registerSchemas(@NonNull final SchemaRegistry registry) { registry.register(new V0500ContractSchema()); } + /** + * Create the metrics for the smart contracts service. This needs to be delayed until _after_ + * the metrics are available - which happens after `Hedera.initializeStatesApi`. + */ + public void registerMetrics() { + component.contractMetrics().createContractMetrics(); + } + /** * @return all contract transaction handlers */ diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/metrics/ContractMetrics.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/metrics/ContractMetrics.java new file mode 100644 index 000000000000..4dbab82f2999 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/metrics/ContractMetrics.java @@ -0,0 +1,237 @@ +/* + * Copyright (C) 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.metrics; + +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CALL; +import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE; +import static com.hedera.hapi.node.base.HederaFunctionality.ETHEREUM_TRANSACTION; +import static java.util.Objects.requireNonNull; +import static java.util.stream.Collectors.toMap; + +import com.google.common.annotations.VisibleForTesting; +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.node.config.data.ContractsConfig; +import com.swirlds.metrics.api.Counter; +import com.swirlds.metrics.api.Metric; +import com.swirlds.metrics.api.Metrics; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.inject.Inject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * Metrics collection management for Smart Contracts service + * + * Includes: + * * Rejected transactions counters: Transactions which failed `pureCheck` for one reason or another + */ +public class ContractMetrics { + + private static final Logger log = LogManager.getLogger(ContractMetrics.class); + + private final Supplier metricsSupplier; + private final Supplier contractsConfigSupplier; + private boolean p1MetricsEnabled; + private boolean p2MetricsEnabled; + + private final HashMap rejectedTxsCounters = new HashMap<>(); + private final HashMap rejectedTxsLackingIntrinsicGas = new HashMap<>(); + private Counter rejectedEthType3Counter; + + private static final Map POSSIBLE_FAILING_TX_TYPES = Map.of( + CONTRACT_CALL, "contractCallTx", CONTRACT_CREATE, "contractCreateTx", ETHEREUM_TRANSACTION, "ethereumTx"); + + private static final String METRIC_CATEGORY = "app"; + private static final String METRIC_SERVICE = "SmartContractService"; + private static final String METRIC_TXN_UNIT = "txs"; + + // Templates: %1$s - HederaFunctionality name + // %2$s - METRIC_SERVICE + // %3$s - short specific metric description + + private static final String REJECTED_NAME_TEMPLATE = "%2$s:Rejected-%1$s_total"; + private static final String REJECTED_DESCR_TEMPLATE = "submitted %1$s %3$s rejected by pureChecks"; + + private static final String REJECTED_TXN_SHORT_DESCR = "txns"; + private static final String REJECTED_TYPE3_SHORT_DESCR = "Ethereum Type 3 txns"; + private static final String REJECTED_FOR_GAS_SHORT_DESCR = "txns with not even intrinsic gas"; + + private static final String REJECTED_TYPE3_FUNCTIONALITY = "ethType3BlobTransaction"; + + @Inject + public ContractMetrics( + @NonNull final Supplier metricsSupplier, + @NonNull final Supplier contractsConfigSupplier) { + this.metricsSupplier = requireNonNull( + metricsSupplier, "metrics supplier (from platform via ServicesMain/Hedera must not be null"); + this.contractsConfigSupplier = + requireNonNull(contractsConfigSupplier, "contracts configuration supplier must not be null"); + } + + public void createContractMetrics() { + + final var contractsConfig = requireNonNull(contractsConfigSupplier.get()); + this.p1MetricsEnabled = contractsConfig.metricsSmartContractPrimaryEnabled(); + this.p2MetricsEnabled = contractsConfig.metricsSmartContractSecondaryEnabled(); + + final var metrics = requireNonNull(metricsSupplier.get()); + + if (p1MetricsEnabled) { + // Rejected transactions counters + for (final var txKind : POSSIBLE_FAILING_TX_TYPES.keySet()) { + final var name = toRejectedName(txKind, REJECTED_TXN_SHORT_DESCR); + final var descr = toRejectedDescr(txKind, REJECTED_TXN_SHORT_DESCR); + final var config = new Counter.Config(METRIC_CATEGORY, name) + .withDescription(descr) + .withUnit(METRIC_TXN_UNIT); + final var metric = newCounter(metrics, config); + rejectedTxsCounters.put(txKind, metric); + } + + // Rejected transactions because they don't even have intrinsic gas + for (final var txKind : POSSIBLE_FAILING_TX_TYPES.keySet()) { + final var functionalityName = POSSIBLE_FAILING_TX_TYPES.get(txKind) + "DueToIntrinsicGas"; + final var name = toRejectedName(functionalityName, REJECTED_FOR_GAS_SHORT_DESCR); + final var descr = toRejectedDescr(functionalityName, REJECTED_FOR_GAS_SHORT_DESCR); + final var config = new Counter.Config(METRIC_CATEGORY, name) + .withDescription(descr) + .withUnit(METRIC_TXN_UNIT); + final var metric = newCounter(metrics, config); + rejectedTxsLackingIntrinsicGas.put(txKind, metric); + } + + // Rejected transactions for ethereum calls that are in type 3 blob transaction format + { + final var name = toRejectedName(REJECTED_TYPE3_FUNCTIONALITY, REJECTED_TYPE3_SHORT_DESCR); + final var descr = toRejectedDescr(REJECTED_TYPE3_FUNCTIONALITY, REJECTED_TYPE3_SHORT_DESCR); + final var config = new Counter.Config(METRIC_CATEGORY, name) + .withDescription(descr) + .withUnit(METRIC_TXN_UNIT); + final var metric = newCounter(metrics, config); + rejectedEthType3Counter = metric; + } + } + + if (p2MetricsEnabled) { + // PLACEHOLDER + } + } + + public void incrementRejectedTx(@NonNull final HederaFunctionality txKind) { + bumpRejectedTx(txKind, 1); + } + + public void bumpRejectedTx(@NonNull final HederaFunctionality txKind, final long bumpBy) { + if (p1MetricsEnabled) requireNonNull(rejectedTxsCounters.get(txKind)).add(bumpBy); + } + + public void incrementRejectedForGasTx(@NonNull final HederaFunctionality txKind) { + bumpRejectedForGasTx(txKind, 1); + } + + public void bumpRejectedForGasTx(@NonNull final HederaFunctionality txKind, final long bumpBy) { + if (p1MetricsEnabled) + requireNonNull(rejectedTxsLackingIntrinsicGas.get(txKind)).add(bumpBy); + } + + public void incrementRejectedType3EthTx() { + bumpRejectedType3EthTx(1); + } + + public void bumpRejectedType3EthTx(final long bumpBy) { + if (p1MetricsEnabled) rejectedEthType3Counter.add(bumpBy); + } + + @VisibleForTesting + public @NonNull Map getAllCounters() { + return Stream.concat( + Stream.concat( + rejectedTxsCounters.values().stream(), + rejectedTxsLackingIntrinsicGas.values().stream()), + Stream.ofNullable(rejectedEthType3Counter)) + .collect(toMap(Counter::getName, Counter::get)); + } + + @VisibleForTesting + public @NonNull List getAllCounterNames() { + return Stream.concat( + Stream.concat( + rejectedTxsCounters.values().stream(), + rejectedTxsLackingIntrinsicGas.values().stream()), + Stream.ofNullable(rejectedEthType3Counter)) + .map(Metric::getName) + .sorted() + .toList(); + } + + @VisibleForTesting + public @NonNull List getAllCounterDescriptions() { + return Stream.concat( + Stream.concat( + rejectedTxsCounters.values().stream(), + rejectedTxsLackingIntrinsicGas.values().stream()), + Stream.ofNullable(rejectedEthType3Counter)) + .map(Metric::getDescription) + .sorted() + .toList(); + } + + @VisibleForTesting + public @NonNull String allCountersToString() { + return getAllCounters().entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .map(e -> e.getKey() + ": " + e.getValue()) + .collect(Collectors.joining(", ", "{", "}")); + } + + private @NonNull Counter newCounter(@NonNull final Metrics metrics, @NonNull final Counter.Config config) { + return metrics.getOrCreate(config); + } + + private static @NonNull String toRejectedName( + @NonNull final HederaFunctionality functionality, @NonNull final String shortDescription) { + return toRejectedName(POSSIBLE_FAILING_TX_TYPES.get(functionality), shortDescription); + } + + private static @NonNull String toRejectedName( + @NonNull final String functionality, @NonNull final String shortDescription) { + return toString(REJECTED_NAME_TEMPLATE, functionality, shortDescription); + } + + private static @NonNull String toRejectedDescr( + @NonNull final HederaFunctionality functionality, @NonNull final String shortDescription) { + return toString(REJECTED_DESCR_TEMPLATE, POSSIBLE_FAILING_TX_TYPES.get(functionality), shortDescription); + } + + private static @NonNull String toRejectedDescr( + @NonNull final String functionality, @NonNull final String shortDescription) { + return toString(REJECTED_DESCR_TEMPLATE, functionality, shortDescription); + } + + private static @NonNull String toString( + @NonNull final String template, + @NonNull final String functionality, + @NonNull final String shortDescription) { + return template.formatted(functionality, METRIC_SERVICE, shortDescription); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/AbstractContractTransactionHandler.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/AbstractContractTransactionHandler.java new file mode 100644 index 000000000000..f2ab8b4aeb79 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/AbstractContractTransactionHandler.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 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.handlers; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_GAS; +import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.base.SubType; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.hapi.utils.fee.SigValueObj; +import com.hedera.node.app.hapi.utils.fee.SmartContractFeeBuilder; +import com.hedera.node.app.service.contract.impl.ContractServiceComponent; +import com.hedera.node.app.service.contract.impl.exec.TransactionComponent; +import com.hedera.node.app.service.contract.impl.exec.TransactionComponent.Factory; +import com.hedera.node.app.spi.fees.FeeContext; +import com.hedera.node.app.spi.fees.Fees; +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.hederahashgraph.api.proto.java.FeeData; +import edu.umd.cs.findbugs.annotations.NonNull; +import javax.inject.Provider; +import org.hyperledger.besu.evm.gascalculator.GasCalculator; + +/** + * Holds some state and functionality common to the smart contract transaction handlers. + */ +public abstract class AbstractContractTransactionHandler implements TransactionHandler { + + protected final Provider provider; + protected final ContractServiceComponent component; + protected final GasCalculator gasCalculator; + protected final SmartContractFeeBuilder usageEstimator = new SmartContractFeeBuilder(); + + protected AbstractContractTransactionHandler( + @NonNull final Provider provider, + @NonNull final GasCalculator gasCalculator, + @NonNull final ContractServiceComponent component) { + this.provider = requireNonNull(provider); + this.gasCalculator = requireNonNull(gasCalculator); + this.component = requireNonNull(component); + } + + @Override + public abstract void preHandle(@NonNull PreHandleContext context) throws PreCheckException; + + @Override + public abstract void pureChecks(@NonNull TransactionBody txn) throws PreCheckException; + + /** + * Handle common metrics for transactions that fail `pureChecks`. + * + * (Caller is responsible to rethrow `e`.) + */ + protected void bumpExceptionMetrics(@NonNull final HederaFunctionality functionality, @NonNull final Exception e) { + final var contractMetrics = component.contractMetrics(); + contractMetrics.incrementRejectedTx(functionality); + if (e instanceof PreCheckException pce && pce.responseCode() == INSUFFICIENT_GAS) { + contractMetrics.incrementRejectedForGasTx(functionality); + } + } + + @Override + public abstract void handle(@NonNull HandleContext context) throws HandleException; + + @Override + public @NonNull Fees calculateFees(@NonNull FeeContext feeContext) { + requireNonNull(feeContext); + final var op = feeContext.body(); + return feeContext + .feeCalculatorFactory() + .feeCalculator(SubType.DEFAULT) + .legacyCalculate(sigValueObj -> getFeeMatrices(usageEstimator, fromPbj(op), sigValueObj)); + } + + /** + * Return the fee matrix for the given transaction. Inheritor is responsible for picking + * the correct fee matrix for the transactions it is handling. + * + * Used by the default implementation of `calculateFees`, above. If inheritor overrides + * `calculateFees` then it doesn't need to override this method. + */ + protected /*abstract*/ @NonNull FeeData getFeeMatrices( + @NonNull final SmartContractFeeBuilder usageEstimator, + @NonNull final com.hederahashgraph.api.proto.java.TransactionBody txBody, + @NonNull final SigValueObj sigValObj) { + throw new IllegalStateException("must be overridden if `calculateFees` _not_ overridden"); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCallHandler.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCallHandler.java index 9756c422210a..ebaf8b8465ed 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCallHandler.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCallHandler.java @@ -22,22 +22,19 @@ import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.throwIfUnsuccessful; import static com.hedera.node.app.spi.validation.Validations.mustExist; 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.SubType; import com.hedera.hapi.node.transaction.TransactionBody; -import com.hedera.node.app.hapi.utils.CommonPbjConverters; +import com.hedera.node.app.hapi.utils.fee.SigValueObj; import com.hedera.node.app.hapi.utils.fee.SmartContractFeeBuilder; +import com.hedera.node.app.service.contract.impl.ContractServiceComponent; import com.hedera.node.app.service.contract.impl.exec.TransactionComponent; import com.hedera.node.app.service.contract.impl.records.ContractCallStreamBuilder; -import com.hedera.node.app.spi.fees.FeeContext; -import com.hedera.node.app.spi.fees.Fees; 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.hederahashgraph.api.proto.java.FeeData; import edu.umd.cs.findbugs.annotations.NonNull; import javax.inject.Inject; import javax.inject.Provider; @@ -49,11 +46,7 @@ * This class contains all workflow-related functionality regarding {@link HederaFunctionality#CONTRACT_CALL}. */ @Singleton -public class ContractCallHandler implements TransactionHandler { - private final Provider provider; - private final GasCalculator gasCalculator; - private final SmartContractFeeBuilder usageEstimator = new SmartContractFeeBuilder(); - +public class ContractCallHandler extends AbstractContractTransactionHandler { /** * Constructs a {@link ContractCallHandler} with the given {@link Provider} and {@link GasCalculator}. * @@ -63,9 +56,9 @@ public class ContractCallHandler implements TransactionHandler { @Inject public ContractCallHandler( @NonNull final Provider provider, - @NonNull final GasCalculator gasCalculator) { - this.provider = requireNonNull(provider); - this.gasCalculator = requireNonNull(gasCalculator); + @NonNull final GasCalculator gasCalculator, + @NonNull final ContractServiceComponent component) { + super(provider, gasCalculator, component); } @Override @@ -89,23 +82,24 @@ public void preHandle(@NonNull final PreHandleContext context) { @Override public void pureChecks(@NonNull TransactionBody txn) throws PreCheckException { - final var op = txn.contractCallOrThrow(); - mustExist(op.contractID(), INVALID_CONTRACT_ID); + try { + final var op = txn.contractCallOrThrow(); + mustExist(op.contractID(), INVALID_CONTRACT_ID); - final var intrinsicGas = gasCalculator.transactionIntrinsicGasCost( - Bytes.wrap(op.functionParameters().toByteArray()), false); - validateTruePreCheck(op.gas() >= intrinsicGas, INSUFFICIENT_GAS); + final var intrinsicGas = gasCalculator.transactionIntrinsicGasCost( + Bytes.wrap(op.functionParameters().toByteArray()), false); + validateTruePreCheck(op.gas() >= intrinsicGas, INSUFFICIENT_GAS); + } catch (@NonNull final Exception e) { + bumpExceptionMetrics(CONTRACT_CALL, e); + throw e; + } } - @NonNull @Override - public Fees calculateFees(@NonNull final FeeContext feeContext) { - requireNonNull(feeContext); - final var op = feeContext.body(); - return feeContext - .feeCalculatorFactory() - .feeCalculator(SubType.DEFAULT) - .legacyCalculate(sigValueObj -> - usageEstimator.getContractCallTxFeeMatrices(CommonPbjConverters.fromPbj(op), sigValueObj)); + protected /*abstract*/ @NonNull FeeData getFeeMatrices( + @NonNull final SmartContractFeeBuilder usageEstimator, + @NonNull final com.hederahashgraph.api.proto.java.TransactionBody txBody, + @NonNull final SigValueObj sigValObj) { + return usageEstimator.getContractCallTxFeeMatrices(txBody, sigValObj); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCreateHandler.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCreateHandler.java index 6d2991a2c0c3..c5b05858dd09 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCreateHandler.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/ContractCreateHandler.java @@ -19,25 +19,23 @@ import static com.hedera.hapi.node.base.HederaFunctionality.CONTRACT_CREATE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_GAS; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; -import static com.hedera.node.app.hapi.utils.CommonPbjConverters.fromPbj; import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.throwIfUnsuccessful; import static com.hedera.node.app.spi.workflows.PreCheckException.validateTruePreCheck; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; -import com.hedera.hapi.node.base.SubType; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.hapi.utils.fee.SigValueObj; import com.hedera.node.app.hapi.utils.fee.SmartContractFeeBuilder; +import com.hedera.node.app.service.contract.impl.ContractServiceComponent; import com.hedera.node.app.service.contract.impl.exec.TransactionComponent; import com.hedera.node.app.service.contract.impl.records.ContractCreateStreamBuilder; -import com.hedera.node.app.spi.fees.FeeContext; -import com.hedera.node.app.spi.fees.Fees; 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.hederahashgraph.api.proto.java.FeeData; import edu.umd.cs.findbugs.annotations.NonNull; import javax.inject.Inject; import javax.inject.Provider; @@ -49,12 +47,9 @@ * This class contains all workflow-related functionality regarding {@link HederaFunctionality#CONTRACT_CREATE}. */ @Singleton -public class ContractCreateHandler implements TransactionHandler { +public class ContractCreateHandler extends AbstractContractTransactionHandler { private static final AccountID REMOVE_AUTO_RENEW_ACCOUNT_SENTINEL = AccountID.newBuilder().shardNum(0).realmNum(0).accountNum(0).build(); - private final Provider provider; - private final SmartContractFeeBuilder usageEstimator = new SmartContractFeeBuilder(); - private final GasCalculator gasCalculator; /** * Constructs a {@link ContractCreateHandler} with the given {@link Provider} and {@link GasCalculator}. @@ -65,9 +60,9 @@ public class ContractCreateHandler implements TransactionHandler { @Inject public ContractCreateHandler( @NonNull final Provider provider, - @NonNull final GasCalculator gasCalculator) { - this.provider = requireNonNull(provider); - this.gasCalculator = requireNonNull(gasCalculator); + @NonNull final GasCalculator gasCalculator, + @NonNull final ContractServiceComponent component) { + super(provider, gasCalculator, component); } @Override @@ -86,10 +81,15 @@ public void handle(@NonNull final HandleContext context) throws HandleException @Override public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { - final var op = txn.contractCreateInstanceOrThrow(); + try { + final var op = txn.contractCreateInstanceOrThrow(); - final var intrinsicGas = gasCalculator.transactionIntrinsicGasCost(Bytes.wrap(new byte[0]), true); - validateTruePreCheck(op.gas() >= intrinsicGas, INSUFFICIENT_GAS); + final var intrinsicGas = gasCalculator.transactionIntrinsicGasCost(Bytes.wrap(new byte[0]), true); + validateTruePreCheck(op.gas() >= intrinsicGas, INSUFFICIENT_GAS); + } catch (@NonNull final Exception e) { + bumpExceptionMetrics(CONTRACT_CREATE, e); + throw e; + } } @Override @@ -117,15 +117,11 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx } } - @NonNull @Override - public Fees calculateFees(@NonNull final FeeContext feeContext) { - requireNonNull(feeContext); - final var op = feeContext.body(); - return feeContext - .feeCalculatorFactory() - .feeCalculator(SubType.DEFAULT) - .legacyCalculate( - sigValueObj -> usageEstimator.getContractCreateTxFeeMatrices(fromPbj(op), sigValueObj)); + protected /*abstract*/ @NonNull FeeData getFeeMatrices( + @NonNull final SmartContractFeeBuilder usageEstimator, + @NonNull final com.hederahashgraph.api.proto.java.TransactionBody txBody, + @NonNull final SigValueObj sigValObj) { + return usageEstimator.getContractCreateTxFeeMatrices(txBody, sigValObj); } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/EthereumTransactionHandler.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/EthereumTransactionHandler.java index 8a8eeed6eb8a..07e86d261b39 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/EthereumTransactionHandler.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/handlers/EthereumTransactionHandler.java @@ -34,7 +34,7 @@ import com.hedera.hapi.node.contract.EthereumTransactionBody; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.hapi.utils.ethereum.EthTxSigs; -import com.hedera.node.app.hapi.utils.fee.SmartContractFeeBuilder; +import com.hedera.node.app.service.contract.impl.ContractServiceComponent; import com.hedera.node.app.service.contract.impl.exec.TransactionComponent; import com.hedera.node.app.service.contract.impl.hevm.HederaEvmTransaction; import com.hedera.node.app.service.contract.impl.infra.EthTxSigsCache; @@ -49,7 +49,6 @@ 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.ContractsConfig; import com.hedera.node.config.data.HederaConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; @@ -67,13 +66,10 @@ * This class contains all workflow-related functionality regarding {@link HederaFunctionality#ETHEREUM_TRANSACTION}. */ @Singleton -public class EthereumTransactionHandler implements TransactionHandler { +public class EthereumTransactionHandler extends AbstractContractTransactionHandler { private final byte[] EMPTY_ADDRESS = new byte[20]; private final EthTxSigsCache ethereumSignatures; private final EthereumCallDataHydration callDataHydration; - private final Provider provider; - private final GasCalculator gasCalculator; - private final SmartContractFeeBuilder usageEstimator = new SmartContractFeeBuilder(); /** * @param ethereumSignatures the ethereum signatures @@ -86,11 +82,11 @@ public EthereumTransactionHandler( @NonNull final EthTxSigsCache ethereumSignatures, @NonNull final EthereumCallDataHydration callDataHydration, @NonNull final Provider provider, - @NonNull final GasCalculator gasCalculator) { + @NonNull final GasCalculator gasCalculator, + @NonNull final ContractServiceComponent component) { + super(provider, gasCalculator, component); this.ethereumSignatures = requireNonNull(ethereumSignatures); this.callDataHydration = requireNonNull(callDataHydration); - this.provider = requireNonNull(provider); - this.gasCalculator = requireNonNull(gasCalculator); } @Override @@ -105,16 +101,25 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx @Override public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { - final var ethTxData = populateEthTxData( - requireNonNull(txn.ethereumTransactionOrThrow().ethereumData()).toByteArray()); - validateTruePreCheck(nonNull(ethTxData), INVALID_ETHEREUM_TRANSACTION); - final byte[] callData = ethTxData.hasCallData() ? ethTxData.callData() : new byte[0]; - final var intrinsicGas = - gasCalculator.transactionIntrinsicGasCost(org.apache.tuweni.bytes.Bytes.wrap(callData), false); - validateTruePreCheck(ethTxData.gasLimit() >= intrinsicGas, INSUFFICIENT_GAS); - // Do not allow sending HBars to Burn Address - if (ethTxData.value().compareTo(BigInteger.ZERO) > 0) { - validateFalsePreCheck(Arrays.equals(ethTxData.to(), EMPTY_ADDRESS), INVALID_SOLIDITY_ADDRESS); + try { + final var ethTxData = populateEthTxData( + requireNonNull(txn.ethereumTransactionOrThrow().ethereumData()) + .toByteArray()); + validateTruePreCheck(nonNull(ethTxData), INVALID_ETHEREUM_TRANSACTION); + final byte[] callData = ethTxData.hasCallData() ? ethTxData.callData() : new byte[0]; + final var intrinsicGas = + gasCalculator.transactionIntrinsicGasCost(org.apache.tuweni.bytes.Bytes.wrap(callData), false); + validateTruePreCheck(ethTxData.gasLimit() >= intrinsicGas, INSUFFICIENT_GAS); + // Do not allow sending HBars to Burn Address + if (ethTxData.value().compareTo(BigInteger.ZERO) > 0) { + validateFalsePreCheck(Arrays.equals(ethTxData.to(), EMPTY_ADDRESS), INVALID_SOLIDITY_ADDRESS); + } + } catch (@NonNull final Exception e) { + bumpExceptionMetrics(ETHEREUM_TRANSACTION, e); + if (e instanceof NullPointerException) { + component.contractMetrics().incrementRejectedType3EthTx(); + } + throw e; } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/module-info.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/module-info.java index 969f78233dd8..67f84fa6aea4 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/module-info.java @@ -12,6 +12,7 @@ requires transitive com.hedera.node.config; requires transitive com.hedera.node.hapi; requires transitive com.swirlds.config.api; + requires transitive com.swirlds.metrics.api; requires transitive com.swirlds.state.api; requires transitive com.hedera.pbj.runtime; requires transitive dagger; @@ -68,6 +69,7 @@ exports com.hedera.node.app.service.contract.impl.exec.v038 to com.hedera.node.app.service.contract.impl.test; exports com.hedera.node.app.service.contract.impl.utils; + exports com.hedera.node.app.service.contract.impl.exec.metrics; exports com.hedera.node.app.service.contract.impl.exec.utils; opens com.hedera.node.app.service.contract.impl.exec to diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/ContractServiceImplTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/ContractServiceImplTest.java index 659fd39c53ca..6c8a131f9de0 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/ContractServiceImplTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/ContractServiceImplTest.java @@ -55,6 +55,7 @@ void setUp() { // given when(appContext.instantSource()).thenReturn(instantSource); when(appContext.signatureVerifier()).thenReturn(signatureVerifier); + when(appContext.metricsSupplier()).thenReturn(() -> null); subject = new ContractServiceImpl(appContext); } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/metrics/ContractMetricsTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/metrics/ContractMetricsTest.java new file mode 100644 index 000000000000..4b42a682ce97 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/metrics/ContractMetricsTest.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 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.test.exec.metrics; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.BDDMockito.given; + +import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.node.app.service.contract.impl.exec.metrics.ContractMetrics; +import com.hedera.node.config.data.ContractsConfig; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.swirlds.common.metrics.config.MetricsConfig; +import com.swirlds.common.metrics.platform.DefaultPlatformMetrics; +import com.swirlds.common.metrics.platform.MetricKeyRegistry; +import com.swirlds.common.metrics.platform.PlatformMetricsFactoryImpl; +import com.swirlds.common.platform.NodeId; +import com.swirlds.metrics.api.Metrics; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ContractMetricsTest { + + private final Supplier metricsSupplier = () -> fakeMetrics(); + + @Mock + private ContractsConfig contractsConfig; + + public @NonNull ContractMetrics getSubject() { + final var contractMetrics = new ContractMetrics(metricsSupplier, () -> contractsConfig); + contractMetrics.createContractMetrics(); + return contractMetrics; + } + + @Test + public void rejectedTxCountersGetBumpedWhenEnabled() { + given(contractsConfig.metricsSmartContractPrimaryEnabled()).willReturn(true); + + final var subject = getSubject(); + + subject.incrementRejectedTx(HederaFunctionality.CONTRACT_CALL); + subject.bumpRejectedTx(HederaFunctionality.CONTRACT_CREATE, 2); + subject.bumpRejectedTx(HederaFunctionality.ETHEREUM_TRANSACTION, 4); + + subject.bumpRejectedForGasTx(HederaFunctionality.CONTRACT_CALL, 10); + subject.bumpRejectedForGasTx(HederaFunctionality.CONTRACT_CREATE, 12); + subject.bumpRejectedForGasTx(HederaFunctionality.ETHEREUM_TRANSACTION, 14); + + subject.bumpRejectedType3EthTx(20); + + assertThat(subject.getAllCounterNames()) + .containsExactlyInAnyOrder( + "SmartContractService:Rejected-ethereumTxDueToIntrinsicGas_total", + "SmartContractService:Rejected-ethereumTx_total", + "SmartContractService:Rejected-contractCallTxDueToIntrinsicGas_total", + "SmartContractService:Rejected-contractCallTx_total", + "SmartContractService:Rejected-contractCreateTxDueToIntrinsicGas_total", + "SmartContractService:Rejected-contractCreateTx_total", + "SmartContractService:Rejected-ethType3BlobTransaction_total"); + assertThat(subject.getAllCounters()) + .containsExactlyInAnyOrderEntriesOf(Map.of( + "SmartContractService:Rejected-ethereumTxDueToIntrinsicGas_total", + 14L, + "SmartContractService:Rejected-ethereumTx_total", + 4L, + "SmartContractService:Rejected-contractCallTxDueToIntrinsicGas_total", + 10L, + "SmartContractService:Rejected-contractCallTx_total", + 1L, + "SmartContractService:Rejected-contractCreateTxDueToIntrinsicGas_total", + 12L, + "SmartContractService:Rejected-contractCreateTx_total", + 2L, + "SmartContractService:Rejected-ethType3BlobTransaction_total", + 20L)); + + // And there is no counter for this functionality + assertThrows(NullPointerException.class, () -> { + subject.bumpRejectedTx(HederaFunctionality.CRYPTO_APPROVE_ALLOWANCE, 22); + }); + } + + @Test + public void rejectedTxCountersGetIgnoredWhenDisabled() { + given(contractsConfig.metricsSmartContractPrimaryEnabled()).willReturn(false); + + final var subject = getSubject(); + + subject.incrementRejectedTx(HederaFunctionality.CONTRACT_CALL); + subject.bumpRejectedTx(HederaFunctionality.CONTRACT_CREATE, 2); + subject.bumpRejectedTx(HederaFunctionality.ETHEREUM_TRANSACTION, 4); + + subject.bumpRejectedForGasTx(HederaFunctionality.CONTRACT_CALL, 10); + subject.bumpRejectedForGasTx(HederaFunctionality.CONTRACT_CREATE, 12); + subject.bumpRejectedForGasTx(HederaFunctionality.ETHEREUM_TRANSACTION, 14); + + subject.bumpRejectedType3EthTx(20); + + assertThat(subject.getAllCounterNames()).isEmpty(); + assertThat(subject.getAllCounters()).isEmpty(); + } + + private static final long DEFAULT_NODE_ID = 3; + + public static Metrics fakeMetrics() { + final MetricsConfig metricsConfig = + HederaTestConfigBuilder.createConfig().getConfigData(MetricsConfig.class); + + return new DefaultPlatformMetrics( + NodeId.of(DEFAULT_NODE_ID), + new MetricKeyRegistry(), + Executors.newSingleThreadScheduledExecutor(), + new PlatformMetricsFactoryImpl(metricsConfig), + metricsConfig); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCallHandlerTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCallHandlerTest.java index d88040b65486..de1dab4291f9 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCallHandlerTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCallHandlerTest.java @@ -32,9 +32,11 @@ import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.contract.ContractCallTransactionBody; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.contract.impl.ContractServiceComponent; import com.hedera.node.app.service.contract.impl.exec.CallOutcome; import com.hedera.node.app.service.contract.impl.exec.ContextTransactionProcessor; import com.hedera.node.app.service.contract.impl.exec.TransactionComponent; +import com.hedera.node.app.service.contract.impl.exec.metrics.ContractMetrics; import com.hedera.node.app.service.contract.impl.handlers.ContractCallHandler; import com.hedera.node.app.service.contract.impl.records.ContractCallStreamBuilder; import com.hedera.node.app.service.contract.impl.state.RootProxyWorldUpdater; @@ -44,12 +46,16 @@ import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.config.data.ContractsConfig; +import com.swirlds.common.metrics.noop.NoOpMetrics; +import com.swirlds.metrics.api.Metrics; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Mock.Strictness; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -80,11 +86,22 @@ class ContractCallHandlerTest extends ContractHandlerTestBase { @Mock private GasCalculator gasCalculator; + @Mock(strictness = Strictness.LENIENT) + private ContractServiceComponent contractServiceComponent; + + @Mock + private ContractsConfig contractsConfig; + + private final Metrics metrics = new NoOpMetrics(); + private final ContractMetrics contractMetrics = new ContractMetrics(() -> metrics, () -> contractsConfig); + private ContractCallHandler subject; @BeforeEach void setUp() { - subject = new ContractCallHandler(() -> factory, gasCalculator); + contractMetrics.createContractMetrics(); + given(contractServiceComponent.contractMetrics()).willReturn(contractMetrics); + subject = new ContractCallHandler(() -> factory, gasCalculator, contractServiceComponent); } @Test diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCreateHandlerTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCreateHandlerTest.java index f0fc7e152f7c..75b78bdc1136 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCreateHandlerTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/ContractCreateHandlerTest.java @@ -36,9 +36,11 @@ import com.hedera.hapi.node.contract.ContractCreateTransactionBody; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.contract.impl.ContractServiceComponent; import com.hedera.node.app.service.contract.impl.exec.CallOutcome; import com.hedera.node.app.service.contract.impl.exec.ContextTransactionProcessor; import com.hedera.node.app.service.contract.impl.exec.TransactionComponent; +import com.hedera.node.app.service.contract.impl.exec.metrics.ContractMetrics; import com.hedera.node.app.service.contract.impl.handlers.ContractCreateHandler; import com.hedera.node.app.service.contract.impl.records.ContractCreateStreamBuilder; import com.hedera.node.app.service.contract.impl.state.RootProxyWorldUpdater; @@ -48,12 +50,16 @@ import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; import com.hedera.node.app.spi.workflows.HandleContext; import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.config.data.ContractsConfig; +import com.swirlds.common.metrics.noop.NoOpMetrics; +import com.swirlds.metrics.api.Metrics; import java.util.List; import org.hyperledger.besu.evm.gascalculator.GasCalculator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.Mock; +import org.mockito.Mock.Strictness; class ContractCreateHandlerTest extends ContractHandlerTestBase { @@ -86,11 +92,22 @@ class ContractCreateHandlerTest extends ContractHandlerTestBase { @Mock private GasCalculator gasCalculator; + @Mock(strictness = Strictness.LENIENT) + private ContractServiceComponent contractServiceComponent; + + @Mock + private ContractsConfig contractsConfig; + + private final Metrics metrics = new NoOpMetrics(); + private final ContractMetrics contractMetrics = new ContractMetrics(() -> metrics, () -> contractsConfig); + private ContractCreateHandler subject; @BeforeEach void setUp() { - subject = new ContractCreateHandler(() -> factory, gasCalculator); + contractMetrics.createContractMetrics(); + given(contractServiceComponent.contractMetrics()).willReturn(contractMetrics); + subject = new ContractCreateHandler(() -> factory, gasCalculator, contractServiceComponent); } @Test diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/EthereumTransactionHandlerTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/EthereumTransactionHandlerTest.java index 056586e0d673..1a9e870bd465 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/EthereumTransactionHandlerTest.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/EthereumTransactionHandlerTest.java @@ -42,11 +42,13 @@ import com.hedera.hapi.node.contract.EthereumTransactionBody; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.hapi.utils.ethereum.EthTxData; +import com.hedera.node.app.service.contract.impl.ContractServiceComponent; import com.hedera.node.app.service.contract.impl.exec.CallOutcome; import com.hedera.node.app.service.contract.impl.exec.ContextTransactionProcessor; import com.hedera.node.app.service.contract.impl.exec.TransactionComponent; import com.hedera.node.app.service.contract.impl.exec.TransactionProcessor; import com.hedera.node.app.service.contract.impl.exec.gas.CustomGasCharging; +import com.hedera.node.app.service.contract.impl.exec.metrics.ContractMetrics; import com.hedera.node.app.service.contract.impl.exec.tracers.EvmActionTracer; import com.hedera.node.app.service.contract.impl.handlers.EthereumTransactionHandler; import com.hedera.node.app.service.contract.impl.hevm.HederaEvmContext; @@ -71,7 +73,9 @@ import com.hedera.node.app.spi.workflows.PreHandleContext; import com.hedera.node.config.data.ContractsConfig; import com.hedera.pbj.runtime.io.buffer.Bytes; +import com.swirlds.common.metrics.noop.NoOpMetrics; import com.swirlds.config.api.Configuration; +import com.swirlds.metrics.api.Metrics; import java.util.List; import java.util.function.Supplier; import org.hyperledger.besu.evm.gascalculator.GasCalculator; @@ -79,6 +83,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Mock.Strictness; import org.mockito.MockedStatic; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -150,15 +155,24 @@ class EthereumTransactionHandlerTest { @Mock private EthTxData ethTxDataReturned; + @Mock(strictness = Strictness.LENIENT) + private ContractServiceComponent contractServiceComponent; + @Mock private Configuration configuration; @Mock private ContractsConfig contractsConfig; + private final Metrics metrics = new NoOpMetrics(); + private final ContractMetrics contractMetrics = new ContractMetrics(() -> metrics, () -> contractsConfig); + @BeforeEach void setUp() { - subject = new EthereumTransactionHandler(ethereumSignatures, callDataHydration, () -> factory, gasCalculator); + contractMetrics.createContractMetrics(); + given(contractServiceComponent.contractMetrics()).willReturn(contractMetrics); + subject = new EthereumTransactionHandler( + ethereumSignatures, callDataHydration, () -> factory, gasCalculator, contractServiceComponent); } void setUpTransactionProcessing() {