[linea-sequencer-plugin] Block blob tx (#1150)

* port code from linea-sequencer pr 195

* boilerplate changes for LineaTransactionValidatorPlugin

* poc for LineaTransactionValidatorPlugin

* first commit for LineaTransactionValidatorPluginTest

* created error enum

* most LineaTransactionValidatorPluginTest tests working

* most LineaTransactionValidatorPluginTest tests working

* LineaTransactionValidatorPluginTest done

* switch plugin for acceptance test

* make BlobTransactionDenialTest inherit prague

* change prague test config

* implemented genesisfile override for prague test

* change rule registration to doRegister

* remove permissioningservice

* passing acceptance test

* add more comments

* spotlessapply fixes

* unit test fixes

* more comments

* fix comment

* new comment

* move sendRawBlobTransaction

* add BlobTransactionImportDenialTest file

* move DEFAULT_REQUESTED_PLUGINS

* go simulated block route

* success blob test

* new changes

* fix engine_newpayload request

* first compile for new blockheader

* stuck on why hash is not the same

* stuck on why hash is not the same

* success for BlobTransactionDenialTest

* more refactor

* refactor

* refactor

* refactor

* refactor

* refactor

* refactor

* refactor

* spotless

* refactor
This commit is contained in:
kyzooghost
2025-06-18 16:03:11 +10:00
committed by GitHub
parent e66abc64fd
commit 959cf52286
16 changed files with 917 additions and 21 deletions

View File

@@ -4,6 +4,8 @@
* feat: Report rejected transactions only due to trace limit overflows to an external service.
* feat: Report rejected transactions to an external service for validators used by LineaTransactionPoolValidatorPlugin [#85](https://github.com/Consensys/linea-sequencer/pull/85)
* feat: Report rejected transactions to an external service for LineaTransactionSelector used by LineaTransactionSelectorPlugin [#69](https://github.com/Consensys/linea-sequencer/pull/69)
* feat: Create LineaTransactionValidatorPlugin to filter transactions using Besu's TransactionValidatorService (currently rejecting BLOB transactions)
* feat: Add CLI option `--plugin-linea-blob-tx-enabled` to control blob transaction acceptance in LineaTransactionValidatorPlugin
## 0.6.0-rc1.1
* bump linea-arithmetization version to 0.6.0-rc1 [#71](https://github.com/Consensys/linea-sequencer/pull/71)

View File

@@ -60,11 +60,12 @@ dependencies {
testImplementation project("${lineaSequencerProjectPath}:sequencer")
testImplementation "${besuArtifactGroup}:besu-datatypes"
testImplementation "${besuArtifactGroup}:besu-evm"
testImplementation "${besuArtifactGroup}.internal:besu-consensus-clique"
testImplementation "${besuArtifactGroup}.internal:besu-ethereum-api"
testImplementation "${besuArtifactGroup}.internal:besu-ethereum-core"
testImplementation "${besuArtifactGroup}.internal:besu-acceptance-tests-dsl"
testImplementation "${besuArtifactGroup}.internal:besu-ethereum-eth"
testImplementation "${besuArtifactGroup}.internal:besu-acceptance-tests-dsl"
testImplementation "${besuArtifactGroup}.internal:besu-metrics-core"
testImplementation "${besuArtifactGroup}.internal:besu-crypto-services"
testImplementation group: "${besuArtifactGroup}.internal", name: "besu-ethereum-core", classifier: "test-support"

View File

@@ -0,0 +1,272 @@
/*
* 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 static org.assertj.core.api.Assertions.assertThat;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import okhttp3.Response;
import org.apache.tuweni.bytes.Bytes;
import org.apache.tuweni.bytes.Bytes32;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.BlobGas;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.datatypes.RequestType;
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.EnginePayloadParameter;
import org.hyperledger.besu.ethereum.core.BlockHeader;
import org.hyperledger.besu.ethereum.core.Difficulty;
import org.hyperledger.besu.ethereum.core.Request;
import org.hyperledger.besu.ethereum.mainnet.BodyValidation;
import org.hyperledger.besu.ethereum.mainnet.MainnetBlockHeaderFunctions;
import org.hyperledger.besu.tests.acceptance.dsl.account.Accounts;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
/**
* Tests that verify the LineaTransactionValidationPlugin correctly rejects BLOB transactions from
* being executed
*/
public class BlobTransactionDenialTest extends LineaPluginTestBasePrague {
private Web3j web3j;
private Credentials credentials;
private String recipient;
@Override
protected String getGenesisFileTemplatePath() {
// We cannot use clique-prague-zero-blobs because `config.blobSchedule.prague.max = 0` will
// block all blob txs
return "/clique/clique-prague-one-blob.json.tpl";
}
@Override
@BeforeEach
public void setup() throws Exception {
super.setup();
web3j = minerNode.nodeRequests().eth();
credentials = Credentials.create(Accounts.GENESIS_ACCOUNT_ONE_PRIVATE_KEY);
recipient = accounts.getSecondaryBenefactor().getAddress();
}
@Test
public void blobTransactionsIsRejectedFromTransactionPool() throws Exception {
// Act - Send a blob transaction to transaction pool
EthSendTransaction response = sendRawBlobTransaction(web3j, credentials, recipient);
// No need to build new block.
// Assert
assertThat(response.hasError()).isTrue();
assertThat(response.getError().getMessage())
.contains("Plugin has marked the transaction as invalid");
}
// Ideally the block import test would be conducted with two nodes as follows:
// 1. Start an additional minimal node with Prague config
// 2. Ensure additional node is peered to minerNode
// 3. Send blob tx to additional node
// 4. Construct block on additional node
// 5. Send 'debug_getBadBlocks' RPC request to minerNode, confirm that block is rejected from
// import
//
// However we are currently unable to run more than one node per test, due to the CLI options
// being
// singleton options and this implemented in dependency repository - linea tracer.
// Thus simulate the block import as below:
// 1. Create a premade block containing a blob tx
// 2. Import the premade block using 'engine_newPayloadV4' Engine API call
@Test
public void blobTransactionsIsRejectedFromNodeImport() throws Exception {
// Arrange
EngineNewPayloadRequest blockWithBlobTxRequest = getBlockWithBlobTxRequest(mapper);
// Act
Response response =
this.importPremadeBlock(
blockWithBlobTxRequest.executionPayload(),
blockWithBlobTxRequest.expectedBlobVersionedHashes(),
blockWithBlobTxRequest.parentBeaconBlockRoot(),
blockWithBlobTxRequest.executionRequests());
// Assert
JsonNode result = mapper.readTree(response.body().string()).get("result");
String status = result.get("status").asText();
String validationError = result.get("validationError").asText();
assertThat(status).isEqualTo("INVALID");
assertThat(validationError).contains("LineaTransactionValidatorPlugin - BLOB_TX_NOT_ALLOWED");
}
private record EngineNewPayloadRequest(
ObjectNode executionPayload,
ArrayNode expectedBlobVersionedHashes,
String parentBeaconBlockRoot,
ArrayNode executionRequests) {}
private EngineNewPayloadRequest getBlockWithBlobTxRequest(ObjectMapper mapper) throws Exception {
// Obtained following values by running `blobTransactionsIsRejectedFromTransactionPool` test
// without the LineaTransactionSelectorPlugin and LineaTransactionValidatorPlugin plugins.
Map<String, String> blockWithBlockTxParams = new HashMap<>();
blockWithBlockTxParams.put(
"STATE_ROOT", "0x2c1457760c057cf42f2d509648d725ec1f557b9d8729a5361e517952f91d050e");
blockWithBlockTxParams.put(
"LOGS_BLOOM",
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
blockWithBlockTxParams.put(
"RECEIPTS_ROOT", "0xeaa8c40899a61ae59615cf9985f5e2194f8fd2b57d273be63bde6733e89b12ab");
blockWithBlockTxParams.put("EXTRA_DATA", "0x626573752032352e362e302d6c696e656131");
blockWithBlockTxParams.put(
"BLOB_TX",
"0x03f8908205398084f461090084f46109008389544094627306090abab3a6e1400e9345bc60c78a8bef578080c001e1a0018ef96865998238a5e1783b6cafbc1253235d636f15d318f1fb50ef6a5b8f6a80a0576a95756f32ab705a22b591ab464d5affc8c1c7fcd14d777bac24d83bc44821a01f93b26f4f9989c3fe764f4a58d264bcd71b9deab72d6852f5dcdf19d55494f1");
blockWithBlockTxParams.put(
"BLOB_VERSIONED_HASH",
"0x018ef96865998238a5e1783b6cafbc1253235d636f15d318f1fb50ef6a5b8f6a");
blockWithBlockTxParams.put(
"EXECUTION_REQUEST",
"0x01a4664c40aacebd82a2db79f0ea36c06bc6a19adbb10a4a15bf67b328c9b101d09e5c6ee6672978fdad9ef0d9e2ceffaee99223555d8601f0cb3bcc4ce1af9864779a416e0000000000000000");
blockWithBlockTxParams.put(
"TRANSACTIONS_ROOT", "0x7a430a1c9da1f6e25ff8e6e96217c359784f3438dc1d983b4695355d66437f8f");
blockWithBlockTxParams.put(
"WITHDRAWALS_ROOT", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421");
blockWithBlockTxParams.put("GAS_LIMIT", "0x1ca35ef");
blockWithBlockTxParams.put("GAS_USED", "0x5208");
blockWithBlockTxParams.put("TIMESTAMP", "0x5");
blockWithBlockTxParams.put("BASE_FEE_PER_GAS", "0x7");
blockWithBlockTxParams.put("EXCESS_BLOB_GAS", "0x0");
blockWithBlockTxParams.put("BLOB_GAS_USED", "0x20000");
blockWithBlockTxParams.put("BLOCK_NUMBER", "0x1");
blockWithBlockTxParams.put("FEE_RECIPIENT", Address.ZERO.toHexString());
blockWithBlockTxParams.put("PREV_RANDAO", Hash.ZERO.toHexString());
blockWithBlockTxParams.put("PARENT_BEACON_BLOCK_ROOT", Hash.ZERO.toHexString());
blockWithBlockTxParams = Collections.unmodifiableMap(blockWithBlockTxParams);
// Seems that the genesis block hash change with each run, despite a constant genesis file
String genesisBlockHash = getLatestBlockHash();
ObjectNode executionPayload =
createExecutionPayload(mapper, genesisBlockHash, blockWithBlockTxParams);
ArrayNode expectedBlobVersionedHashes =
createBlobVersionedHashes(mapper, blockWithBlockTxParams);
ArrayNode executionRequests = createExecutionRequests(mapper, blockWithBlockTxParams);
// Compute block hash and update payload
BlockHeader blockHeader = computeBlockHeader(executionPayload, mapper, blockWithBlockTxParams);
updateExecutionPayloadWithBlockHash(executionPayload, blockHeader);
return new EngineNewPayloadRequest(
executionPayload,
expectedBlobVersionedHashes,
blockWithBlockTxParams.get("PARENT_BEACON_BLOCK_ROOT"),
executionRequests);
}
private String getLatestBlockHash() throws Exception {
return web3j
.ethGetBlockByNumber(org.web3j.protocol.core.DefaultBlockParameterName.LATEST, false)
.send()
.getBlock()
.getHash();
}
private ObjectNode createExecutionPayload(
ObjectMapper mapper, String genesisBlockHash, Map<String, String> blockParams) {
ObjectNode payload =
mapper
.createObjectNode()
.put("parentHash", genesisBlockHash)
.put("feeRecipient", blockParams.get("FEE_RECIPIENT"))
.put("stateRoot", blockParams.get("STATE_ROOT"))
.put("logsBloom", blockParams.get("LOGS_BLOOM"))
.put("prevRandao", blockParams.get("PREV_RANDAO"))
.put("gasLimit", blockParams.get("GAS_LIMIT"))
.put("gasUsed", blockParams.get("GAS_USED"))
.put("timestamp", blockParams.get("TIMESTAMP"))
.put("extraData", blockParams.get("EXTRA_DATA"))
.put("baseFeePerGas", blockParams.get("BASE_FEE_PER_GAS"))
.put("excessBlobGas", blockParams.get("EXCESS_BLOB_GAS"))
.put("blobGasUsed", blockParams.get("BLOB_GAS_USED"))
.put("receiptsRoot", blockParams.get("RECEIPTS_ROOT"))
.put("blockNumber", blockParams.get("BLOCK_NUMBER"));
// Add transactions (blob tx)
ArrayNode transactions = mapper.createArrayNode();
transactions.add(blockParams.get("BLOB_TX"));
payload.set("transactions", transactions);
// Add withdrawals (empty list)
ArrayNode withdrawals = mapper.createArrayNode();
payload.set("withdrawals", withdrawals);
return payload;
}
private ArrayNode createBlobVersionedHashes(
ObjectMapper mapper, Map<String, String> blockParams) {
ArrayNode hashes = mapper.createArrayNode();
hashes.add(blockParams.get("BLOB_VERSIONED_HASH"));
return hashes;
}
private ArrayNode createExecutionRequests(ObjectMapper mapper, Map<String, String> blockParams) {
ArrayNode requests = mapper.createArrayNode();
requests.add(blockParams.get("EXECUTION_REQUEST"));
return requests;
}
private BlockHeader computeBlockHeader(
ObjectNode executionPayload, ObjectMapper mapper, Map<String, String> blockParams)
throws Exception {
EnginePayloadParameter blockParam =
mapper.readValue(executionPayload.toString(), EnginePayloadParameter.class);
Hash transactionsRoot = Hash.fromHexString(blockParams.get("TRANSACTIONS_ROOT"));
Hash withdrawalsRoot = Hash.fromHexString(blockParams.get("WITHDRAWALS_ROOT"));
// Take code from AbstractEngineNewPayload in Besu codebase
Bytes executionRequestBytes = Bytes.fromHexString(blockParams.get("EXECUTION_REQUEST"));
Bytes executionRequestBytesData = executionRequestBytes.slice(1);
Request executionRequest =
new Request(RequestType.of(executionRequestBytes.get(0)), executionRequestBytesData);
Optional<List<Request>> maybeRequests = Optional.of(List.of(executionRequest));
return new BlockHeader(
blockParam.getParentHash(),
Hash.EMPTY_LIST_HASH, // OMMERS_HASH_CONSTANT
blockParam.getFeeRecipient(),
blockParam.getStateRoot(),
transactionsRoot,
blockParam.getReceiptsRoot(),
blockParam.getLogsBloom(),
Difficulty.ZERO,
blockParam.getBlockNumber(),
blockParam.getGasLimit(),
blockParam.getGasUsed(),
blockParam.getTimestamp(),
Bytes.fromHexString(blockParam.getExtraData()),
blockParam.getBaseFeePerGas(),
blockParam.getPrevRandao(),
0, // Nonce
withdrawalsRoot,
blockParam.getBlobGasUsed(),
BlobGas.fromHexString(blockParam.getExcessBlobGas()),
Bytes32.fromHexString(blockParams.get("PARENT_BEACON_BLOCK_ROOT")),
maybeRequests.map(BodyValidation::requestsHash).orElse(null),
new MainnetBlockHeaderFunctions());
}
private void updateExecutionPayloadWithBlockHash(
ObjectNode executionPayload, BlockHeader blockHeader) {
executionPayload.put("blockHash", blockHeader.getBlockHash().toHexString());
}
}

View File

@@ -91,6 +91,17 @@ public abstract class LineaPluginTestBase extends AcceptanceTestBase {
public static final int BLOCK_PERIOD_SECONDS = 5;
public static final CliqueOptions DEFAULT_LINEA_CLIQUE_OPTIONS =
new CliqueOptions(BLOCK_PERIOD_SECONDS, CliqueOptions.DEFAULT.epochLength(), false);
protected static final List<String> DEFAULT_REQUESTED_PLUGINS =
List.of(
"LineaExtraDataPlugin",
"LineaEstimateGasEndpointPlugin",
"LineaSetExtraDataEndpointPlugin",
"LineaTransactionPoolValidatorPlugin",
"LineaTransactionSelectorPlugin",
"LineaBundleEndpointsPlugin",
"ForwardBundlesPlugin",
"LineaTransactionValidatorPlugin");
protected static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
protected BesuNode minerNode;
@@ -98,7 +109,12 @@ public abstract class LineaPluginTestBase extends AcceptanceTestBase {
public void setup() throws Exception {
minerNode =
createCliqueNodeWithExtraCliOptionsAndRpcApis(
"miner1", getCliqueOptions(), getTestCliOptions(), Set.of("LINEA", "MINER"), false);
"miner1",
getCliqueOptions(),
getTestCliOptions(),
Set.of("LINEA", "MINER"),
false,
DEFAULT_REQUESTED_PLUGINS);
minerNode.setTransactionPoolConfiguration(
ImmutableTransactionPoolConfiguration.builder()
.from(TransactionPoolConfiguration.DEFAULT)
@@ -131,7 +147,8 @@ public abstract class LineaPluginTestBase extends AcceptanceTestBase {
final CliqueOptions cliqueOptions,
final List<String> extraCliOptions,
final Set<String> extraRpcApis,
final boolean isEngineRpcEnabled)
final boolean isEngineRpcEnabled,
final List<String> requestedPlugins)
throws IOException {
final NodeConfigurationFactory node = new NodeConfigurationFactory();
@@ -155,15 +172,7 @@ public abstract class LineaPluginTestBase extends AcceptanceTestBase {
.metricCategories(
Set.of(PRICING_CONF, SEQUENCER_PROFITABILITY, TX_POOL_PROFITABILITY))
.build())
.requestedPlugins(
List.of(
"LineaExtraDataPlugin",
"LineaEstimateGasEndpointPlugin",
"LineaSetExtraDataEndpointPlugin",
"LineaTransactionPoolValidatorPlugin",
"LineaTransactionSelectorPlugin",
"LineaBundleEndpointsPlugin",
"ForwardBundlesPlugin"));
.requestedPlugins(requestedPlugins);
return besu.create(nodeConfBuilder.build());
}

View File

@@ -11,12 +11,21 @@ package linea.plugin.acc.test;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.io.Resources;
import java.io.File;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import org.apache.tuweni.bytes.Bytes;
import org.hyperledger.besu.consensus.clique.CliqueExtraData;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration;
@@ -26,21 +35,45 @@ import org.hyperledger.besu.tests.acceptance.dsl.node.RunnableNode;
import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory;
import org.hyperledger.besu.tests.acceptance.dsl.node.configuration.genesis.GenesisConfigurationFactory.CliqueOptions;
import org.junit.jupiter.api.BeforeEach;
import org.web3j.crypto.Blob;
import org.web3j.crypto.BlobUtils;
import org.web3j.crypto.Credentials;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.core.DefaultBlockParameterName;
import org.web3j.protocol.core.methods.response.EthSendTransaction;
import org.web3j.tx.gas.DefaultGasProvider;
import org.web3j.utils.Numeric;
// This file initializes a Besu node configured for the Prague fork and makes it available to
// acceptance tests.
@Slf4j
public abstract class LineaPluginTestBasePrague extends LineaPluginTestBase {
private EngineAPIService engineApiService;
private ObjectMapper mapper;
private final String GENESIS_FILE_TEMPLATE_PATH = "/clique/clique-prague.json.tpl";
protected ObjectMapper mapper;
private static final BigInteger GAS_PRICE = DefaultGasProvider.GAS_PRICE;
private static final BigInteger GAS_LIMIT = DefaultGasProvider.GAS_LIMIT;
private static final BigInteger VALUE = BigInteger.ZERO;
private static final String DATA = "0x";
// Override this in subclasses to use a different genesis file template
protected String getGenesisFileTemplatePath() {
return "/clique/clique-prague-no-blobs.json.tpl";
}
@BeforeEach
@Override
public void setup() throws Exception {
minerNode =
createCliqueNodeWithExtraCliOptionsAndRpcApis(
"miner1", getCliqueOptions(), getTestCliOptions(), Set.of("LINEA", "MINER"), true);
"miner1",
getCliqueOptions(),
getTestCliOptions(),
Set.of("LINEA", "MINER"),
true,
DEFAULT_REQUESTED_PLUGINS);
minerNode.setTransactionPoolConfiguration(
ImmutableTransactionPoolConfiguration.builder()
.from(TransactionPoolConfiguration.DEFAULT)
@@ -59,7 +92,7 @@ public abstract class LineaPluginTestBasePrague extends LineaPluginTestBase {
final Collection<? extends RunnableNode> validators, final CliqueOptions cliqueOptions) {
// Target state
final String genesisTemplate =
GenesisConfigurationFactory.readGenesisFile(GENESIS_FILE_TEMPLATE_PATH);
GenesisConfigurationFactory.readGenesisFile(getGenesisFileTemplatePath());
final String hydratedGenesisTemplate =
genesisTemplate
.replace("%blockperiodseconds%", String.valueOf(cliqueOptions.blockPeriodSeconds()))
@@ -94,4 +127,55 @@ public abstract class LineaPluginTestBasePrague extends LineaPluginTestBase {
throws IOException, InterruptedException {
this.engineApiService.buildNewBlock(blockTimestampSeconds, blockBuildingTimeMs);
}
/**
* Creates and sends a blob transaction. This method is designed to be stateless and should not
* rely on any class properties or instance methods. All required data should be passed as
* parameters. This makes it easier to test and reuse in different contexts.
*/
protected EthSendTransaction sendRawBlobTransaction(
Web3j web3j, Credentials credentials, String recipient) throws IOException {
BigInteger nonce =
web3j
.ethGetTransactionCount(credentials.getAddress(), DefaultBlockParameterName.PENDING)
.send()
.getTransactionCount();
// Take blob file from public reference so we can sanity check values -
// https://github.com/LFDT-web3j/web3j/blob/9dbd2f90468538408eeb9a1e87e8e73a9f3dda3b/crypto/src/test/java/org/web3j/crypto/BlobUtilsTest.java#L63-L83
URL blobUrl = new File(getResourcePath("/blob.txt")).toURI().toURL();
final var blobHexString = Resources.toString(blobUrl, StandardCharsets.UTF_8);
final Blob blob = new Blob(Numeric.hexStringToByteArray(blobHexString));
final Bytes kzgCommitment = BlobUtils.getCommitment(blob);
final Bytes kzgProof = BlobUtils.getProof(blob, kzgCommitment);
final Bytes versionedHash = BlobUtils.kzgToVersionedHash(kzgCommitment);
final RawTransaction rawTransaction =
RawTransaction.createTransaction(
List.of(blob),
List.of(kzgCommitment),
List.of(kzgProof),
CHAIN_ID,
nonce,
GAS_PRICE,
GAS_PRICE,
GAS_LIMIT,
recipient,
VALUE,
DATA,
BigInteger.ONE,
List.of(versionedHash));
byte[] signedMessage = TransactionEncoder.signMessage(rawTransaction, credentials);
String hexValue = Numeric.toHexString(signedMessage);
return web3j.ethSendRawTransaction(hexValue).send();
}
protected Response importPremadeBlock(
final ObjectNode executionPayload,
final ArrayNode expectedBlobVersionedHashes,
final String parentBeaconBlockRoot,
final ArrayNode executionRequests)
throws IOException, InterruptedException {
return this.engineApiService.importPremadeBlock(
executionPayload, expectedBlobVersionedHashes, parentBeaconBlockRoot, executionRequests);
}
}

View File

@@ -23,9 +23,12 @@ import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.apache.tuweni.bytes.Bytes;
import org.hyperledger.besu.datatypes.Address;
import org.hyperledger.besu.datatypes.Hash;
import org.hyperledger.besu.tests.acceptance.dsl.node.BesuNode;
import org.hyperledger.besu.tests.acceptance.dsl.transaction.eth.EthTransactions;
import org.web3j.crypto.BlobUtils;
import org.web3j.protocol.core.methods.response.EthBlock;
/*
@@ -40,8 +43,6 @@ public class EngineAPIService {
private static final String JSONRPC_VERSION = "2.0";
private static final long JSONRPC_REQUEST_ID = 67;
private static final String SUGGESTED_BLOCK_FEE_RECIPIENT =
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b";
public EngineAPIService(BesuNode node, EthTransactions ethTransactions, ObjectMapper mapper) {
httpClient = new OkHttpClient();
@@ -108,21 +109,33 @@ public class EngineAPIService {
final Call getPayloadRequest = createGetPayloadRequest(payloadId);
final ObjectNode executionPayload;
final ObjectNode blobsBundle;
final ArrayNode executionRequests;
final String newBlockHash;
final String parentBeaconBlockRoot;
ArrayNode expectedBlobVersionedHashes = mapper.createArrayNode();
try (final Response getPayloadResponse = getPayloadRequest.execute()) {
assertThat(getPayloadResponse.code()).isEqualTo(200);
JsonNode result = mapper.readTree(getPayloadResponse.body().string()).get("result");
executionPayload = (ObjectNode) result.get("executionPayload");
blobsBundle = (ObjectNode) result.get("blobsBundle");
executionRequests = (ArrayNode) result.get("executionRequests");
newBlockHash = executionPayload.get("blockHash").asText();
parentBeaconBlockRoot = executionPayload.remove("parentBeaconBlockRoot").asText();
// Transform KZG commitments to versioned hashes
for (JsonNode kzgCommitment : blobsBundle.get("commitments")) {
Bytes kzgBytes = Bytes.fromHexString(kzgCommitment.asText());
expectedBlobVersionedHashes.add(BlobUtils.kzgToVersionedHash(kzgBytes).toString());
}
assertThat(newBlockHash).isNotEmpty();
}
final Call newPayloadRequest =
createNewPayloadRequest(executionPayload, parentBeaconBlockRoot, executionRequests);
createNewPayloadRequest(
executionPayload,
expectedBlobVersionedHashes,
parentBeaconBlockRoot,
executionRequests);
try (final Response newPayloadResponse = newPayloadRequest.execute()) {
assertThat(newPayloadResponse.code()).isEqualTo(200);
@@ -138,6 +151,22 @@ public class EngineAPIService {
}
}
public Response importPremadeBlock(
final ObjectNode executionPayload,
final ArrayNode expectedBlobVersionedHashes,
final String parentBeaconBlockRoot,
final ArrayNode executionRequests)
throws IOException, InterruptedException {
final Call newPayloadRequest =
createNewPayloadRequest(
executionPayload,
expectedBlobVersionedHashes,
parentBeaconBlockRoot,
executionRequests);
return newPayloadRequest.execute();
}
private Call createForkChoiceRequest(final String blockHash) {
return createForkChoiceRequest(blockHash, null);
}
@@ -158,7 +187,7 @@ public class EngineAPIService {
ObjectNode payloadAttributes = mapper.createObjectNode();
payloadAttributes.put("timestamp", blockTimestamp);
payloadAttributes.put("prevRandao", Hash.ZERO.toString());
payloadAttributes.put("suggestedFeeRecipient", SUGGESTED_BLOCK_FEE_RECIPIENT);
payloadAttributes.put("suggestedFeeRecipient", Address.ZERO.toString());
payloadAttributes.set("withdrawals", mapper.createArrayNode());
payloadAttributes.put("parentBeaconBlockRoot", Hash.ZERO.toString());
params.add(payloadAttributes);
@@ -174,11 +203,12 @@ public class EngineAPIService {
private Call createNewPayloadRequest(
final ObjectNode executionPayload,
final ArrayNode expectedBlobVersionedHashes,
final String parentBeaconBlockRoot,
final ArrayNode executionRequests) {
ArrayNode params = mapper.createArrayNode();
params.add(executionPayload);
params.add(mapper.createArrayNode()); // empty withdrawals
params.add(expectedBlobVersionedHashes);
params.add(parentBeaconBlockRoot);
params.add(executionRequests);

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,117 @@
{
"config": {
"chainId": 1337,
"petersburgBlock": 0,
"istanbulBlock": 0,
"berlinBlock": 0,
"londonBlock": 0,
"terminalTotalDifficulty":0,
"cancunTime":0,
"pragueTime":0,
"blobSchedule": {
"cancun": {
"target": 0,
"max": 0,
"baseFeeUpdateFraction": 3338477
},
"prague": {
"target": 0,
"max": 1,
"baseFeeUpdateFraction": 5007716
},
"osaka": {
"target": 0,
"max": 0,
"baseFeeUpdateFraction": 5007716
}
},
"clique": {
"blockperiodseconds": %blockperiodseconds%,
"epochlength": %epochlength%,
"createemptyblocks": %createemptyblocks%
},
"depositContractAddress": "0x4242424242424242424242424242424242424242",
"withdrawalRequestContractAddress": "0x00A3ca265EBcb825B45F985A16CEFB49958cE017",
"consolidationRequestContractAddress": "0x00b42dbF2194e931E80326D950320f7d9Dbeac02"
},
"zeroBaseFee": false,
"baseFeePerGas": "7",
"nonce": "0x0",
"timestamp": "0x0",
"extraData": "%extraData%",
"gasLimit": "0x1C9C380",
"difficulty": "0x1",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000",
"alloc": {
"fe3b557e8fb62b89f4916b721be55ceb828dbd73": {
"privateKey": "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "0xad78ebc5ac6200000"
},
"627306090abaB3A6e1400e9345bC60c78a8BEf57": {
"privateKey": "c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "90000000000000000000000"
},
"f17f52151EbEF6C7334FAD080c5704D77216b732": {
"privateKey": "ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f",
"comment": "private key and this comment are ignored. In a real chain, the private key should NOT be stored",
"balance": "90000000000000000000000"
},
"a05b21E5186Ce93d2a226722b85D6e550Ac7D6E3": {
"privateKey": "3a4ff6d22d7502ef2452368165422861c01a0f72f851793b372b87888dc3c453",
"balance": "90000000000000000000000"
},
"8da48afC965480220a3dB9244771bd3afcB5d895": {
"comment": "This account has signed a authorization for contract 0x0000000000000000000000000000000000009999 to send a 7702 transaction",
"privateKey": "11f2e7b6a734ab03fa682450e0d4681d18a944f8b83c99bf7b9b4de6c0f35ea1",
"balance": "90000000000000000000000"
},
"0x0000000000000000000000000000000000000666": {
"comment": "Contract reverts immediately when called",
"balance": "0",
"code": "5F5FFD",
"codeDecompiled": "PUSH0 PUSH0 REVERT",
"storage": {}
},
"0x0000000000000000000000000000000000009999": {
"comment": "Contract sends all its Ether to the address provided as a call data.",
"balance": "0",
"code": "5F5F5F5F475F355AF100",
"codeDecompiled": "PUSH0 PUSH0 PUSH0 PUSH0 SELFBALANCE PUSH0 CALLDATALOAD GAS CALL STOP",
"storage": {}
},
"0xa4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb": {
"balance": "1000000000000000000000000000"
},
"0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f": {
"comment": "This is the account used to sign the transaction that creates a validator exit",
"balance": "1000000000000000000000000000"
},
"0x00A3ca265EBcb825B45F985A16CEFB49958cE017": {
"comment": "This is the runtime bytecode for the Withdrawal Request Smart Contract. It was created from the generated alloc section of fork_Prague_blockchain_test_engine_single_block_single_withdrawal_request_from_contract spec test",
"balance": "0",
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460c7573615156028575f545f5260205ff35b36603814156101f05760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f057600182026001905f5b5f821115608057810190830284830290049160010191906065565b9093900434106101f057600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160db575060105b5f5b81811461017f5780604c02838201600302600401805490600101805490600101549160601b83528260140152807fffffffffffffffffffffffffffffffff0000000000000000000000000000000016826034015260401c906044018160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160dd565b9101809214610191579060025561019c565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101c957505f5b6001546002828201116101de5750505f6101e4565b01600290035b5f555f600155604c025ff35b5f5ffd",
"storage": {
"0x0000000000000000000000000000000000000000000000000000000000000000": "0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000003": "0000000000000000000000000000000000000000000000000000000000000001",
"0x0000000000000000000000000000000000000000000000000000000000000004": "000000000000000000000000a4664C40AACeBD82A2Db79f0ea36C06Bc6A19Adb",
"0x0000000000000000000000000000000000000000000000000000000000000005": "b10a4a15bf67b328c9b101d09e5c6ee6672978fdad9ef0d9e2ceffaee9922355",
"0x0000000000000000000000000000000000000000000000000000000000000006": "5d8601f0cb3bcc4ce1af9864779a416e00000000000000000000000000000000"
}
},
"0x00b42dbF2194e931E80326D950320f7d9Dbeac02": {
"comment": "This is the runtime bytecode for the Consolidation Request Smart Contract",
"nonce": "0x01",
"balance": "0x00",
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500",
"storage": {}
}
},
"number": "0x0",
"gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000"
}

View File

@@ -40,6 +40,11 @@ afterEvaluate {
subproject.tasks.matching { task -> task.name == 'spotlessCheck' }
}
}
if (tasks.findByName('spotlessApply')) {
spotlessApply.dependsOn subprojects.collect { subproject ->
subproject.tasks.matching { task -> task.name == 'spotlessApply' }
}
}
}
build {

View File

@@ -91,6 +91,21 @@ The validators are in the package `net.consensys.linea.sequencer.txpoolvalidatio
| `--plugin-linea-tx-pool-profitability-check-api-enabled` | true |
| `--plugin-linea-tx-pool-profitability-check-p2p-enabled` | false |
### Transaction validation - LineaTransactionValidatorPlugin
This plugin uses Besu's `TransactionValidatorService` to filter transactions at multiple critical lifecycle stages:
1. Block import - when a Besu validator receives a block via P2P gossip
2. Transaction pool - when a Besu node adds to its local transaction pool
3. Block production - when a Besu node builds a block
The plugin implements transaction filtering for blob transactions (EIP-4844), which can be enabled or disabled via configuration. In the future, this plugin may consolidate logic from other transaction-filtering plugins to unify transaction filtering logic.
#### CLI options
| Command Line Argument | Default Value |
|--------------------------------------|---------------|
| `--plugin-linea-blob-tx-enabled` | false |
### Reporting rejected transactions
The transaction selection and validation plugins can report rejected transactions as JSON-RPC calls to an external
service. This feature can be enabled by setting the following CLI options:

View File

@@ -29,6 +29,8 @@ import net.consensys.linea.config.LineaTransactionPoolValidatorCliOptions;
import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration;
import net.consensys.linea.config.LineaTransactionSelectorCliOptions;
import net.consensys.linea.config.LineaTransactionSelectorConfiguration;
import net.consensys.linea.config.LineaTransactionValidatorCliOptions;
import net.consensys.linea.config.LineaTransactionValidatorConfiguration;
import net.consensys.linea.plugins.AbstractLineaSharedOptionsPlugin;
import net.consensys.linea.plugins.LineaOptionsPluginConfiguration;
import net.consensys.linea.utils.Compressor;
@@ -95,6 +97,9 @@ public abstract class AbstractLineaSharedPrivateOptionsPlugin
LineaRejectedTxReportingCliOptions.create().asPluginConfig());
configMap.put(
LineaBundleCliOptions.CONFIG_KEY, LineaBundleCliOptions.create().asPluginConfig());
configMap.put(
LineaTransactionValidatorCliOptions.CONFIG_KEY,
LineaTransactionValidatorCliOptions.create().asPluginConfig());
return configMap;
}
@@ -133,6 +138,11 @@ public abstract class AbstractLineaSharedPrivateOptionsPlugin
getConfigurationByKey(LineaBundleCliOptions.CONFIG_KEY).optionsConfig();
}
public LineaTransactionValidatorConfiguration transactionValidatorConfiguration() {
return (LineaTransactionValidatorConfiguration)
getConfigurationByKey(LineaTransactionValidatorCliOptions.CONFIG_KEY).optionsConfig();
}
@Override
public synchronized void register(final ServiceManager serviceManager) {
super.register(serviceManager);

View File

@@ -0,0 +1,68 @@
/*
* 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 com.google.common.base.MoreObjects;
import net.consensys.linea.plugins.LineaCliOptions;
import picocli.CommandLine;
/** CLI options specific to the LineaTransactionValidator Plugin. */
public class LineaTransactionValidatorCliOptions implements LineaCliOptions {
public static final String CONFIG_KEY = "transaction-validator-config";
public static final String BLOB_TX_ENABLED = "--plugin-linea-blob-tx-enabled";
public static final boolean DEFAULT_BLOB_TX_ENABLED = false;
@CommandLine.Option(
names = {BLOB_TX_ENABLED},
arity = "0..1",
hidden = true,
paramLabel = "<BOOLEAN>",
description = "Enable blob transactions? (default: ${DEFAULT-VALUE})")
private boolean blobTxEnabled = DEFAULT_BLOB_TX_ENABLED;
public LineaTransactionValidatorCliOptions() {}
/**
* Create Linea cli options.
*
* @return the Linea cli options
*/
public static LineaTransactionValidatorCliOptions create() {
return new LineaTransactionValidatorCliOptions();
}
/**
* Cli options from config.
*
* @param config the config
* @return the cli options
*/
public static LineaTransactionValidatorCliOptions fromConfig(
final LineaTransactionValidatorConfiguration config) {
final LineaTransactionValidatorCliOptions options = create();
options.blobTxEnabled = config.blobTxEnabled();
return options;
}
/**
* To domain object Linea factory configuration.
*
* @return the Linea factory configuration
*/
@Override
public LineaTransactionValidatorConfiguration toDomainObject() {
return new LineaTransactionValidatorConfiguration(blobTxEnabled);
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this).add(BLOB_TX_ENABLED, blobTxEnabled).toString();
}
}

View File

@@ -0,0 +1,17 @@
/*
* 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 lombok.Builder;
import net.consensys.linea.plugins.LineaOptionsConfiguration;
/** The Linea transaction validation configuration. */
@Builder(toBuilder = true)
public record LineaTransactionValidatorConfiguration(boolean blobTxEnabled)
implements LineaOptionsConfiguration {}

View File

@@ -0,0 +1,77 @@
/*
* 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.sequencer.txvalidation;
import com.google.auto.service.AutoService;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import net.consensys.linea.AbstractLineaRequiredPlugin;
import net.consensys.linea.config.LineaTransactionValidatorConfiguration;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.plugin.BesuPlugin;
import org.hyperledger.besu.plugin.ServiceManager;
import org.hyperledger.besu.plugin.services.TransactionValidatorService;
/**
* This class 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, for example, setting a deny list of addresses
* that are not allowed to add transactions to the pool.
*/
@Slf4j
@AutoService(BesuPlugin.class)
public class LineaTransactionValidatorPlugin extends AbstractLineaRequiredPlugin {
private TransactionValidatorService transactionValidatorService;
private LineaTransactionValidatorConfiguration config;
public enum LineaTransactionValidatorError {
BLOB_TX_NOT_ALLOWED;
@Override
public String toString() {
return "LineaTransactionValidatorPlugin - " + name();
}
}
@Override
public void doRegister(final ServiceManager serviceManager) {
transactionValidatorService =
serviceManager
.getService(TransactionValidatorService.class)
.orElseThrow(
() ->
new RuntimeException(
"Failed to obtain TransactionValidatorService from the ServiceManager."));
}
// CLI config is not available in doRegister
// 'registerTransactionValidatorRule' does not do anything if done in doStart
// Therefore we must use beforeExternalServices hook
@Override
public void beforeExternalServices() {
super.beforeExternalServices();
this.config = transactionValidatorConfiguration();
// Register rule to reject blob transactions
this.transactionValidatorService.registerTransactionValidatorRule(
(tx) -> {
if (tx.getType() == TransactionType.BLOB && !config.blobTxEnabled())
return Optional.of(LineaTransactionValidatorError.BLOB_TX_NOT_ALLOWED.toString());
return Optional.empty();
});
}
@Override
public void doStart() {}
@Override
public void stop() {
super.stop();
}
}

View File

@@ -0,0 +1,188 @@
/*
* 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.sequencer.txvalidation;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Optional;
import net.consensys.linea.config.LineaTransactionValidatorConfiguration;
import net.consensys.linea.sequencer.txvalidation.LineaTransactionValidatorPlugin.LineaTransactionValidatorError;
import org.hyperledger.besu.datatypes.TransactionType;
import org.hyperledger.besu.ethereum.core.Transaction;
import org.hyperledger.besu.plugin.ServiceManager;
import org.hyperledger.besu.plugin.services.TransactionValidatorService;
import org.hyperledger.besu.plugin.services.txvalidator.TransactionValidationRule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class LineaTransactionValidatorPluginTest {
@Mock private ServiceManager serviceManager;
@Mock private TransactionValidatorService transactionValidatorService;
@Mock private Transaction transaction;
@Mock private LineaTransactionValidatorConfiguration lineaTransactionValidatorConfiguration;
private LineaTransactionValidatorPlugin plugin;
@BeforeEach
public void setUp() {
plugin =
new LineaTransactionValidatorPlugin() {
@Override
public LineaTransactionValidatorConfiguration transactionValidatorConfiguration() {
return lineaTransactionValidatorConfiguration;
}
};
when(serviceManager.getService(TransactionValidatorService.class))
.thenReturn(Optional.of(transactionValidatorService));
}
@Test
public void shouldRegisterWithServiceManager() {
// Act
plugin.doRegister(serviceManager);
// Assert
verify(serviceManager).getService(TransactionValidatorService.class);
}
@Test
public void shouldThrowExceptionWhenTransactionValidatorServiceNotAvailable() {
// Arrange
when(serviceManager.getService(TransactionValidatorService.class)).thenReturn(Optional.empty());
// Act/Assert
assertThatThrownBy(() -> plugin.doRegister(serviceManager))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining(
"Failed to obtain TransactionValidatorService from the ServiceManager");
}
@Test
public void shouldRegisterTransactionValidatorRule() {
// Arrange
plugin.doRegister(serviceManager);
// Act
plugin.beforeExternalServices();
// Assert
verify(transactionValidatorService).registerTransactionValidatorRule(any());
}
@Test
public void shouldRejectBlobTransactionsByDefault() {
// Arrange
when(lineaTransactionValidatorConfiguration.blobTxEnabled()).thenReturn(false);
plugin.doRegister(serviceManager);
plugin.beforeExternalServices();
final TransactionValidationRule validatorRule = this.getTransactionValidatorRule();
// Act - BLOB transaction
when(transaction.getType()).thenReturn(TransactionType.BLOB);
Optional<String> result = validatorRule.validate(transaction);
// Assert
assertThat(result).isPresent();
assertThat(result.get())
.isEqualTo(LineaTransactionValidatorError.BLOB_TX_NOT_ALLOWED.toString());
}
@Test
public void shouldPermitEIP7702Transactions() {
// Arrange
plugin.doRegister(serviceManager);
plugin.beforeExternalServices();
final TransactionValidationRule validatorRule = this.getTransactionValidatorRule();
// Act - EIP7702 transaction
when(transaction.getType()).thenReturn(TransactionType.DELEGATE_CODE);
Optional<String> result = validatorRule.validate(transaction);
// Assert
assertThat(result).isEmpty();
}
@Test
public void shouldPermitLegacyTransactions() {
// Arrange
plugin.doRegister(serviceManager);
plugin.beforeExternalServices();
final TransactionValidationRule validatorRule = this.getTransactionValidatorRule();
// Act - LEGACY/FRONTIER transaction
when(transaction.getType()).thenReturn(TransactionType.FRONTIER);
Optional<String> result = validatorRule.validate(transaction);
// Assert
assertThat(result).isEmpty();
}
@Test
public void shouldPermitAccessListTransactions() {
// Arrange
plugin.doRegister(serviceManager);
plugin.beforeExternalServices();
final TransactionValidationRule validatorRule = this.getTransactionValidatorRule();
// Act - ACCESS_LIST transaction
when(transaction.getType()).thenReturn(TransactionType.ACCESS_LIST);
Optional<String> result = validatorRule.validate(transaction);
// Assert
assertThat(result).isEmpty();
}
@Test
public void shouldPermitEIP1559Transactions() {
// Arrange
plugin.doRegister(serviceManager);
plugin.beforeExternalServices();
final TransactionValidationRule validatorRule = this.getTransactionValidatorRule();
// Act - EIP1559 transaction
when(transaction.getType()).thenReturn(TransactionType.EIP1559);
Optional<String> result = validatorRule.validate(transaction);
// Assert
assertThat(result).isEmpty();
}
@Test
public void shouldPermitBlobTransactionsWhenEnabled() {
// Arrange
when(lineaTransactionValidatorConfiguration.blobTxEnabled()).thenReturn(true);
plugin.doRegister(serviceManager);
plugin.beforeExternalServices();
final TransactionValidationRule validatorRule = this.getTransactionValidatorRule();
// Act - BLOB transaction
when(transaction.getType()).thenReturn(TransactionType.BLOB);
Optional<String> result = validatorRule.validate(transaction);
// Assert
assertThat(result).isEmpty();
}
private TransactionValidationRule getTransactionValidatorRule() {
ArgumentCaptor<TransactionValidationRule> ruleCaptor =
ArgumentCaptor.forClass(TransactionValidationRule.class);
verify(transactionValidatorService).registerTransactionValidatorRule(ruleCaptor.capture());
return ruleCaptor.getValue();
}
}