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 a5d52571..6ad55f56 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 @@ -15,6 +15,9 @@ package linea.plugin.acc.test; +import static net.consensys.linea.metrics.LineaMetricCategory.PRICING_CONF; +import static net.consensys.linea.metrics.LineaMetricCategory.SEQUENCER_PROFITABILITY; +import static net.consensys.linea.metrics.LineaMetricCategory.TX_POOL_PROFITABILITY; import static org.assertj.core.api.Assertions.*; import java.io.IOException; @@ -40,7 +43,6 @@ import linea.plugin.acc.test.tests.web3j.generated.RevertExample; 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; @@ -136,7 +138,8 @@ private BesuNode createCliqueNodeWithExtraCliOptionsAndRpcApis( .metricsConfiguration( MetricsConfiguration.builder() .enabled(true) - .metricCategories(Set.of(LineaMetricCategory.PROFITABILITY)) + .metricCategories( + Set.of(PRICING_CONF, SEQUENCER_PROFITABILITY, TX_POOL_PROFITABILITY)) .build()) .requestedPlugins( List.of( 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 c6cab90a..fce533ba 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 @@ -15,6 +15,7 @@ package linea.plugin.acc.test.extradata; import static java.util.Map.entry; +import static net.consensys.linea.metrics.LineaMetricCategory.PRICING_CONF; import static org.assertj.core.api.Assertions.assertThat; import java.io.IOException; @@ -23,7 +24,6 @@ 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; @@ -125,24 +125,17 @@ public void updateProfitabilityParamsViaExtraData() throws IOException, Interrup assertThat(getTxPoolContent()).isEmpty(); final var fixedCostMetric = - getMetricValue( - LineaMetricCategory.PROFITABILITY, "conf", List.of(entry("field", "fixed_cost_wei"))); + getMetricValue(PRICING_CONF, "values", 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"))); + getMetricValue(PRICING_CONF, "values", 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"))); + getMetricValue(PRICING_CONF, "values", List.of(entry("field", "eth_gas_price_wei"))); assertThat(ethGasPriceMetric).isEqualTo(MIN_GAS_PRICE.getValue().doubleValue()); } diff --git a/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java b/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java index c8898c64..56aa94eb 100644 --- a/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java @@ -33,7 +33,6 @@ 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.ServiceManager; @@ -59,6 +58,7 @@ public abstract class AbstractLineaSharedPrivateOptionsPlugin extends AbstractLineaSharedOptionsPlugin { protected static BlockchainService blockchainService; protected static MetricsSystem metricsSystem; + protected static MetricCategoryRegistry metricCategoryRegistry; private static final AtomicBoolean sharedRegisterTasksDone = new AtomicBoolean(false); private static final AtomicBoolean sharedStartTasksDone = new AtomicBoolean(false); @@ -143,13 +143,13 @@ protected static void performSharedRegisterTasksOnce(final ServiceManager servic new RuntimeException( "Failed to obtain BlockchainService from the ServiceManager.")); - serviceManager - .getService(MetricCategoryRegistry.class) - .orElseThrow( - () -> - new RuntimeException( - "Failed to obtain MetricCategoryRegistry from the ServiceManager.")) - .addMetricCategory(LineaMetricCategory.PROFITABILITY); + metricCategoryRegistry = + serviceManager + .getService(MetricCategoryRegistry.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain MetricCategoryRegistry from the ServiceManager.")); } @Override diff --git a/sequencer/src/main/java/net/consensys/linea/bl/TransactionProfitabilityCalculator.java b/sequencer/src/main/java/net/consensys/linea/bl/TransactionProfitabilityCalculator.java index 3c71015f..6ef2e89a 100644 --- a/sequencer/src/main/java/net/consensys/linea/bl/TransactionProfitabilityCalculator.java +++ b/sequencer/src/main/java/net/consensys/linea/bl/TransactionProfitabilityCalculator.java @@ -113,6 +113,28 @@ public boolean isProfitable( final Wei profitablePriorityFee = profitablePriorityFeePerGas(transaction, minMargin, gas, minGasPriceWei); + + return isProfitable( + context, + profitablePriorityFee, + transaction, + minMargin, + baseFee, + payingGasPrice, + gas, + minGasPriceWei); + } + + public boolean isProfitable( + final String context, + final Wei profitablePriorityFee, + final Transaction transaction, + final double minMargin, + final Wei baseFee, + final Wei payingGasPrice, + final long gas, + final Wei minGasPriceWei) { + final Wei profitableGasPrice = baseFee.add(profitablePriorityFee); if (payingGasPrice.lessThan(profitableGasPrice)) { diff --git a/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java b/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java index 037c4a90..36ed3637 100644 --- a/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityCliOptions.java @@ -57,6 +57,12 @@ public class LineaProfitabilityCliOptions implements LineaCliOptions { "--plugin-linea-extra-data-set-min-gas-price-enabled"; public static final boolean DEFAULT_EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED = true; + public static final String PROFITABILITY_METRICS_BUCKETS = + "--plugin-linea-profitability-metrics-buckets"; + public static final double[] DEFAULT_PROFITABILITY_METRICS_BUCKETS = { + 0.1, 0.3, 0.5, 0.7, 0.9, 1.0, 1.1, 1.2, 1.5, 2.0, 5.0, 10.0 + }; + @Positive @CommandLine.Option( names = {FIXED_GAS_COST_WEI}, @@ -135,6 +141,17 @@ public class LineaProfitabilityCliOptions implements LineaCliOptions { "Enable setting min gas price runtime value via extra data field (default: ${DEFAULT-VALUE})") private boolean extraDataSetMinGasPriceEnabled = DEFAULT_EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED; + @CommandLine.Option( + names = {PROFITABILITY_METRICS_BUCKETS}, + arity = "1..*", + split = ",", + hidden = true, + paramLabel = "", + description = + "List of buckets to use to create the histogram for ratio between the effective priority fee " + + "and the calculate profitable priority of the tx (default: ${DEFAULT-VALUE})") + private double[] profitabilityMetricsBuckets = DEFAULT_PROFITABILITY_METRICS_BUCKETS; + private LineaProfitabilityCliOptions() {} /** @@ -164,6 +181,7 @@ public static LineaProfitabilityCliOptions fromConfig( options.txPoolCheckP2pEnabled = config.txPoolCheckP2pEnabled(); options.extraDataPricingEnabled = config.extraDataPricingEnabled(); options.extraDataSetMinGasPriceEnabled = config.extraDataSetMinGasPriceEnabled(); + options.profitabilityMetricsBuckets = config.profitabilityMetricsBuckets(); return options; } @@ -184,6 +202,7 @@ public LineaProfitabilityConfiguration toDomainObject() { .txPoolCheckP2pEnabled(txPoolCheckP2pEnabled) .extraDataPricingEnabled(extraDataPricingEnabled) .extraDataSetMinGasPriceEnabled(extraDataSetMinGasPriceEnabled) + .profitabilityMetricsBuckets(profitabilityMetricsBuckets) .build(); } @@ -199,6 +218,7 @@ public String toString() { .add(TX_POOL_ENABLE_CHECK_P2P, txPoolCheckP2pEnabled) .add(EXTRA_DATA_PRICING_ENABLED, extraDataPricingEnabled) .add(EXTRA_DATA_SET_MIN_GAS_PRICE_ENABLED, extraDataSetMinGasPriceEnabled) + .add(PROFITABILITY_METRICS_BUCKETS, profitabilityMetricsBuckets) .toString(); } } diff --git a/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java b/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java index 6aa5af24..012ad4d7 100644 --- a/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaProfitabilityConfiguration.java @@ -33,6 +33,7 @@ public class LineaProfitabilityConfiguration implements LineaOptionsConfiguratio /** It is safe to keep this as long, since it will store value <= max_int * 1000 */ private long variableCostWei; + /** It is safe to keep this as long, since it will store value <= max_int * 1000 */ private long ethGasPriceWei; private double minMargin; @@ -42,6 +43,7 @@ public class LineaProfitabilityConfiguration implements LineaOptionsConfiguratio private boolean txPoolCheckP2pEnabled; private boolean extraDataPricingEnabled; private boolean extraDataSetMinGasPriceEnabled; + private double[] profitabilityMetricsBuckets; /** * These 2 parameters must be atomically updated 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 907e3bd0..f43f477c 100644 --- a/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java @@ -15,13 +15,14 @@ package net.consensys.linea.extradata; +import static net.consensys.linea.metrics.LineaMetricCategory.PRICING_CONF; + import java.util.concurrent.atomic.AtomicBoolean; 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.BesuPlugin; import org.hyperledger.besu.plugin.ServiceManager; import org.hyperledger.besu.plugin.data.AddedBlockContext; @@ -46,6 +47,8 @@ public void doRegister(final ServiceManager context) { () -> new RuntimeException( "Failed to obtain RpcEndpointService from the ServiceManager.")); + + metricCategoryRegistry.addMetricCategory(PRICING_CONF); } /** @@ -95,16 +98,15 @@ public synchronized void onInitialSyncRestart() { }); } - initMetrics(profitabilityConfiguration()); + if (metricCategoryRegistry.isMetricCategoryEnabled(PRICING_CONF)) { + initMetrics(profitabilityConfiguration()); + } } private void initMetrics(final LineaProfitabilityConfiguration lineaProfitabilityConfiguration) { final var confLabelledGauge = metricsSystem.createLabelledSuppliedGauge( - LineaMetricCategory.PROFITABILITY, - "conf", - "Profitability configuration values at runtime", - "field"); + PRICING_CONF, "values", "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/HistogramMetrics.java b/sequencer/src/main/java/net/consensys/linea/metrics/HistogramMetrics.java new file mode 100644 index 00000000..a7702d38 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/metrics/HistogramMetrics.java @@ -0,0 +1,154 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.function.DoubleSupplier; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.metrics.Histogram; +import org.hyperledger.besu.plugin.services.metrics.LabelledMetric; +import org.hyperledger.besu.plugin.services.metrics.LabelledSuppliedMetric; + +@Slf4j +public class HistogramMetrics { + public interface LabelValue { + String value(); + } + + private static final String LABEL_VALUES_SEPARATOR = "\u2060"; + private final LabelledMetric histogram; + private final Map mins; + private final Map maxs; + + @SafeVarargs + public HistogramMetrics( + final MetricsSystem metricsSystem, + final LineaMetricCategory category, + final String name, + final String help, + final double[] buckets, + final Class... labels) { + + final var labelNames = getLabelNames(labels); + + final LabelledSuppliedMetric minRatio = + metricsSystem.createLabelledSuppliedGauge( + category, name + "_min", "Lowest " + help, labelNames); + + final LabelledSuppliedMetric maxRatio = + metricsSystem.createLabelledSuppliedGauge( + category, name + "_max", "Highest " + help, labelNames); + + final var combinations = getLabelValuesCombinations(labels); + mins = HashMap.newHashMap(combinations.size()); + maxs = HashMap.newHashMap(combinations.size()); + for (final var combination : combinations) { + final var key = String.join(LABEL_VALUES_SEPARATOR, combination); + final var minSupplier = new MutableDoubleSupplier(Double.POSITIVE_INFINITY); + mins.put(key, minSupplier); + minRatio.labels(minSupplier, combination); + final var maxSupplier = new MutableDoubleSupplier(Double.NEGATIVE_INFINITY); + maxs.put(key, maxSupplier); + maxRatio.labels(maxSupplier, combination); + } + + this.histogram = + metricsSystem.createLabelledHistogram( + category, name, StringUtils.capitalize(help) + " buckets", buckets, labelNames); + } + + @SafeVarargs + private String[] getLabelNames(final Class... labels) { + return Arrays.stream(labels) + .map(Class::getSimpleName) + .map(sn -> sn.toLowerCase(Locale.ROOT)) + .toArray(String[]::new); + } + + @SafeVarargs + private List getLabelValuesCombinations(final Class... labels) { + if (labels.length == 0) { + return Collections.singletonList(new String[0]); + } + if (labels.length == 1) { + return Arrays.stream(labels[0].getEnumConstants()) + .map(lv -> new String[] {lv.value()}) + .toList(); + } + final var head = labels[0]; + final var tail = Arrays.copyOfRange(labels, 1, labels.length); + final var tailCombinations = getLabelValuesCombinations(tail); + final int newSize = tailCombinations.size() * head.getEnumConstants().length; + final List combinations = new ArrayList<>(newSize); + for (final var headValue : head.getEnumConstants()) { + for (final var tailValues : tailCombinations) { + final var combination = new String[tailValues.length + 1]; + combination[0] = headValue.value(); + System.arraycopy(tailValues, 0, combination, 1, tailValues.length); + combinations.add(combination); + } + } + return combinations; + } + + public void track(final double value, final String... labelValues) { + + // Record the observation + histogram.labels(labelValues).observe(value); + } + + public void setMinMax(final double min, final double max, final String... labelValues) { + final var key = String.join(LABEL_VALUES_SEPARATOR, labelValues); + + // Update lowest seen + mins.get(key).set(min); + + // Update highest seen + maxs.get(key).set(max); + } + + private static class MutableDoubleSupplier implements DoubleSupplier { + private final double initialValue; + private volatile double value; + + public MutableDoubleSupplier(final double initialValue) { + this.initialValue = initialValue; + this.value = initialValue; + } + + @Override + public double getAsDouble() { + return value; + } + + public void set(final double value) { + this.value = value; + } + + public void reset() { + value = initialValue; + } + } +} diff --git a/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java b/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java index 31ade658..849e7ddb 100644 --- a/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java +++ b/sequencer/src/main/java/net/consensys/linea/metrics/LineaMetricCategory.java @@ -14,19 +14,25 @@ */ package net.consensys.linea.metrics; +import java.util.Locale; import java.util.Optional; import org.hyperledger.besu.plugin.services.metrics.MetricCategory; public enum LineaMetricCategory implements MetricCategory { - /** Profitability metric category */ - PROFITABILITY("profitability"); + /** Sequencer profitability metric category */ + SEQUENCER_PROFITABILITY, + /** Tx pool profitability metric category */ + TX_POOL_PROFITABILITY, + /** Runtime pricing configuration */ + PRICING_CONF; private static final Optional APPLICATION_PREFIX = Optional.of("linea_"); + private final String name; - LineaMetricCategory(final String name) { - this.name = name; + LineaMetricCategory() { + this.name = name().toLowerCase(Locale.ROOT); } @Override diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java index 4ca4a2ac..f20e9350 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java @@ -15,6 +15,7 @@ package net.consensys.linea.sequencer.txpoolvalidation; +import static net.consensys.linea.metrics.LineaMetricCategory.TX_POOL_PROFITABILITY; import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.createLimitModules; import java.io.File; @@ -30,12 +31,15 @@ import net.consensys.linea.AbstractLineaRequiredPlugin; import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.sequencer.txpoolvalidation.metrics.TransactionPoolProfitabilityMetrics; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.plugin.BesuPlugin; import org.hyperledger.besu.plugin.ServiceManager; import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BesuEvents; import org.hyperledger.besu.plugin.services.TransactionPoolValidatorService; import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.hyperledger.besu.plugin.services.transactionpool.TransactionPoolService; /** * This class extends the default transaction validation rules for adding transactions to the @@ -46,6 +50,7 @@ @Slf4j @AutoService(BesuPlugin.class) public class LineaTransactionPoolValidatorPlugin extends AbstractLineaRequiredPlugin { + private ServiceManager serviceManager; private BesuConfiguration besuConfiguration; private TransactionPoolValidatorService transactionPoolValidatorService; private TransactionSimulationService transactionSimulationService; @@ -53,6 +58,7 @@ public class LineaTransactionPoolValidatorPlugin extends AbstractLineaRequiredPl @Override public void doRegister(final ServiceManager serviceManager) { + this.serviceManager = serviceManager; besuConfiguration = serviceManager .getService(BesuConfiguration.class) @@ -76,6 +82,8 @@ public void doRegister(final ServiceManager serviceManager) { () -> new RuntimeException( "Failed to obtain TransactionSimulatorService from the ServiceManager.")); + + metricCategoryRegistry.addMetricCategory(TX_POOL_PROFITABILITY); } @Override @@ -113,6 +121,46 @@ public void start() { l1L2BridgeSharedConfiguration(), rejectedTxJsonRpcManager)); + if (metricCategoryRegistry.isMetricCategoryEnabled(TX_POOL_PROFITABILITY)) { + final var besuEventsService = + serviceManager + .getService(BesuEvents.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain BesuEvents from the ServiceManager.")); + + final var transactionPoolService = + serviceManager + .getService(TransactionPoolService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain TransactionPoolService from the ServiceManager.")); + + final var transactionPoolProfitabilityMetrics = + new TransactionPoolProfitabilityMetrics( + besuConfiguration, + metricsSystem, + profitabilityConfiguration(), + transactionPoolService, + blockchainService); + + besuEventsService.addBlockAddedListener( + addedBlockContext -> { + try { + // on new block let's calculate profitability for every txs in the pool + transactionPoolProfitabilityMetrics.update(); + } catch (final Exception e) { + log.warn( + "Error calculating transaction profitability for block {}({})", + addedBlockContext.getBlockHeader().getNumber(), + addedBlockContext.getBlockHeader().getBlockHash(), + e); + } + }); + } + } catch (Exception e) { throw new RuntimeException(e); } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/metrics/TransactionPoolProfitabilityMetrics.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/metrics/TransactionPoolProfitabilityMetrics.java new file mode 100644 index 00000000..fccc65fa --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/metrics/TransactionPoolProfitabilityMetrics.java @@ -0,0 +1,126 @@ +/* + * 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.sequencer.txpoolvalidation.metrics; + +import static net.consensys.linea.metrics.LineaMetricCategory.TX_POOL_PROFITABILITY; + +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.bl.TransactionProfitabilityCalculator; +import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.metrics.HistogramMetrics; +import org.apache.tuweni.units.bigints.UInt256s; +import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.datatypes.Transaction; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.plugin.services.BlockchainService; +import org.hyperledger.besu.plugin.services.MetricsSystem; +import org.hyperledger.besu.plugin.services.transactionpool.TransactionPoolService; + +/** + * Tracks profitability metrics for transactions in the transaction pool. Specifically monitors the + * ratio of profitable priority fee to actual priority fee: + * profitablePriorityFeePerGas/transaction.priorityFeePerGas + * + *

Provides: - Lowest ratio seen (minimum profitability) - Highest ratio seen (maximum + * profitability) - Distribution histogram of ratios + */ +@Slf4j +public class TransactionPoolProfitabilityMetrics { + private final TransactionProfitabilityCalculator profitabilityCalculator; + private final LineaProfitabilityConfiguration profitabilityConf; + private final BesuConfiguration besuConfiguration; + private final TransactionPoolService transactionPoolService; + private final BlockchainService blockchainService; + private final HistogramMetrics histogramMetrics; + + public TransactionPoolProfitabilityMetrics( + final BesuConfiguration besuConfiguration, + final MetricsSystem metricsSystem, + final LineaProfitabilityConfiguration profitabilityConf, + final TransactionPoolService transactionPoolService, + final BlockchainService blockchainService) { + + this.besuConfiguration = besuConfiguration; + this.profitabilityConf = profitabilityConf; + this.profitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf); + this.transactionPoolService = transactionPoolService; + this.blockchainService = blockchainService; + this.histogramMetrics = + new HistogramMetrics( + metricsSystem, + TX_POOL_PROFITABILITY, + "ratio", + "transaction pool profitability ratio", + profitabilityConf.profitabilityMetricsBuckets()); + } + + public void update() { + final long startTime = System.currentTimeMillis(); + final var txPoolContent = transactionPoolService.getPendingTransactions(); + + final var ratioStats = + txPoolContent.parallelStream() + .map(PendingTransaction::getTransaction) + .map( + tx -> { + final var ratio = handleTransaction(tx); + histogramMetrics.track(ratio); + log.trace("Recorded profitability ratio {} for tx {}", ratio, tx.getHash()); + return ratio; + }) + .collect(Collectors.summarizingDouble(Double::doubleValue)); + + histogramMetrics.setMinMax(ratioStats.getMin(), ratioStats.getMax()); + + log.atDebug() + .setMessage("Transaction pool profitability metrics processed {}txs in {}ms, statistics {}") + .addArgument(txPoolContent::size) + .addArgument(() -> System.currentTimeMillis() - startTime) + .addArgument(ratioStats) + .log(); + } + + private double handleTransaction(final Transaction transaction) { + final Wei actualPriorityFeePerGas; + if (transaction.getMaxPriorityFeePerGas().isEmpty()) { + actualPriorityFeePerGas = + Wei.fromQuantity(transaction.getGasPrice().orElseThrow()) + .subtract(blockchainService.getNextBlockBaseFee().orElseThrow()); + } else { + final Wei maxPriorityFeePerGas = + Wei.fromQuantity(transaction.getMaxPriorityFeePerGas().get()); + actualPriorityFeePerGas = + UInt256s.min( + maxPriorityFeePerGas.add(blockchainService.getNextBlockBaseFee().orElseThrow()), + Wei.fromQuantity(transaction.getMaxFeePerGas().orElseThrow())); + } + + final Wei profitablePriorityFeePerGas = + profitabilityCalculator.profitablePriorityFeePerGas( + transaction, + profitabilityConf.txPoolMinMargin(), + transaction.getGasLimit(), + besuConfiguration.getMinGasPrice()); + + final double ratio = + actualPriorityFeePerGas.toBigInteger().doubleValue() + / profitablePriorityFeePerGas.toBigInteger().doubleValue(); + + return ratio; + } +} diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java index 0d5fc6ff..529e9bb0 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java @@ -22,6 +22,7 @@ import net.consensys.linea.config.LineaTracerConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.metrics.HistogramMetrics; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import net.consensys.linea.sequencer.txselection.selectors.LineaTransactionSelector; import org.hyperledger.besu.plugin.services.BlockchainService; @@ -39,6 +40,7 @@ public class LineaTransactionSelectorFactory implements PluginTransactionSelecto private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; private final LineaProfitabilityConfiguration profitabilityConfiguration; private final LineaTracerConfiguration tracerConfiguration; + private final Optional maybeProfitabilityMetrics; private final Map limitsMap; @@ -49,7 +51,8 @@ public LineaTransactionSelectorFactory( final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, final Map limitsMap, - final Optional rejectedTxJsonRpcManager) { + final Optional rejectedTxJsonRpcManager, + final Optional maybeProfitabilityMetrics) { this.blockchainService = blockchainService; this.txSelectorConfiguration = txSelectorConfiguration; this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; @@ -57,6 +60,7 @@ public LineaTransactionSelectorFactory( this.tracerConfiguration = tracerConfiguration; this.limitsMap = limitsMap; this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; + this.maybeProfitabilityMetrics = maybeProfitabilityMetrics; } @Override @@ -68,6 +72,7 @@ public PluginTransactionSelector create() { profitabilityConfiguration, tracerConfiguration, limitsMap, - rejectedTxJsonRpcManager); + rejectedTxJsonRpcManager, + maybeProfitabilityMetrics); } } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java index e25a0c03..7be6cc93 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java @@ -15,6 +15,7 @@ package net.consensys.linea.sequencer.txselection; +import static net.consensys.linea.metrics.LineaMetricCategory.SEQUENCER_PROFITABILITY; import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.createLimitModules; import java.util.Optional; @@ -25,6 +26,8 @@ import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.metrics.HistogramMetrics; +import net.consensys.linea.sequencer.txselection.selectors.ProfitableTransactionSelector; import org.hyperledger.besu.plugin.BesuPlugin; import org.hyperledger.besu.plugin.ServiceManager; import org.hyperledger.besu.plugin.services.BesuConfiguration; @@ -38,12 +41,15 @@ @Slf4j @AutoService(BesuPlugin.class) public class LineaTransactionSelectorPlugin extends AbstractLineaRequiredPlugin { + public static final String NAME = "linea"; + private ServiceManager serviceManager; private TransactionSelectionService transactionSelectionService; private Optional rejectedTxJsonRpcManager = Optional.empty(); private BesuConfiguration besuConfiguration; @Override public void doRegister(final ServiceManager serviceManager) { + this.serviceManager = serviceManager; transactionSelectionService = serviceManager .getService(TransactionSelectionService.class) @@ -59,6 +65,7 @@ public void doRegister(final ServiceManager serviceManager) { () -> new RuntimeException( "Failed to obtain BesuConfiguration from the ServiceManager.")); + metricCategoryRegistry.addMetricCategory(SEQUENCER_PROFITABILITY); } @Override @@ -78,6 +85,19 @@ public void start() { besuConfiguration.getDataPath(), lineaRejectedTxReportingConfiguration) .start()); + + final Optional maybeProfitabilityMetrics = + metricCategoryRegistry.isMetricCategoryEnabled(SEQUENCER_PROFITABILITY) + ? Optional.of( + new HistogramMetrics( + metricsSystem, + SEQUENCER_PROFITABILITY, + "ratio", + "sequencer profitability ratio", + profitabilityConfiguration().profitabilityMetricsBuckets(), + ProfitableTransactionSelector.Phase.class)) + : Optional.empty(); + transactionSelectionService.registerPluginTransactionSelectorFactory( new LineaTransactionSelectorFactory( blockchainService, @@ -86,7 +106,8 @@ public void start() { profitabilityConfiguration(), tracerConfiguration(), createLimitModules(tracerConfiguration()), - rejectedTxJsonRpcManager)); + rejectedTxJsonRpcManager, + maybeProfitabilityMetrics)); } @Override diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java index 2d6586d4..61e2b883 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java @@ -30,6 +30,7 @@ import net.consensys.linea.config.LineaTransactionSelectorConfiguration; import net.consensys.linea.jsonrpc.JsonRpcManager; import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; +import net.consensys.linea.metrics.HistogramMetrics; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.plugin.data.TransactionProcessingResult; @@ -55,7 +56,8 @@ public LineaTransactionSelector( final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, final Map limitsMap, - final Optional rejectedTxJsonRpcManager) { + final Optional rejectedTxJsonRpcManager, + final Optional maybeProfitabilityMetrics) { this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; // only report rejected transaction selection result from TraceLineLimitTransactionSelector @@ -71,7 +73,8 @@ public LineaTransactionSelector( l1L2BridgeConfiguration, profitabilityConfiguration, tracerConfiguration, - limitsMap); + limitsMap, + maybeProfitabilityMetrics); } /** @@ -81,6 +84,7 @@ public LineaTransactionSelector( * @param txSelectorConfiguration The configuration to use. * @param profitabilityConfiguration The profitability configuration. * @param limitsMap The limits map. + * @param maybeProfitabilityMetrics The optional profitability metrics * @return A list of selectors. */ private List createTransactionSelectors( @@ -89,7 +93,8 @@ private List createTransactionSelectors( final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, - final Map limitsMap) { + final Map limitsMap, + final Optional maybeProfitabilityMetrics) { traceLineLimitTransactionSelector = new TraceLineLimitTransactionSelector( @@ -103,7 +108,10 @@ private List createTransactionSelectors( new MaxBlockCallDataTransactionSelector(txSelectorConfiguration.maxBlockCallDataSize()), new MaxBlockGasTransactionSelector(txSelectorConfiguration.maxGasPerBlock()), new ProfitableTransactionSelector( - blockchainService, txSelectorConfiguration, profitabilityConfiguration), + blockchainService, + txSelectorConfiguration, + profitabilityConfiguration, + maybeProfitabilityMetrics), traceLineLimitTransactionSelector); } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelector.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelector.java index 0ba8e371..2cd6768f 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelector.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelector.java @@ -19,7 +19,11 @@ import static net.consensys.linea.sequencer.txselection.LineaTransactionSelectionResult.TX_UNPROFITABLE_UPFRONT; import static org.hyperledger.besu.plugin.data.TransactionSelectionResult.SELECTED; +import java.util.EnumMap; import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; import java.util.Set; import com.google.common.annotations.VisibleForTesting; @@ -27,6 +31,8 @@ import net.consensys.linea.bl.TransactionProfitabilityCalculator; import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.metrics.HistogramMetrics; +import net.consensys.linea.metrics.HistogramMetrics.LabelValue; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.datatypes.Transaction; @@ -48,11 +54,34 @@ */ @Slf4j public class ProfitableTransactionSelector implements PluginTransactionSelector { + public enum Phase implements LabelValue { + PRE_PROCESSING, + POST_PROCESSING; + + final String value; + + Phase() { + this.value = name().toLowerCase(Locale.ROOT); + } + + @Override + public String value() { + return value; + } + } + @VisibleForTesting protected static Set unprofitableCache = new LinkedHashSet<>(); + protected static Map lastBlockMinRatios = new EnumMap<>(Phase.class); + protected static Map lastBlockMaxRatios = new EnumMap<>(Phase.class); + + static { + resetMinMaxRatios(); + } private final LineaTransactionSelectorConfiguration txSelectorConf; private final LineaProfitabilityConfiguration profitabilityConf; private final TransactionProfitabilityCalculator transactionProfitabilityCalculator; + private final Optional maybeProfitabilityMetrics; private final Wei baseFee; private int unprofitableRetries; @@ -60,11 +89,34 @@ public class ProfitableTransactionSelector implements PluginTransactionSelector public ProfitableTransactionSelector( final BlockchainService blockchainService, final LineaTransactionSelectorConfiguration txSelectorConf, - final LineaProfitabilityConfiguration profitabilityConf) { + final LineaProfitabilityConfiguration profitabilityConf, + final Optional maybeProfitabilityMetrics) { this.txSelectorConf = txSelectorConf; this.profitabilityConf = profitabilityConf; this.transactionProfitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf); + this.maybeProfitabilityMetrics = maybeProfitabilityMetrics; + maybeProfitabilityMetrics.ifPresent( + histogramMetrics -> { + // temporary solution to update min and max metrics + // we should do this just after the block is created, but we do not have any API for that + // so we postponed the update asap the next block creation starts. + histogramMetrics.setMinMax( + lastBlockMinRatios.get(Phase.PRE_PROCESSING), + lastBlockMaxRatios.get(Phase.PRE_PROCESSING), + Phase.PRE_PROCESSING.value()); + histogramMetrics.setMinMax( + lastBlockMinRatios.get(Phase.POST_PROCESSING), + lastBlockMaxRatios.get(Phase.POST_PROCESSING), + Phase.POST_PROCESSING.value()); + log.atTrace() + .setMessage("Setting profitability ratio metrics for last block to min={}, max={}") + .addArgument(lastBlockMinRatios) + .addArgument(lastBlockMaxRatios) + .log(); + resetMinMaxRatios(); + }); + this.baseFee = blockchainService .getNextBlockBaseFee() @@ -94,9 +146,17 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( final Transaction transaction = evaluationContext.getPendingTransaction().getTransaction(); final long gasLimit = transaction.getGasLimit(); + final var profitablePriorityFeePerGas = + transactionProfitabilityCalculator.profitablePriorityFeePerGas( + transaction, profitabilityConf.minMargin(), gasLimit, minGasPrice); + + updateMetric( + Phase.PRE_PROCESSING, evaluationContext, transaction, profitablePriorityFeePerGas); + // check the upfront profitability using the gas limit of the tx if (!transactionProfitabilityCalculator.isProfitable( "PreProcessing", + profitablePriorityFeePerGas, transaction, profitabilityConf.minMargin(), baseFee, @@ -145,8 +205,19 @@ public TransactionSelectionResult evaluateTransactionPostProcessing( final Transaction transaction = evaluationContext.getPendingTransaction().getTransaction(); final long gasUsed = processingResult.getEstimateGasUsedByTransaction(); + final var profitablePriorityFeePerGas = + transactionProfitabilityCalculator.profitablePriorityFeePerGas( + transaction, + profitabilityConf.minMargin(), + gasUsed, + evaluationContext.getMinGasPrice()); + + updateMetric( + Phase.POST_PROCESSING, evaluationContext, transaction, profitablePriorityFeePerGas); + if (!transactionProfitabilityCalculator.isProfitable( "PostProcessing", + profitablePriorityFeePerGas, transaction, profitabilityConf.minMargin(), baseFee, @@ -185,9 +256,9 @@ public void onTransactionSelected( public void onTransactionNotSelected( final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult transactionSelectionResult) { + final var txHash = evaluationContext.getPendingTransaction().getTransaction().getHash(); if (transactionSelectionResult.discard()) { - unprofitableCache.remove( - evaluationContext.getPendingTransaction().getTransaction().getHash()); + unprofitableCache.remove(txHash); } } @@ -202,4 +273,48 @@ private void rememberUnprofitable(final Transaction transaction) { unprofitableCache.add(transaction.getHash()); log.atTrace().setMessage("unprofitableCache={}").addArgument(unprofitableCache::size).log(); } + + private void updateMetric( + final Phase label, + final TransactionEvaluationContext evaluationContext, + final Transaction tx, + final Wei profitablePriorityFeePerGas) { + + maybeProfitabilityMetrics.ifPresent( + histogramMetrics -> { + final var effectivePriorityFee = + evaluationContext.getTransactionGasPrice().subtract(baseFee); + final var ratio = + effectivePriorityFee.getValue().doubleValue() + / profitablePriorityFeePerGas.getValue().doubleValue(); + + histogramMetrics.track(ratio, label.value()); + + if (ratio < lastBlockMinRatios.get(label)) { + lastBlockMinRatios.put(label, ratio); + } + if (ratio > lastBlockMaxRatios.get(label)) { + lastBlockMaxRatios.put(label, ratio); + } + + log.atTrace() + .setMessage( + "POST_PROCESSING: block[{}] tx {} , baseFee {}, effectiveGasPrice {}, ratio (effectivePayingPriorityFee {} / calculatedProfitablePriorityFee {}) {}") + .addArgument(evaluationContext.getPendingBlockHeader().getNumber()) + .addArgument(tx.getHash()) + .addArgument(baseFee::toHumanReadableString) + .addArgument(evaluationContext.getTransactionGasPrice()::toHumanReadableString) + .addArgument(effectivePriorityFee::toHumanReadableString) + .addArgument(profitablePriorityFeePerGas::toHumanReadableString) + .addArgument(ratio) + .log(); + }); + } + + private static void resetMinMaxRatios() { + lastBlockMinRatios.put(Phase.PRE_PROCESSING, Double.POSITIVE_INFINITY); + lastBlockMinRatios.put(Phase.POST_PROCESSING, Double.POSITIVE_INFINITY); + lastBlockMaxRatios.put(Phase.PRE_PROCESSING, Double.NEGATIVE_INFINITY); + lastBlockMaxRatios.put(Phase.POST_PROCESSING, Double.NEGATIVE_INFINITY); + } } diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java index 164e7e30..ff3b08b9 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java @@ -28,6 +28,7 @@ import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTransactionSelectorCliOptions; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.metrics.HistogramMetrics; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.bouncycastle.crypto.digests.KeccakDigest; @@ -73,7 +74,7 @@ private TestableProfitableTransactionSelector newSelectorForNewBlock() { final var blockchainService = mock(BlockchainService.class); when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(BASE_FEE)); return new TestableProfitableTransactionSelector( - blockchainService, txSelectorConf, profitabilityConf); + blockchainService, txSelectorConf, profitabilityConf, Optional.empty()); } @Test @@ -410,8 +411,9 @@ private static class TestableProfitableTransactionSelector extends ProfitableTra TestableProfitableTransactionSelector( final BlockchainService blockchainService, final LineaTransactionSelectorConfiguration txSelectorConf, - final LineaProfitabilityConfiguration profitabilityConf) { - super(blockchainService, txSelectorConf, profitabilityConf); + final LineaProfitabilityConfiguration profitabilityConf, + final Optional maybeProfitabilityMetrics) { + super(blockchainService, txSelectorConf, profitabilityConf, maybeProfitabilityMetrics); } boolean isUnprofitableTxCached(final Hash txHash) {