Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* 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 linea.plugin.acc.test

import linea.kotlin.encodeHex
import org.assertj.core.api.Assertions.assertThat
import org.hyperledger.besu.tests.acceptance.dsl.account.Account
import org.junit.jupiter.api.Test
import org.web3j.crypto.RawTransaction
import org.web3j.crypto.TransactionEncoder
import org.web3j.protocol.core.methods.response.TransactionReceipt
import org.web3j.tx.gas.DefaultGasProvider
import java.math.BigInteger
import kotlin.jvm.optionals.getOrNull
import kotlin.random.Random

class CompressionAwareBlockBuildingTest : LineaPluginPoSTestBase() {

companion object {
// Set a blob size limit that allows testing compression-aware block building.
// A transaction with random calldata compresses poorly (roughly 1:1 ratio).
// Transaction overhead (signature, nonce, gas, etc.) adds ~100-200 bytes.
//
// We use smaller limits to ensure our test transactions actually exceed them.
// The compressor is efficient, so we need tight limits.
private const val BLOB_SIZE_LIMIT = 4096 // 4 KB
private const val HEADER_OVERHEAD = 512 // 0.5 KB for block header

// Effective limit for transactions = BLOB_SIZE_LIMIT - HEADER_OVERHEAD = 3584 bytes
// A transaction with ~4000 bytes of random calldata should exceed this limit.
// Two transactions with ~2000 bytes each should together exceed the limit.
// A small transaction with ~500 bytes should fit easily.

private val GAS_PRICE: BigInteger = DefaultGasProvider.GAS_PRICE
private val GAS_LIMIT: BigInteger = DefaultGasProvider.GAS_LIMIT

// Fixed seed for reproducible random data generation.
// Random data compresses poorly regardless of seed, but using a fixed seed
// ensures consistent test behavior across runs.
private const val RANDOM_SEED = 42L

// Shared Random instance to ensure different transactions get different calldata.
// If we created a new Random(RANDOM_SEED) for each transaction, they would all
// get identical calldata, which compresses very well together (defeating the test).
private val random = Random(RANDOM_SEED)
}

override fun getTestCliOptions(): List<String> {
return TestCommandLineOptionsBuilder()
.set("--plugin-linea-blob-size-limit=", BLOB_SIZE_LIMIT.toString())
.set("--plugin-linea-compressed-block-header-overhead=", HEADER_OVERHEAD.toString())
.set("--plugin-linea-module-limit-file-path=", getResourcePath("/noModuleLimits.toml"))
.build()
}

/**
* Test that a transaction with calldata that compresses to more than the blob size limit
* is not included in any block.
*
* Strategy:
* 1. Disable background block building
* 2. Submit a large transaction that exceeds the compressed size limit
* 3. Submit a small transaction that fits
* 4. Build a block
* 5. Verify the small transaction was included but the large one was not
*/
@Test
fun largeTransactionExceedingCompressedLimitIsNotIncluded() {
val newAccounts = createAccounts(2, 10)

// Now disable background block building for the actual test
buildBlocksInBackground = false
val largeTxSender = newAccounts[0]
val smallTxSender = newAccounts[1]

val largeTxRaw = createRawTransactionWithRandomCalldata(largeTxSender, 0, 4000)
val smallTxRaw = createRawTransactionWithRandomCalldata(smallTxSender, 0, 500)
val web3j = minerNode.nodeRequests().eth()
val largeTxResponse = web3j.ethSendRawTransaction(largeTxRaw).send()
val smallTxResponse = web3j.ethSendRawTransaction(smallTxRaw).send()

// Both should be accepted into the pool (no error at submission time)
assertThat(largeTxResponse.hasError())
.withFailMessage { "Large tx submission failed: ${largeTxResponse.error?.message}" }
.isFalse()
assertThat(smallTxResponse.hasError())
.withFailMessage { "Small tx submission failed: ${smallTxResponse.error?.message}" }
.isFalse()

val largeTxHash = largeTxResponse.transactionHash
val smallTxHash = smallTxResponse.transactionHash

buildNewBlockAndWait()

minerNode.verify(eth.expectSuccessfulTransactionReceipt(smallTxHash))
minerNode.verify(eth.expectNoTransactionReceipt(largeTxHash))
}

/**
* Test that two transactions that individually fit but together exceed the compressed size limit
* are spaced out in separate blocks.
*
* Strategy:
* 1. Disable background block building
* 2. Submit two medium-sized transactions that each fit individually but together exceed the limit
* 3. Build a block - only one should be included
* 4. Build another block - the second should be included
*/
@Test
fun twoTransactionsExceedingLimitAreSpacedInSeparateBlocks() {
val newAccounts = createAccounts(2, 10)

buildBlocksInBackground = false
val sender1 = newAccounts[0]
val sender2 = newAccounts[1]

val tx1Raw = createRawTransactionWithRandomCalldata(sender1, 0, 2000)
val tx2Raw = createRawTransactionWithRandomCalldata(sender2, 0, 2000)

val web3j = minerNode.nodeRequests().eth()
val tx1Response = web3j.ethSendRawTransaction(tx1Raw).send()
val tx2Response = web3j.ethSendRawTransaction(tx2Raw).send()

assertThat(tx1Response.hasError())
.withFailMessage { "Tx1 submission failed: ${tx1Response.error?.message}" }
.isFalse()
assertThat(tx2Response.hasError())
.withFailMessage { "Tx2 submission failed: ${tx2Response.error?.message}" }
.isFalse()

val tx1Hash = tx1Response.transactionHash
val tx2Hash = tx2Response.transactionHash

buildNewBlockAndWait()

val tx1ReceiptAfterBlock1 = getTransactionReceiptIfExists(tx1Hash)
val tx2ReceiptAfterBlock1 = getTransactionReceiptIfExists(tx2Hash)

val tx1InBlock1 = tx1ReceiptAfterBlock1 != null
val tx2InBlock1 = tx2ReceiptAfterBlock1 != null

assertThat(tx1InBlock1 xor tx2InBlock1)
.withFailMessage {
"Expected exactly one transaction in first block, but tx1InBlock1=$tx1InBlock1, tx2InBlock1=$tx2InBlock1"
}
.isTrue()

buildNewBlockAndWait()

minerNode.verify(eth.expectSuccessfulTransactionReceipt(tx1Hash))
minerNode.verify(eth.expectSuccessfulTransactionReceipt(tx2Hash))

val tx1Receipt = ethTransactions.getTransactionReceipt(tx1Hash).execute(minerNode.nodeRequests())
val tx2Receipt = ethTransactions.getTransactionReceipt(tx2Hash).execute(minerNode.nodeRequests())

assertThat(tx1Receipt).isPresent
assertThat(tx2Receipt).isPresent

val tx1BlockNumber = tx1Receipt.get().blockNumber
val tx2BlockNumber = tx2Receipt.get().blockNumber

assertThat(tx1BlockNumber)
.withFailMessage {
"Expected transactions in different blocks, but both in block $tx1BlockNumber"
}
.isNotEqualTo(tx2BlockNumber)
}

/**
* Creates a signed raw transaction with random calldata of the specified size.
* Random data compresses poorly, making it ideal for testing compression limits.
*/
private fun createRawTransactionWithRandomCalldata(
sender: Account,
nonce: Int,
calldataSize: Int,
): String {
val randomCalldata = ByteArray(calldataSize)
random.nextBytes(randomCalldata)

val rawTx = RawTransaction.createTransaction(
CHAIN_ID,
nonce.toBigInteger(),
GAS_LIMIT,
"0x" + "00".repeat(20),
BigInteger.ZERO,
randomCalldata.encodeHex(),
GAS_PRICE,
GAS_PRICE.multiply(BigInteger.TEN).add(BigInteger.ONE),
)

return TransactionEncoder.signMessage(rawTx, sender.web3jCredentialsOrThrow()).encodeHex()
}

private fun getTransactionReceiptIfExists(txHash: String): TransactionReceipt? {
return ethTransactions.getTransactionReceipt(txHash).execute(minerNode.nodeRequests()).getOrNull()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,6 @@ class TransactionCallDataSizeLimitTest : LineaPluginPoSTestBase() {
.isEqualTo("Calldata of transaction is greater than the allowed max of 1188")
}

/**
* if we have a list of transactions [t_small, t_tooBig, t_small, ..., t_small] where t_tooBig is
* too big to fit in a block, we have blocks created that contain all t_small transactions.
*
* @throws Exception if send transaction fails
*/
@Test
fun multipleSmallTxsMinedWhileTxTooBigNot() {
val simpleStorage = deploySimpleStorage()
Expand Down
41 changes: 32 additions & 9 deletions besu-plugins/linea-sequencer/docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,29 @@ These values are just passed to the ZkTracer
## Sequencer

### Transaction selection - LineaTransactionSelectorPlugin
This plugin extends the standard transaction selection protocols employed by Besu for block creation.
It leverages the `TransactionSelectionService` to manage and customize the process of transaction selection.
This includes setting limits such as `TraceLineLimit`, `maxBlockGas`, and `maxCallData`, and check the profitability
of a transaction.
The selectors are in the package `net.consensys.linea.sequencer.txselection.selectors`.
This plugin extends the standard transaction selection protocols employed by Besu for block creation.
It leverages the `TransactionSelectionService` to manage and customize the process of transaction selection.
This includes setting limits such as `TraceLineLimit`, `maxBlockGas`, and optionally a compression-aware blob
size limit and/or a raw block calldata limit. Block size selectors are only instantiated when their
respective CLI flag is set. The selectors are in the package `net.consensys.linea.sequencer.txselection.selectors`.

- **`--plugin-linea-blob-size-limit`** (optional): If set, enables `CompressionAwareBlockTransactionSelector`
which constructs an RLP-encoded block (using placeholder values for header fields) containing all currently
selected transactions and then compresses this full block. The transaction is rejected if the compressed
size of this encoded block exceeds the configured limit. The `--plugin-linea-compressed-block-header-overhead`
option accounts for variability in real headers during the fast-path check.
- **`--plugin-linea-max-block-calldata-size`** (optional): If set, enables `MaxBlockCallDataTransactionSelector`
which enforces a cumulative raw calldata size limit per block.

Both can be set simultaneously (both checks run, the more restrictive one wins).

#### CLI options

| Command Line Argument | Default Value |
|--------------------------------------------------------|----------------------|
| `--plugin-linea-max-block-calldata-size` | 70000 |
| `--plugin-linea-blob-size-limit` | not set (disabled) |
| `--plugin-linea-compressed-block-header-overhead` | 1024 |
| `--plugin-linea-max-block-calldata-size` | not set (disabled) |
| `--plugin-linea-module-limit-file-path` | moduleLimitFile.toml |
| `--plugin-linea-over-line-count-limit-cache-size` | 10_000 |
| `--plugin-linea-max-block-gas` | 30_000_000L |
Expand All @@ -75,8 +87,9 @@ The selectors are in the package `net.consensys.linea.sequencer.txselection.sele
This plugin extends the default transaction validation rules for adding transactions to the
transaction pool. It leverages the `PluginTransactionValidatorService` to manage and customize the
process of transaction validation.
This includes setting limits such as `TraceLineLimit`, `maxTxGasLimit`, and `maxTxCallData`, and checking the profitability
of a transaction.
This includes setting limits such as `TraceLineLimit`, `maxTxGasLimit`, and checking the profitability
of a transaction. Per-transaction calldata size validation is optional and only enabled when
`--plugin-linea-max-tx-calldata-size` is set.
The validators are in the package `net.consensys.linea.sequencer.txpoolvalidation.validators`.

#### CLI options
Expand All @@ -85,7 +98,7 @@ The validators are in the package `net.consensys.linea.sequencer.txpoolvalidatio
|----------------------------------------------------------|-------------------|
| `--plugin-linea-deny-list-path` | lineaDenyList.txt |
| `--plugin-linea-max-tx-gas-limit` | 30_000_000 |
| `--plugin-linea-max-tx-calldata-size` | 60_000 |
| `--plugin-linea-max-tx-calldata-size` | not set (disabled)|
| `--plugin-linea-tx-pool-simulation-check-api-enabled` | false |
| `--plugin-linea-tx-pool-simulation-check-p2p-enabled` | false |
| `--plugin-linea-tx-pool-profitability-check-api-enabled` | true |
Expand Down Expand Up @@ -159,3 +172,13 @@ same as `miner_setExtraData` with the added constraint that the number of bytes
}
```

## Migration

### Compression-aware block building

All block/tx size flags are now optional and only activate their respective selectors when set:

- **`--plugin-linea-blob-size-limit`**: New flag. When set, enables the compression-aware block selector which uses a two-phase strategy: (1) a fast path that accumulates per-transaction compressed sizes and accepts transactions while the cumulative sum is below the limit minus header overhead, and (2) a slow path that builds a full RLP-encoded block (with placeholder header fields) and checks if it compresses within the limit. This maximizes block utilization by leveraging cross-transaction compression context. Recommended value: 131072 (128 KB).
- **`--plugin-linea-max-block-calldata-size`** (deprecated): Still supported but deprecated. When set, enables the raw calldata block size selector (legacy behaviour) and logs a deprecation warning at startup. When not set, the selector is not instantiated. Will be removed in a future release.
- **`--plugin-linea-max-tx-calldata-size`** (deprecated): Still supported but deprecated. When set, enables the per-tx calldata pool validator and logs a deprecation warning at startup. When not set, the validator is not instantiated. Will be removed in a future release.
- Both old and new flags can be set simultaneously; both selectors will run and the more restrictive one wins.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ public abstract class AbstractLineaSharedPrivateOptionsPlugin
protected static MetricCategoryRegistry metricCategoryRegistry;
protected static RpcEndpointService rpcEndpointService;
protected static InvalidTransactionByLineCountCache invalidTransactionByLineCountCache;
protected static TransactionCompressor transactionCompressor;
protected static TransactionProfitabilityCalculator transactionProfitabilityCalculator;

private static final AtomicBoolean sharedRegisterTasksDone = new AtomicBoolean(false);
Expand Down Expand Up @@ -294,7 +295,7 @@ private void performSharedStartTasksOnce(final ServiceManager serviceManager) {
transactionSelectorConfiguration().overLinesLimitCacheSize());

final LineaProfitabilityConfiguration profitabilityConfiguration = profitabilityConfiguration();
final TransactionCompressor transactionCompressor =
transactionCompressor =
new CachingTransactionCompressor(profitabilityConfiguration.compressedTxCacheSize());
transactionProfitabilityCalculator =
new TransactionProfitabilityCalculator(profitabilityConfiguration, transactionCompressor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
package net.consensys.linea.config;

import com.google.common.base.MoreObjects;
import jakarta.validation.constraints.Positive;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -73,15 +74,14 @@ public class LineaTransactionPoolValidatorCliOptions implements LineaCliOptions
+ ")")
private int maxTxGasLimit = DEFAULT_MAX_TRANSACTION_GAS_LIMIT;

@Positive
@CommandLine.Option(
names = {MAX_TX_CALLDATA_SIZE},
hidden = true,
paramLabel = "<INTEGER>",
description =
"Maximum size for the calldata of a Transaction (default: "
+ DEFAULT_MAX_TX_CALLDATA_SIZE
+ ")")
private int maxTxCallDataSize = DEFAULT_MAX_TX_CALLDATA_SIZE;
"Maximum size for the calldata of a Transaction. If set, the calldata validator is enabled.")
private Integer maxTxCallDataSize;

@CommandLine.Option(
names = {TX_POOL_ENABLE_SIMULATION_CHECK_API},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public record LineaTransactionPoolValidatorConfiguration(
String bundleOverridingDenyListPath,
Set<Address> bundleDeniedAddresses,
int maxTxGasLimit,
int maxTxCalldataSize,
Integer maxTxCalldataSize,
boolean txPoolSimulationCheckApiEnabled,
boolean txPoolSimulationCheckP2pEnabled)
implements LineaOptionsConfiguration {}
Loading
Loading