diff --git a/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java b/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java index c5d08d7a..ff777ceb 100644 --- a/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java +++ b/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java @@ -19,6 +19,10 @@ import java.io.IOException; import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -34,6 +38,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import linea.plugin.acc.test.tests.web3j.generated.SimpleStorage; +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.metrics.LineaMetricCategory; import org.apache.commons.lang3.RandomStringUtils; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -42,6 +48,8 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; +import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; import org.hyperledger.besu.tests.acceptance.dsl.AcceptanceTestBase; import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts; import org.hyperledger.besu.tests.acceptance.dsl.condition.txpool.TxPoolConditions; @@ -66,6 +74,7 @@ import org.web3j.tx.response.TransactionReceiptProcessor; /** Base class for plugin tests. */ +@Slf4j public class LineaPluginTestBase extends AcceptanceTestBase { public static final int MAX_CALLDATA_SIZE = 1188; // contract has a call data size of 1160 public static final int MAX_TX_GAS_LIMIT = DefaultGasProvider.GAS_LIMIT.intValue(); @@ -73,6 +82,7 @@ public class LineaPluginTestBase extends AcceptanceTestBase { public static final CliqueOptions LINEA_CLIQUE_OPTIONS = new CliqueOptions( CliqueOptions.DEFAULT.blockPeriodSeconds(), CliqueOptions.DEFAULT.epochLength(), false); + protected static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); protected BesuNode minerNode; @BeforeEach @@ -122,6 +132,11 @@ private BesuNode createCliqueNodeWithExtraCliOptionsAndRpcApis( .genesisConfigProvider( validators -> Optional.of(provideGenesisConfig(validators, cliqueOptions))) .extraCLIOptions(extraCliOptions) + .metricsConfiguration( + MetricsConfiguration.builder() + .enabled(true) + .metricCategories(Set.of(LineaMetricCategory.PROFITABILITY)) + .build()) .requestedPlugins( List.of( "LineaExtraDataPlugin", @@ -306,4 +321,33 @@ protected Bytes32 createExtraDataPricingField( return Bytes32.rightPad( Bytes.concatenate(Bytes.of((byte) 1), fixed.toBytes(), variable.toBytes(), min.toBytes())); } + + protected double getMetricValue( + final MetricCategory category, + final String metricName, + final List> labelValues) + throws IOException, InterruptedException { + + final var metricsReq = + HttpRequest.newBuilder().GET().uri(URI.create("http://127.0.0.1:9545/metrics")).build(); + + final var respLines = HTTP_CLIENT.send(metricsReq, HttpResponse.BodyHandlers.ofLines()); + + final var searchString = + category.getApplicationPrefix().orElse("") + + category.getName() + + "_" + + metricName + + labelValues.stream() + .map(lv -> lv.getKey() + "=\"" + lv.getValue() + "\"") + .collect(Collectors.joining(",", "{", ",}")); + + final var foundMetric = + respLines.body().filter(line -> line.startsWith(searchString)).findFirst(); + + return foundMetric + .map(line -> line.substring(searchString.length()).trim()) + .map(Double::valueOf) + .orElse(Double.NaN); + } } diff --git a/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTest.java b/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTest.java index b326675e..c6cab90a 100644 --- a/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTest.java +++ b/acceptance-tests/src/test/java/linea/plugin/acc/test/extradata/ExtraDataPricingTest.java @@ -14,6 +14,7 @@ */ package linea.plugin.acc.test.extradata; +import static java.util.Map.entry; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; @@ -22,6 +23,7 @@ import linea.plugin.acc.test.LineaPluginTestBase; import linea.plugin.acc.test.TestCommandLineOptionsBuilder; +import net.consensys.linea.metrics.LineaMetricCategory; import org.apache.tuweni.bytes.Bytes32; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.tests.acceptance.dsl.account.Account; @@ -78,7 +80,7 @@ public void updateMinGasPriceViaExtraData() { } @Test - public void updateProfitabilityParamsViaExtraData() throws IOException { + public void updateProfitabilityParamsViaExtraData() throws IOException, InterruptedException { final Web3j web3j = minerNode.nodeRequests().eth(); final Account sender = accounts.getSecondaryBenefactor(); final Account recipient = accounts.createAccount("recipient"); @@ -121,6 +123,28 @@ public void updateProfitabilityParamsViaExtraData() throws IOException { assertThat(signedUnprofitableTxResp.getError().getMessage()).isEqualTo("Gas price too low"); assertThat(getTxPoolContent()).isEmpty(); + + final var fixedCostMetric = + getMetricValue( + LineaMetricCategory.PROFITABILITY, "conf", List.of(entry("field", "fixed_cost_wei"))); + + assertThat(fixedCostMetric).isEqualTo(MIN_GAS_PRICE.multiply(2).getValue().doubleValue()); + + final var variableCostMetric = + getMetricValue( + LineaMetricCategory.PROFITABILITY, + "conf", + List.of(entry("field", "variable_cost_wei"))); + + assertThat(variableCostMetric).isEqualTo(MIN_GAS_PRICE.getValue().doubleValue()); + + final var ethGasPriceMetric = + getMetricValue( + LineaMetricCategory.PROFITABILITY, + "conf", + List.of(entry("field", "eth_gas_price_wei"))); + + assertThat(ethGasPriceMetric).isEqualTo(MIN_GAS_PRICE.getValue().doubleValue()); } static class MinerSetExtraDataRequest implements Transaction { diff --git a/gradle.properties b/gradle.properties index 3b1a8781..9b4da4f6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -releaseVersion=0.8.0-rc4.1 -besuVersion=24.10-delivery34 +releaseVersion=0.8.0-rc4.1-local +besuVersion=24.10-develop-829db23 arithmetizationVersion=0.8.0-rc4 besuArtifactGroup=io.consensys.linea-besu distributionIdentifier=linea-sequencer diff --git a/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java b/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java index 24085307..427796a1 100644 --- a/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java @@ -18,11 +18,9 @@ import lombok.extern.slf4j.Slf4j; import org.hyperledger.besu.plugin.BesuContext; import org.hyperledger.besu.plugin.BesuPlugin; -import org.hyperledger.besu.plugin.services.BlockchainService; @Slf4j -public abstract class AbstractLineaRequiredPlugin extends AbstractLineaPrivateOptionsPlugin { - protected BlockchainService blockchainService; +public abstract class AbstractLineaRequiredPlugin extends AbstractLineaSharedPrivateOptionsPlugin { /** * Linea plugins extending this class will halt startup of Besu in case of exception during @@ -38,14 +36,6 @@ public void register(final BesuContext context) { try { log.info("Registering Linea plugin {}", this.getClass().getName()); - blockchainService = - context - .getService(BlockchainService.class) - .orElseThrow( - () -> - new RuntimeException( - "Failed to obtain BlockchainService from the BesuContext.")); - doRegister(context); } catch (Exception e) { @@ -62,21 +52,4 @@ public void register(final BesuContext context) { * @param context the BesuContext to be used. */ public abstract void doRegister(final BesuContext context); - - @Override - public void start() { - super.start(); - - blockchainService - .getChainId() - .ifPresentOrElse( - chainId -> { - if (chainId.signum() <= 0) { - throw new IllegalArgumentException("Chain id must be greater than zero."); - } - }, - () -> { - throw new IllegalArgumentException("Chain id required"); - }); - } } diff --git a/sequencer/src/main/java/net/consensys/linea/AbstractLineaPrivateOptionsPlugin.java b/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java similarity index 63% rename from sequencer/src/main/java/net/consensys/linea/AbstractLineaPrivateOptionsPlugin.java rename to sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java index 569e6281..2473d6b8 100644 --- a/sequencer/src/main/java/net/consensys/linea/AbstractLineaPrivateOptionsPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java @@ -17,6 +17,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.compress.LibCompress; @@ -32,21 +33,35 @@ import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; import net.consensys.linea.config.LineaTransactionSelectorCliOptions; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.metrics.LineaMetricCategory; import net.consensys.linea.plugins.AbstractLineaSharedOptionsPlugin; import net.consensys.linea.plugins.LineaOptionsPluginConfiguration; +import org.hyperledger.besu.plugin.BesuContext; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.MetricCategoryRegistry; /** * This abstract class is used as superclass for all the plugins that share one or more - * configuration options. + * configuration options, services and common initializations. * *

Configuration options that are exclusive of a single plugin, are not required to be added * here, but they could stay in the class that implement a plugin, but in case that configuration * becomes to be used by multiple plugins, then to avoid code duplications and possible different * management of the options, it is better to move the configuration here so all plugins will * automatically see it. + * + *

Same for services and other initialization tasks, that are shared by more than one plugin, + * like registration of metrics categories or check to perform once at startup */ @Slf4j -public abstract class AbstractLineaPrivateOptionsPlugin extends AbstractLineaSharedOptionsPlugin { +public abstract class AbstractLineaSharedPrivateOptionsPlugin + extends AbstractLineaSharedOptionsPlugin { + protected static BlockchainService blockchainService; + protected static MetricsSystem metricsSystem; + + private static AtomicBoolean sharedRegisterTasksDone = new AtomicBoolean(false); + private static AtomicBoolean sharedStartTasksDone = new AtomicBoolean(false); static { // force the initialization of the gnark compress native library to fail fast in case of issues @@ -106,8 +121,68 @@ public LineaRejectedTxReportingConfiguration rejectedTxReportingConfiguration() getConfigurationByKey(LineaRejectedTxReportingCliOptions.CONFIG_KEY).optionsConfig(); } + @Override + public synchronized void register(final BesuContext context) { + super.register(context); + + if (sharedRegisterTasksDone.compareAndSet(false, true)) { + performSharedRegisterTasksOnce(context); + } + } + + protected void performSharedRegisterTasksOnce(final BesuContext context) { + blockchainService = + context + .getService(BlockchainService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain BlockchainService from the BesuContext.")); + + metricsSystem = + context + .getService(MetricsSystem.class) + .orElseThrow( + () -> new RuntimeException("Failed to obtain MetricSystem from the BesuContext.")); + + context + .getService(MetricCategoryRegistry.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain MetricCategoryRegistry from the BesuContext.")) + .addMetricCategory(LineaMetricCategory.PROFITABILITY); + } + @Override public void start() { super.start(); + + if (sharedStartTasksDone.compareAndSet(false, true)) { + performSharedStartTasksOnce(); + } + } + + private static void performSharedStartTasksOnce() { + blockchainService + .getChainId() + .ifPresentOrElse( + chainId -> { + if (chainId.signum() <= 0) { + throw new IllegalArgumentException("Chain id must be greater than zero."); + } + }, + () -> { + throw new IllegalArgumentException("Chain id required"); + }); + } + + @Override + public void stop() { + super.stop(); + sharedRegisterTasksDone.set(false); + sharedStartTasksDone.set(false); + blockchainService = null; + metricsSystem = null; } } diff --git a/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java b/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java index ed100ec3..504a7bf8 100644 --- a/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java @@ -20,6 +20,8 @@ import com.google.auto.service.AutoService; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.metrics.LineaMetricCategory; import org.hyperledger.besu.plugin.BesuContext; import org.hyperledger.besu.plugin.BesuPlugin; import org.hyperledger.besu.plugin.services.BesuEvents; @@ -97,5 +99,19 @@ public void start() { } }); } + + initMetrics(profitabilityConfiguration()); + } + + private void initMetrics(final LineaProfitabilityConfiguration lineaProfitabilityConfiguration) { + final var confLabelledGauge = + metricsSystem.createLabelledGauge( + LineaMetricCategory.PROFITABILITY, + "conf", + "Profitability configuration values at runtime", + "field"); + confLabelledGauge.labels(lineaProfitabilityConfiguration::fixedCostWei, "fixed_cost_wei"); + confLabelledGauge.labels(lineaProfitabilityConfiguration::variableCostWei, "variable_cost_wei"); + confLabelledGauge.labels(lineaProfitabilityConfiguration::ethGasPriceWei, "eth_gas_price_wei"); } } diff --git a/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java b/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java new file mode 100644 index 00000000..31ade658 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java @@ -0,0 +1,41 @@ +/* + * Copyright Consensys Software Inc. + * + * 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. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.metrics; + +import java.util.Optional; + +import org.hyperledger.besu.plugin.services.metrics.MetricCategory; + +public enum LineaMetricCategory implements MetricCategory { + /** Profitability metric category */ + PROFITABILITY("profitability"); + + private static final Optional APPLICATION_PREFIX = Optional.of("linea_"); + private final String name; + + LineaMetricCategory(final String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public Optional getApplicationPrefix() { + return APPLICATION_PREFIX; + } +}