diff --git a/besu-plugins/linea-sequencer/acceptance-tests/build.gradle b/besu-plugins/linea-sequencer/acceptance-tests/build.gradle index 41987b569e2..7f5dae51851 100644 --- a/besu-plugins/linea-sequencer/acceptance-tests/build.gradle +++ b/besu-plugins/linea-sequencer/acceptance-tests/build.gradle @@ -80,8 +80,9 @@ dependencies { kapt("org.apache.logging.log4j:log4j-core:${libs.versions.log4j.get()}") annotationProcessor "org.apache.logging.log4j:log4j-core:${libs.versions.log4j.get()}" implementation "org.apache.logging.log4j:log4j-core:${libs.versions.log4j.get()}" - implementation project("${lineaSequencerProjectPath}:sequencer") + implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" + testImplementation "build.linea:blob-compressor:${libs.versions.blobCompressor.get()}" testImplementation project(":tracer:arithmetization") testImplementation project(":jvm-libs:generic:serialization:jackson") diff --git a/besu-plugins/linea-sequencer/acceptance-tests/src/test/kotlin/linea/plugin/acc/test/rpc/linea/EstimateGasTest.kt b/besu-plugins/linea-sequencer/acceptance-tests/src/test/kotlin/linea/plugin/acc/test/rpc/linea/EstimateGasTest.kt index ab51014aac7..e80256cf53e 100644 --- a/besu-plugins/linea-sequencer/acceptance-tests/src/test/kotlin/linea/plugin/acc/test/rpc/linea/EstimateGasTest.kt +++ b/besu-plugins/linea-sequencer/acceptance-tests/src/test/kotlin/linea/plugin/acc/test/rpc/linea/EstimateGasTest.kt @@ -17,8 +17,8 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.ObjectReader import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import linea.blob.BlobCompressorSelectorByTimestamp import linea.blob.BlobCompressorVersion -import linea.blob.GoBackedBlobCompressor import linea.plugin.acc.test.LineaPluginPoSTestBase import linea.plugin.acc.test.TestCommandLineOptionsBuilder import net.consensys.linea.bl.TransactionProfitabilityCalculator @@ -47,6 +47,7 @@ import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.charset.StandardCharsets +import kotlin.time.Instant open class EstimateGasTest : LineaPluginPoSTestBase() { protected lateinit var profitabilityCalculator: TransactionProfitabilityCalculator @@ -79,7 +80,12 @@ open class EstimateGasTest : LineaPluginPoSTestBase() { .build() profitabilityCalculator = TransactionProfitabilityCalculator( profitabilityConf, - CachingTransactionCompressor(GoBackedBlobCompressor.getInstance(BlobCompressorVersion.V2, 128 * 1024)), + CachingTransactionCompressor( + BlobCompressorSelectorByTimestamp( + mapOf(BlobCompressorVersion.V2 to Instant.DISTANT_PAST), + 128 * 1024, + ), + ), ) } diff --git a/besu-plugins/linea-sequencer/gradle/dist.gradle b/besu-plugins/linea-sequencer/gradle/dist.gradle index f9fe4ba466c..787a6c8b2ed 100644 --- a/besu-plugins/linea-sequencer/gradle/dist.gradle +++ b/besu-plugins/linea-sequencer/gradle/dist.gradle @@ -33,4 +33,12 @@ jar { 'Implementation-Version': calculateVersion() ) } + + // Explicitly include Kotlin stdlib in the JAR to ensure kotlin.time.Instant and other + // Kotlin standard library classes are available at runtime when the plugin is loaded by Besu + from { + configurations.runtimeClasspath + .filter { it.name.startsWith('kotlin-stdlib') } + .collect { zipTree(it) } + } } diff --git a/besu-plugins/linea-sequencer/sequencer/build.gradle b/besu-plugins/linea-sequencer/sequencer/build.gradle index bbd0ad7df13..449e8094371 100644 --- a/besu-plugins/linea-sequencer/sequencer/build.gradle +++ b/besu-plugins/linea-sequencer/sequencer/build.gradle @@ -20,10 +20,25 @@ apply from: lineaSequencerProject.file("gradle/java.gradle") apply from: lineaSequencerProject.file("gradle/dependency-management.gradle") apply from: lineaSequencerProject.file("gradle/lint.gradle") +// Force a single version of kotlin-stdlib across all transitive dependencies. +// Multiple dependencies (blob-compressor, tuweni, dagger-compiler, etc.) each bring in different +// versions of kotlin-stdlib (1.9.10, 2.0.21, 2.1.0, 2.3.0, etc.), causing version conflicts. +// This resolution strategy ensures we use the canonical Kotlin version (libs.versions.kotlin) +// throughout the entire dependency tree, preventing "Cannot find a version that satisfies +// all constraints" errors and ensuring consistent Kotlin behavior at runtime. +configurations.all { + resolutionStrategy.eachDependency { details -> + if (details.requested.group == 'org.jetbrains.kotlin' && details.requested.name.startsWith('kotlin-stdlib')) { + details.useVersion "${libs.versions.kotlin.get()}" + } + } +} + dependencies { implementation project(":tracer:arithmetization") implementation "build.linea:blob-compressor:${libs.versions.blobCompressor.get()}" implementation "build.linea.internal:kotlin:${libs.versions.lineaKotlin.get()}" + implementation "org.jetbrains.kotlin:kotlin-stdlib:${libs.versions.kotlin.get()}" testImplementation 'org.assertj:assertj-core' diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java index 87f7128a677..c568d3891c9 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/AbstractLineaSharedPrivateOptionsPlugin.java @@ -12,9 +12,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; -import linea.blob.BlobCompressor; -import linea.blob.BlobCompressorVersion; -import linea.blob.GoBackedBlobCompressor; +import linea.blob.BlobCompressorSelectorByTimestamp; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.bl.TransactionProfitabilityCalculator; import net.consensys.linea.bundles.BundlePoolService; @@ -84,7 +82,7 @@ public abstract class AbstractLineaSharedPrivateOptionsPlugin protected static MetricCategoryRegistry metricCategoryRegistry; protected static RpcEndpointService rpcEndpointService; protected static InvalidTransactionByLineCountCache invalidTransactionByLineCountCache; - protected static BlobCompressor blobCompressor; + protected static BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp; protected static TransactionCompressor transactionCompressor; protected static TransactionProfitabilityCalculator transactionProfitabilityCalculator; @@ -300,13 +298,15 @@ private void performSharedStartTasksOnce(final ServiceManager serviceManager) { transactionSelectorConfiguration().blobSizeLimit() != null ? transactionSelectorConfiguration().blobSizeLimit() : DEFAULT_COMPRESSED_SIZE_LIMIT; - blobCompressor = - GoBackedBlobCompressor.getInstance(BlobCompressorVersion.V2, effectiveBlobLimit); + blobCompressorSelectorByTimestamp = + new BlobCompressorSelectorByTimestamp( + transactionSelectorConfiguration().blobCompressorVersionActivationTimes(), + effectiveBlobLimit); final LineaProfitabilityConfiguration profitabilityConfiguration = profitabilityConfiguration(); transactionCompressor = new CachingTransactionCompressor( - profitabilityConfiguration.compressedTxCacheSize(), blobCompressor); + profitabilityConfiguration.compressedTxCacheSize(), blobCompressorSelectorByTimestamp); transactionProfitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConfiguration, transactionCompressor); } @@ -318,6 +318,6 @@ public void stop() { sharedStartTasksDone.set(false); blockchainService = null; metricsSystem = null; - blobCompressor = null; + blobCompressorSelectorByTimestamp = null; } } diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java index a0e03ef9e1a..5f1b69f0602 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java @@ -9,18 +9,24 @@ package net.consensys.linea.config; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import jakarta.validation.constraints.Positive; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; +import kotlin.time.Instant; +import linea.blob.BlobCompressorVersion; import net.consensys.linea.plugins.LineaCliOptions; import net.consensys.linea.sequencer.txselection.selectors.TransactionEventFilter; import org.apache.tuweni.bytes.Bytes32; @@ -52,6 +58,10 @@ public class LineaTransactionSelectorCliOptions implements LineaCliOptions { public static final String EVENTS_DENY_LIST_PATH = "--plugin-linea-events-deny-list-path"; public static final String EVENTS_BUNDLE_DENY_LIST_PATH = "--plugin-linea-events-bundle-deny-list-path"; + public static final String BLOB_COMPRESSOR_VERSION_TIMESTAMPS = + "--plugin-linea-blob-compressor-version-timestamps"; + public static final String BLOB_COMPRESSOR_VERSION_TIMESTAMPS_DEFAULT = + String.format("%s=%s", BlobCompressorVersion.V2.name(), Instant.Companion.getDISTANT_PAST()); @Positive @CommandLine.Option( @@ -132,7 +142,24 @@ public class LineaTransactionSelectorCliOptions implements LineaCliOptions { description = "Path to the file containing the events deny list for bundles") private String eventsBundleDenyListPath; - private LineaTransactionSelectorCliOptions() {} + private static final class BlobCompressorVersionCandidates implements Iterable { + @Override + public Iterator iterator() { + return Arrays.stream(BlobCompressorVersion.values()).map(Enum::name).iterator(); + } + } + + @CommandLine.Option( + names = {BLOB_COMPRESSOR_VERSION_TIMESTAMPS}, + hidden = true, + paramLabel = "", + completionCandidates = BlobCompressorVersionCandidates.class, + description = + "Comma-separated map of BlobCompressorVersion to Instant, " + + "e.g. V1_2=2025-01-01T00:00:00Z,V2=2026-01-01T00:00:00Z ." + + "Available versions: ${COMPLETION-CANDIDATES} . " + + "(default: ${DEFAULT-VALUE})") + private String blobCompressorVersionTimestampsRaw = BLOB_COMPRESSOR_VERSION_TIMESTAMPS_DEFAULT; /** * Create Linea cli options. @@ -181,6 +208,7 @@ public LineaTransactionSelectorConfiguration toDomainObject() { .eventsDenyList(parseTransactionEventDenyList(eventsDenyListPath)) .eventsBundleDenyListPath(eventsBundleDenyListPath) .eventsBundleDenyList(parseTransactionEventDenyList(eventsBundleDenyListPath)) + .blobCompressorVersionActivationTimes(getBlobCompressorVersionTimestamps()) .build(); } @@ -196,6 +224,7 @@ public String toString() { .add(MAX_BUNDLE_POOL_SIZE_BYTES, maxBundlePoolSizeBytes) .add(EVENTS_DENY_LIST_PATH, eventsDenyListPath) .add(EVENTS_BUNDLE_DENY_LIST_PATH, eventsBundleDenyListPath) + .add(BLOB_COMPRESSOR_VERSION_TIMESTAMPS, blobCompressorVersionTimestampsRaw) .toString(); } @@ -233,4 +262,24 @@ public Map> parseTransactionEventDenyList( throw new RuntimeException(e); } } + + @VisibleForTesting + Map parseBlobCompressorVersionTimestamps(String input) { + Map result = new HashMap<>(); + String[] pairs = input.split(","); + for (String pair : pairs) { + String[] kv = pair.split("="); + if (kv.length != 2) { + throw new IllegalArgumentException("Invalid BlobCompressorVersion=Instant pair: " + pair); + } + BlobCompressorVersion version = BlobCompressorVersion.valueOf(kv[0]); + Instant instant = Instant.Companion.parse(kv[1]); + result.put(version, instant); + } + return result; + } + + public Map getBlobCompressorVersionTimestamps() { + return parseBlobCompressorVersionTimestamps(blobCompressorVersionTimestampsRaw); + } } diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java index 36571f49da7..24a04f66a0d 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java @@ -11,6 +11,8 @@ import java.util.Map; import java.util.Set; +import kotlin.time.Instant; +import linea.blob.BlobCompressorVersion; import lombok.Builder; import net.consensys.linea.plugins.LineaOptionsConfiguration; import net.consensys.linea.sequencer.txselection.selectors.TransactionEventFilter; @@ -29,5 +31,6 @@ public record LineaTransactionSelectorConfiguration( String eventsDenyListPath, Map> eventsDenyList, String eventsBundleDenyListPath, - Map> eventsBundleDenyList) + Map> eventsBundleDenyList, + Map blobCompressorVersionActivationTimes) implements LineaOptionsConfiguration {} diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java index e939c38f5da..a32dfbb4500 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java @@ -21,7 +21,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; -import linea.blob.BlobCompressor; +import linea.blob.BlobCompressorSelectorByTimestamp; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.bl.TransactionProfitabilityCalculator; import net.consensys.linea.bundles.BundlePoolService; @@ -73,7 +73,7 @@ public class LineaTransactionSelectorFactory implements PluginTransactionSelecto private final AtomicBoolean isSelectionInterrupted = new AtomicBoolean(false); private final TransactionProfitabilityCalculator transactionProfitabilityCalculator; private final TransactionCompressor transactionCompressor; - private final BlobCompressor blobCompressor; + private final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp; public LineaTransactionSelectorFactory( final BlockchainService blockchainService, @@ -92,7 +92,7 @@ public LineaTransactionSelectorFactory( final AtomicReference> deniedAddresses, final TransactionProfitabilityCalculator transactionProfitabilityCalculator, final TransactionCompressor transactionCompressor, - final BlobCompressor blobCompressor) { + final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp) { this.blockchainService = blockchainService; this.txSelectorConfiguration = txSelectorConfiguration; this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; @@ -109,7 +109,7 @@ public LineaTransactionSelectorFactory( this.deniedAddresses = deniedAddresses; this.transactionProfitabilityCalculator = transactionProfitabilityCalculator; this.transactionCompressor = transactionCompressor; - this.blobCompressor = blobCompressor; + this.blobCompressorSelectorByTimestamp = blobCompressorSelectorByTimestamp; if (txSelectorConfiguration.maxBlockCallDataSize() != null) { log.warn( @@ -137,7 +137,7 @@ public PluginTransactionSelector create(final SelectorsStateManager selectorsSta deniedAddresses, transactionProfitabilityCalculator, transactionCompressor, - blobCompressor); + blobCompressorSelectorByTimestamp); currSelector.set(selector); return selector; } diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java index 96353f5139b..d30ecebb1aa 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicReference; -import linea.blob.BlobCompressor; +import linea.blob.BlobCompressorSelectorByTimestamp; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.AbstractLineaRequiredPlugin; import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; @@ -130,8 +130,8 @@ public void doStart() { // blobCompressor is initialised in AbstractLineaSharedPrivateOptionsPlugin with the effective // limit. Only pass it to the factory when a blob size limit is explicitly configured so that // CompressionAwareTransactionSelector is only active when intentionally enabled. - final BlobCompressor selectorBlobCompressor = - txSelectorConfiguration.blobSizeLimit() != null ? blobCompressor : null; + final BlobCompressorSelectorByTimestamp blobCompressorSelector = + txSelectorConfiguration.blobSizeLimit() != null ? blobCompressorSelectorByTimestamp : null; transactionSelectionService.registerPluginTransactionSelectorFactory( new LineaTransactionSelectorFactory( @@ -151,7 +151,7 @@ public void doStart() { deniedAddresses, transactionProfitabilityCalculator, transactionCompressor, - selectorBlobCompressor)); + blobCompressorSelector)); } @Override diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelector.java index d07b0ad1499..9a78b7fef48 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelector.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelector.java @@ -19,7 +19,9 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import kotlin.time.Instant; import linea.blob.BlobCompressor; +import linea.blob.BlobCompressorSelectorByTimestamp; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.utils.TransactionCompressor; import org.apache.tuweni.bytes.Bytes; @@ -105,7 +107,7 @@ public class CompressionAwareTransactionSelector private final long fastExecutionPathLimit; private final TransactionCompressor transactionCompressor; - private final BlobCompressor blobCompressor; + private final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp; /** * Shared single-threaded executor for slow-execution-path compression. Declared static so that @@ -143,7 +145,7 @@ public CompressionAwareTransactionSelector( final int blobSizeLimit, final int compressedBlockHeaderOverhead, final TransactionCompressor transactionCompressor, - final BlobCompressor blobCompressor) { + final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp) { super( selectorsStateManager, new CompressionState(0L, new ArrayList<>()), @@ -154,7 +156,7 @@ public CompressionAwareTransactionSelector( "fastExecutionPathLimit must be positive, got " + fastExecutionPathLimit); } this.transactionCompressor = transactionCompressor; - this.blobCompressor = blobCompressor; + this.blobCompressorSelectorByTimestamp = blobCompressorSelectorByTimestamp; } @Override @@ -163,8 +165,11 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( final Transaction transaction = (Transaction) evaluationContext.getPendingTransaction().getTransaction(); final CompressionState state = getWorkingState(); - - final int txCompressedSize = transactionCompressor.getCompressedSize(transaction); + final ProcessableBlockHeader pendingHeader = evaluationContext.getPendingBlockHeader(); + final Instant timestamp = Instant.Companion.fromEpochSeconds(pendingHeader.getTimestamp(), 0L); + final BlobCompressor blobCompressor = + blobCompressorSelectorByTimestamp.getBlobCompressor(timestamp); + final int txCompressedSize = transactionCompressor.getCompressedSize(transaction, timestamp); long newConservativeCumulative = state.cumulativeCompressedSize() + txCompressedSize; // Fast execution path: sum of per-tx compressed sizes is at or below the effective limit (blob @@ -187,7 +192,6 @@ public TransactionSelectionResult evaluateTransactionPreProcessing( // Slow execution path: conservative estimate exceeded the limit. // Build the block RLP on this thread (cheap, CPU-only), then submit the native // reset+appendBlock to the background executor so it can overlap with EVM execution. - final ProcessableBlockHeader pendingHeader = evaluationContext.getPendingBlockHeader(); final List tentativeTxs = new ArrayList<>(state.selectedTransactions()); tentativeTxs.add(transaction); final byte[] blockRlp = buildBlockRlp(pendingHeader, tentativeTxs); diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java index db77a64c982..69d2c1d6109 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java @@ -15,7 +15,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; -import linea.blob.BlobCompressor; +import linea.blob.BlobCompressorSelectorByTimestamp; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.bl.TransactionProfitabilityCalculator; import net.consensys.linea.bundles.TransactionBundle; @@ -33,7 +33,6 @@ import org.hyperledger.besu.plugin.data.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; import org.hyperledger.besu.plugin.services.BlockchainService; -import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.SelectorsStateManager; import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; @@ -64,7 +63,7 @@ public LineaTransactionSelector( final AtomicReference> deniedAddresses, final TransactionProfitabilityCalculator transactionProfitabilityCalculator, final TransactionCompressor transactionCompressor, - final BlobCompressor blobCompressor) { + final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp) { this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; selectors = @@ -82,7 +81,7 @@ public LineaTransactionSelector( deniedAddresses, transactionProfitabilityCalculator, transactionCompressor, - blobCompressor); + blobCompressorSelectorByTimestamp); } /** @@ -97,6 +96,7 @@ public LineaTransactionSelector( * @param deniedEvents The transaction event deny list * @param deniedBundleEvents The bundle transaction event deny list * @param deniedAddresses The denied addresses set + * @param blobCompressorSelectorByTimestamp * @return A list of selectors. */ private List createTransactionSelectors( @@ -113,7 +113,7 @@ private List createTransactionSelectors( final AtomicReference> deniedAddresses, final TransactionProfitabilityCalculator transactionProfitabilityCalculator, final TransactionCompressor transactionCompressor, - final BlobCompressor blobCompressor) { + final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp) { traceLineLimitTransactionSelector = new TraceLineLimitTransactionSelector( @@ -134,14 +134,14 @@ private List createTransactionSelectors( if (txSelectorConfiguration.blobSizeLimit() != null && transactionCompressor != null - && blobCompressor != null) { + && blobCompressorSelectorByTimestamp != null) { selectorsList.add( new CompressionAwareTransactionSelector( selectorsStateManager, txSelectorConfiguration.blobSizeLimit(), txSelectorConfiguration.compressedBlockHeaderOverhead(), transactionCompressor, - blobCompressor)); + blobCompressorSelectorByTimestamp)); } selectorsList.add( diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/CachingTransactionCompressor.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/CachingTransactionCompressor.java index e181979cc0c..b063d26f397 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/CachingTransactionCompressor.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/CachingTransactionCompressor.java @@ -12,7 +12,10 @@ import com.google.common.cache.CacheBuilder; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import kotlin.time.Instant; import linea.blob.BlobCompressor; +import linea.blob.BlobCompressorSelectorByTimestamp; +import linea.blob.BlobCompressorVersion; import lombok.extern.slf4j.Slf4j; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Transaction; @@ -24,12 +27,16 @@ */ @Slf4j public class CachingTransactionCompressor implements TransactionCompressor { + record CacheKey(Hash transactionHash, BlobCompressorVersion compressorVersion) {} + private static final long DEFAULT_CACHE_SIZE = 10000; - private final BlobCompressor blobCompressor; - private final Cache compressedSizeCache; + private final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp; + private final Cache compressedSizeCache; - public CachingTransactionCompressor(final long cacheSize, final BlobCompressor blobCompressor) { - this.blobCompressor = blobCompressor; + public CachingTransactionCompressor( + final long cacheSize, + final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp) { + this.blobCompressorSelectorByTimestamp = blobCompressorSelectorByTimestamp; compressedSizeCache = CacheBuilder.newBuilder() .maximumSize(cacheSize) @@ -37,11 +44,13 @@ public CachingTransactionCompressor(final long cacheSize, final BlobCompressor b .build(); } - public CachingTransactionCompressor(final BlobCompressor blobCompressor) { - this(DEFAULT_CACHE_SIZE, blobCompressor); + public CachingTransactionCompressor( + final BlobCompressorSelectorByTimestamp blobCompressorSelectorByTimestamp) { + this(DEFAULT_CACHE_SIZE, blobCompressorSelectorByTimestamp); } - private int calculateCompressedSize(final Transaction transaction) { + private int calculateCompressedSize( + final Transaction transaction, final BlobCompressor blobCompressor) { final byte[] encoded = TxEncodingUtils.encodeForCompressor(transaction); return blobCompressor.compressedSize(encoded); } @@ -55,10 +64,13 @@ private int calculateCompressedSize(final Transaction transaction) { * @return the compressed size of the transaction */ @Override - public int getCompressedSize(final Transaction transaction) { + public int getCompressedSize(final Transaction transaction, final Instant blockTimestamp) { + final BlobCompressor blobCompressor = + blobCompressorSelectorByTimestamp.getBlobCompressor(blockTimestamp); try { return compressedSizeCache.get( - transaction.getHash(), () -> calculateCompressedSize(transaction)); + new CacheKey(transaction.getHash(), blobCompressor.getVersion()), + () -> calculateCompressedSize(transaction, blobCompressor)); } catch (ExecutionException e) { log.atWarn() .setMessage( @@ -66,7 +78,7 @@ public int getCompressedSize(final Transaction transaction) { .addArgument(transaction::getHash) .setCause(e) .log(); - return calculateCompressedSize(transaction); + return calculateCompressedSize(transaction, blobCompressor); } } } diff --git a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/TransactionCompressor.java b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/TransactionCompressor.java index 800bbbe3518..5dedf79338d 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/TransactionCompressor.java +++ b/besu-plugins/linea-sequencer/sequencer/src/main/java/net/consensys/linea/utils/TransactionCompressor.java @@ -8,6 +8,7 @@ */ package net.consensys.linea.utils; +import kotlin.time.Instant; import org.hyperledger.besu.datatypes.Transaction; /** @@ -21,7 +22,13 @@ public interface TransactionCompressor { * transaction hash to improve performance. * * @param transaction the transaction for which to get the compressed size + * @param blockTimestamp the timestamp of the block which is expected to include this transaction * @return the compressed size of the transaction */ - int getCompressedSize(Transaction transaction); + int getCompressedSize(Transaction transaction, Instant blockTimestamp); + + default int getCompressedSize(Transaction transaction) { + return getCompressedSize( + transaction, Instant.Companion.fromEpochMilliseconds(System.currentTimeMillis())); + } } diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/config/LineaTransactionSelectorCliOptionsTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/config/LineaTransactionSelectorCliOptionsTest.java new file mode 100644 index 00000000000..34f6a20f2a8 --- /dev/null +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/config/LineaTransactionSelectorCliOptionsTest.java @@ -0,0 +1,42 @@ +/* + * Copyright Consensys Software Inc. + * + * This file is dual-licensed under either the MIT license or Apache License 2.0. + * See the LICENSE-MIT and LICENSE-APACHE files in the repository root for details. + * + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ + +package net.consensys.linea.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import kotlin.time.Instant; +import linea.blob.BlobCompressorVersion; +import org.junit.jupiter.api.Test; + +class LineaTransactionSelectorCliOptionsTest { + + @Test + void parseBlobCompressorVersionTimestamps_validInput() { + LineaTransactionSelectorCliOptions opts = new LineaTransactionSelectorCliOptions(); + String input = "V1_2=2025-01-01T00:00:00Z,V2=2026-01-01T00:00:00Z"; + Map result = opts.parseBlobCompressorVersionTimestamps(input); + assertEquals(2, result.size()); + assertEquals( + Instant.Companion.parse("2025-01-01T00:00:00Z"), result.get(BlobCompressorVersion.V1_2)); + assertEquals( + Instant.Companion.parse("2026-01-01T00:00:00Z"), result.get(BlobCompressorVersion.V2)); + } + + @Test + void parseBlobCompressorVersionTimestamps_invalidInput() { + LineaTransactionSelectorCliOptions opts = new LineaTransactionSelectorCliOptions(); + String input = "V1_2=2025-01-01T00:00:00Z,V2"; + Exception ex = + assertThrows( + IllegalArgumentException.class, () -> opts.parseBlobCompressorVersionTimestamps(input)); + assertTrue(ex.getMessage().contains("Invalid BlobCompressorVersion=Instant pair")); + } +} diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java index f01410e2c9a..ff117bd0464 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java @@ -13,9 +13,11 @@ import static org.mockito.Mockito.when; import java.math.BigInteger; +import java.util.Map; import java.util.Optional; +import kotlin.time.Instant; +import linea.blob.BlobCompressorSelectorByTimestamp; import linea.blob.BlobCompressorVersion; -import linea.blob.GoBackedBlobCompressor; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.bl.TransactionProfitabilityCalculator; @@ -78,7 +80,8 @@ public void initialize() { .txPoolMinMargin(TX_POOL_MIN_MARGIN); final var transactionCompressor = new CachingTransactionCompressor( - GoBackedBlobCompressor.getInstance(BlobCompressorVersion.V2, 128 * 1024)); + new BlobCompressorSelectorByTimestamp( + Map.of(BlobCompressorVersion.V2, Instant.Companion.getDISTANT_PAST()), 128 * 1024)); final var profitabilityCalculatorAlways = new TransactionProfitabilityCalculator( diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java index be7734f71a4..d1208810d5a 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactoryTest.java @@ -27,12 +27,14 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import kotlin.time.Instant; +import linea.blob.BlobCompressorSelectorByTimestamp; import linea.blob.BlobCompressorVersion; -import linea.blob.GoBackedBlobCompressor; import net.consensys.linea.bl.TransactionProfitabilityCalculator; import net.consensys.linea.bundles.LineaLimitedBundlePool; import net.consensys.linea.bundles.TransactionBundle; @@ -131,7 +133,8 @@ private void setUpWithLivenessService(Optional livenessService) new InvalidTransactionByLineCountCache(10); final var transactionCompressor = new CachingTransactionCompressor( - GoBackedBlobCompressor.getInstance(BlobCompressorVersion.V2, 128 * 1024)); + new BlobCompressorSelectorByTimestamp( + Map.of(BlobCompressorVersion.V2, Instant.Companion.getDISTANT_PAST()), 128 * 1024)); TransactionProfitabilityCalculator transactionProfitabilityCalculator = new TransactionProfitabilityCalculator( mockProfitabilityConfiguration, transactionCompressor); diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelectorTest.java index 6c2e488f12c..cd0da40aa51 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelectorTest.java +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/CompressionAwareTransactionSelectorTest.java @@ -17,9 +17,11 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.util.Map; +import kotlin.time.Instant; import linea.blob.BlobCompressor; +import linea.blob.BlobCompressorSelectorByTimestamp; import linea.blob.BlobCompressorVersion; -import linea.blob.GoBackedBlobCompressor; import net.consensys.linea.utils.CachingTransactionCompressor; import net.consensys.linea.utils.TestTransactionFactory; import net.consensys.linea.utils.TransactionCompressor; @@ -48,7 +50,8 @@ class CompressionAwareTransactionSelectorTest { private static final TransactionCompressor TX_COMPRESSOR = new CachingTransactionCompressor( - GoBackedBlobCompressor.getInstance(BlobCompressorVersion.V2, 128 * 1024)); + new BlobCompressorSelectorByTimestamp( + Map.of(BlobCompressorVersion.V2, Instant.Companion.getDISTANT_PAST()), 128 * 1024)); private SelectorsStateManager selectorsStateManager; private TestTransactionFactory txFactory; @@ -64,8 +67,10 @@ void setUp() { * new limit re-runs {@code Init} on the underlying native singleton and returns a wrapper that * uses it. */ - private static GoBackedBlobCompressor compressorWithLimit(final int dataLimit) { - return GoBackedBlobCompressor.getInstance(BlobCompressorVersion.V2, dataLimit); + private static BlobCompressorSelectorByTimestamp compressorSelectorWithLimit( + final int dataLimit) { + return new BlobCompressorSelectorByTimestamp( + Map.of(BlobCompressorVersion.V2, Instant.Companion.getDISTANT_PAST()), dataLimit); } @Test @@ -239,6 +244,9 @@ void afterSlowPathSuccess_cumulativeSizeUpdatedToActualCompressedSize() { final TransactionCompressor mockTxCompressor = mock(TransactionCompressor.class); final BlobCompressor mockBlobCompressor = mock(BlobCompressor.class); + final BlobCompressorSelectorByTimestamp mockBlobCompressorSelector = + mock(BlobCompressorSelectorByTimestamp.class); + when(mockBlobCompressorSelector.getBlobCompressor(any())).thenReturn(mockBlobCompressor); // appendBlock reports that the block fits with a compressed size well below // fastExecutionPathLimit. @@ -253,27 +261,30 @@ void afterSlowPathSuccess_cumulativeSizeUpdatedToActualCompressedSize() { blobSizeLimit, headerOverhead, mockTxCompressor, - mockBlobCompressor); + mockBlobCompressorSelector); selectorsStateManager.blockSelectionStarted(); // tx1: just below fastExecutionPathLimit → fast execution path; cumulative = // fastExecutionPathLimit - 1. + final Instant timestamp = + Instant.Companion.fromEpochSeconds(mockBlockHeader().getTimestamp(), 0L); final var tx1 = txFactory.createTransaction(); - when(mockTxCompressor.getCompressedSize(tx1)).thenReturn(fastExecutionPathLimit - 1); + when(mockTxCompressor.getCompressedSize(tx1, timestamp)).thenReturn(fastExecutionPathLimit - 1); assertThat(evaluateTx(selector, wrapTx(tx1))).isEqualTo(SELECTED); // tx2: size 2 pushes conservative cumulative above fastExecutionPathLimit → slow execution // path. // appendBlock returns compressedSizeAfter = fastExecutionPathLimit / 2. final var tx2 = txFactory.createTransaction(); - when(mockTxCompressor.getCompressedSize(tx2)).thenReturn(2); + when(mockTxCompressor.getCompressedSize(tx2, timestamp)).thenReturn(2); assertThat(evaluateTx(selector, wrapTx(tx2))).isEqualTo(SELECTED); // tx3: compressedSizeAfter + tx3Size <= fastExecutionPathLimit, so it fits on the fast // execution path. // With the fix, no slow-execution-path check is needed. final var tx3 = txFactory.createTransaction(); - when(mockTxCompressor.getCompressedSize(tx3)).thenReturn(fastExecutionPathLimit / 2 - 1); + when(mockTxCompressor.getCompressedSize(tx3, timestamp)) + .thenReturn(fastExecutionPathLimit / 2 - 1); assertThat(evaluateTx(selector, wrapTx(tx3))).isEqualTo(SELECTED); // Only tx2 should have triggered a slow-execution-path compression check. @@ -290,11 +301,15 @@ private CompressionAwareTransactionSelector createSelector(final int blobSizeLim private CompressionAwareTransactionSelector createSelector( final int blobSizeLimit, final int headerOverhead) { - final var compressor = compressorWithLimit(blobSizeLimit); + final var compressorSelectorWithLimit = compressorSelectorWithLimit(blobSizeLimit); selectorsStateManager = new SelectorsStateManager(); final var selector = new CompressionAwareTransactionSelector( - selectorsStateManager, blobSizeLimit, headerOverhead, TX_COMPRESSOR, compressor); + selectorsStateManager, + blobSizeLimit, + headerOverhead, + TX_COMPRESSOR, + compressorSelectorWithLimit); selectorsStateManager.blockSelectionStarted(); return selector; } diff --git a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java index 3097f35f769..30359596bc9 100644 --- a/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java +++ b/besu-plugins/linea-sequencer/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ProfitableTransactionSelectorTest.java @@ -17,9 +17,11 @@ import java.math.BigInteger; import java.util.List; +import java.util.Map; import java.util.Optional; +import kotlin.time.Instant; +import linea.blob.BlobCompressorSelectorByTimestamp; import linea.blob.BlobCompressorVersion; -import linea.blob.GoBackedBlobCompressor; import net.consensys.linea.bl.TransactionProfitabilityCalculator; import net.consensys.linea.config.LineaProfitabilityCliOptions; import net.consensys.linea.config.LineaProfitabilityConfiguration; @@ -89,7 +91,8 @@ private ProfitableTransactionSelector newSelectorForNewBlock() { when(blockchainService.getNextBlockBaseFee()).thenReturn(Optional.of(BASE_FEE)); final var transactionCompressor = new CachingTransactionCompressor( - GoBackedBlobCompressor.getInstance(BlobCompressorVersion.V2, 128 * 1024)); + new BlobCompressorSelectorByTimestamp( + Map.of(BlobCompressorVersion.V2, Instant.Companion.getDISTANT_PAST()), 128 * 1024)); final var transactionProfitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf, transactionCompressor); return new ProfitableTransactionSelector( diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c62b1b458b7..0b7f56fc513 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -42,7 +42,7 @@ undercouchDownload = "5.6.0" besuCommit = "d68679d6887abf8c407f99456cf2502004fa2bad" besu = "25.12.0-d68679d" -blobCompressor = "3.0.1" +blobCompressor = "3.0.2" blobShnarfCalculator = "3.0.1" bouncycastle = "1.79" caffeine = "3.1.6" @@ -52,7 +52,7 @@ jackson = "2.19.4" jna = "5.14.0" kotlinResult = "1.1.16" ktlint = "1.0.1" -lineaKotlin = "0.1.0" +lineaKotlin = "0.2.0" log4j = "2.25.3" micrometer = "1.12.13" netty = "4.1.92.Final"