mirror of
https://github.com/vacp2p/linea-monorepo.git
synced 2026-01-09 04:08:01 -05:00
[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:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user