diff --git a/CHANGELOG.md b/CHANGELOG.md index 31eaa4e4..1c3198f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## Next release -* feat: Report rejected transactions to an external service [#69](https://github.com/Consensys/linea-sequencer/pull/69) +* 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) ## 0.6.0-rc1.1 * bump linea-arithmetization version to 0.6.0-rc1 [#71](https://github.com/Consensys/linea-sequencer/pull/71) diff --git a/acceptance-tests/src/test/java/linea/plugin/acc/test/BlockGasLimitTest.java b/acceptance-tests/src/test/java/linea/plugin/acc/test/BlockGasLimitTest.java index b2057972..b1decd2c 100644 --- a/acceptance-tests/src/test/java/linea/plugin/acc/test/BlockGasLimitTest.java +++ b/acceptance-tests/src/test/java/linea/plugin/acc/test/BlockGasLimitTest.java @@ -41,8 +41,7 @@ public List getTestCliOptions() { @Override @BeforeEach public void setup() throws Exception { - minerNode = besu.createMinerNodeWithExtraCliOptions("miner1", getTestCliOptions()); - cluster.start(minerNode); + super.setup(); minerNode.execute(minerTransactions.minerStop()); } diff --git a/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java b/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java index 036154ef..c5d08d7a 100644 --- a/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java +++ b/acceptance-tests/src/test/java/linea/plugin/acc/test/LineaPluginTestBase.java @@ -121,7 +121,14 @@ private BesuNode createCliqueNodeWithExtraCliOptionsAndRpcApis( .jsonRpcTxPool() .genesisConfigProvider( validators -> Optional.of(provideGenesisConfig(validators, cliqueOptions))) - .extraCLIOptions(extraCliOptions); + .extraCLIOptions(extraCliOptions) + .requestedPlugins( + List.of( + "LineaExtraDataPlugin", + "LineaEstimateGasEndpointPlugin", + "LineaSetExtraDataEndpointPlugin", + "LineaTransactionPoolValidatorPlugin", + "LineaTransactionSelectorPlugin")); return besu.create(nodeConfBuilder.build()); } diff --git a/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java b/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java index cec5acbd..f611cbfe 100644 --- a/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java +++ b/acceptance-tests/src/test/java/org/hyperledger/besu/tests/acceptance/dsl/AcceptanceTestBase.java @@ -36,7 +36,6 @@ import org.hyperledger.besu.tests.acceptance.dsl.condition.login.LoginConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.net.NetConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.perm.PermissioningConditions; -import org.hyperledger.besu.tests.acceptance.dsl.condition.priv.PrivConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.process.ExitedWithCode; import org.hyperledger.besu.tests.acceptance.dsl.condition.txpool.TxPoolConditions; import org.hyperledger.besu.tests.acceptance.dsl.condition.web3.Web3Conditions; @@ -54,7 +53,6 @@ import org.hyperledger.besu.tests.acceptance.dsl.transaction.miner.MinerTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.net.NetTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.perm.PermissioningTransactions; -import org.hyperledger.besu.tests.acceptance.dsl.transaction.privacy.PrivacyTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.txpool.TxPoolTransactions; import org.hyperledger.besu.tests.acceptance.dsl.transaction.web3.Web3Transactions; import org.junit.jupiter.api.AfterEach; @@ -88,8 +86,6 @@ public class AcceptanceTestBase { protected final PermissioningTransactions permissioningTransactions; protected final MinerTransactions minerTransactions; protected final Web3Conditions web3; - protected final PrivConditions priv; - protected final PrivacyTransactions privacyTransactions; protected final TxPoolConditions txPoolConditions; protected final TxPoolTransactions txPoolTransactions; protected final ExitedWithCode exitedSuccessfully; @@ -104,7 +100,6 @@ protected AcceptanceTestBase() { bftTransactions = new BftTransactions(); accountTransactions = new AccountTransactions(accounts); permissioningTransactions = new PermissioningTransactions(); - privacyTransactions = new PrivacyTransactions(); contractTransactions = new ContractTransactions(); minerTransactions = new MinerTransactions(); @@ -116,7 +111,6 @@ protected AcceptanceTestBase() { net = new NetConditions(new NetTransactions()); cluster = new Cluster(net); perm = new PermissioningConditions(permissioningTransactions); - priv = new PrivConditions(privacyTransactions); admin = new AdminConditions(adminTransactions); web3 = new Web3Conditions(new Web3Transactions()); besu = new BesuNodeFactory(); diff --git a/gradle.properties b/gradle.properties index 7775aec8..17e58774 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ -releaseVersion=0.1.4-test34 -besuVersion=24.9-delivery32 +releaseVersion=0.1.4-test35 +besuVersion=24.10-delivery34 besuArtifactGroup=io.consensys.linea-besu distributionIdentifier=besu-sequencer-plugins -distributionBaseUrl=https://artifacts.consensys.net/public/linea-besu/raw/names/linea-besu.tar.gz/versions/ +distributionBaseUrl=https://artifacts.consensys.net/public/linea-besu/raw/names/linea-besu.tar.gz/versions/ \ No newline at end of file diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index df1b75b4..1ffa7421 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -74,6 +74,7 @@ dependencyManagement { entry "dsl" entry "eth" entry "rlp" + entry "besu" } dependencySet(group: 'ch.qos.logback', version: '1.5.6') { diff --git a/sequencer/build.gradle b/sequencer/build.gradle index 00655015..9191d84f 100644 --- a/sequencer/build.gradle +++ b/sequencer/build.gradle @@ -67,8 +67,9 @@ dependencies { testImplementation "${besuArtifactGroup}.internal:algorithms" testImplementation "${besuArtifactGroup}.internal:core" testImplementation "${besuArtifactGroup}.internal:rlp" - testImplementation "${besuArtifactGroup}.internal:core" testImplementation "${besuArtifactGroup}:plugin-api" + testImplementation "${besuArtifactGroup}.internal:besu" + testImplementation "info.picocli:picocli" testImplementation "org.awaitility:awaitility" // workaround for bug https://github.com/dnsjava/dnsjava/issues/329, remove when upgraded upstream diff --git a/sequencer/src/main/java/net/consensys/linea/AbstractLineaPrivateOptionsPlugin.java b/sequencer/src/main/java/net/consensys/linea/AbstractLineaPrivateOptionsPlugin.java index fef9621c..9f245d71 100644 --- a/sequencer/src/main/java/net/consensys/linea/AbstractLineaPrivateOptionsPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/AbstractLineaPrivateOptionsPlugin.java @@ -22,6 +22,8 @@ import net.consensys.linea.compress.LibCompress; import net.consensys.linea.config.LineaProfitabilityCliOptions; import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.config.LineaRejectedTxReportingCliOptions; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import net.consensys.linea.config.LineaRpcCliOptions; import net.consensys.linea.config.LineaRpcConfiguration; import net.consensys.linea.config.LineaTracerCliOptions; @@ -66,6 +68,9 @@ public Map getLineaPluginConfigMap() { configMap.put( LineaTracerCliOptions.CONFIG_KEY, LineaTracerCliOptions.create().asPluginConfig()); + configMap.put( + LineaRejectedTxReportingCliOptions.CONFIG_KEY, + LineaRejectedTxReportingCliOptions.create().asPluginConfig()); return configMap; } @@ -94,6 +99,11 @@ public LineaTracerConfiguration tracerConfiguration() { getConfigurationByKey(LineaTracerCliOptions.CONFIG_KEY).optionsConfig(); } + public LineaRejectedTxReportingConfiguration rejectedTxReportingConfiguration() { + return (LineaRejectedTxReportingConfiguration) + getConfigurationByKey(LineaRejectedTxReportingCliOptions.CONFIG_KEY).optionsConfig(); + } + @Override public void start() { super.start(); diff --git a/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java b/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java index c3b51351..152dd326 100644 --- a/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/AbstractLineaRequiredPlugin.java @@ -18,9 +18,11 @@ import lombok.extern.slf4j.Slf4j; import org.hyperledger.besu.plugin.BesuContext; import org.hyperledger.besu.plugin.BesuPlugin; +import org.hyperledger.besu.plugin.services.BlockchainService; @Slf4j public abstract class AbstractLineaRequiredPlugin extends AbstractLineaPrivateOptionsPlugin { + protected BlockchainService blockchainService; /** * Linea plugins extending this class will halt startup of Besu in case of exception during @@ -34,7 +36,15 @@ public abstract class AbstractLineaRequiredPlugin extends AbstractLineaPrivateOp public void register(final BesuContext context) { super.register(context); try { - log.info("Registering Linea plugin " + this.getClass().getName()); + log.info("Registering Linea plugin {}", this.getClass().getName()); + + blockchainService = + context + .getService(BlockchainService.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain BlockchainService from the BesuContext.")); doRegister(context); @@ -52,4 +62,21 @@ public void register(final BesuContext context) { * @param context */ public abstract void doRegister(final BesuContext context); + + @Override + public void beforeExternalServices() { + super.beforeExternalServices(); + + blockchainService + .getChainId() + .ifPresentOrElse( + chainId -> { + if (chainId.signum() <= 0) { + throw new IllegalArgumentException("Chain id must be greater than zero."); + } + }, + () -> { + throw new IllegalArgumentException("Chain id required"); + }); + } } diff --git a/sequencer/src/main/java/net/consensys/linea/config/LineaNodeType.java b/sequencer/src/main/java/net/consensys/linea/config/LineaNodeType.java new file mode 100644 index 00000000..5bdb40fe --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaNodeType.java @@ -0,0 +1,23 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +/** Linea node type that is used when reporting rejected transactions. */ +public enum LineaNodeType { + SEQUENCER, + RPC, + P2P +} diff --git a/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptions.java b/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptions.java new file mode 100644 index 00000000..16877f14 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptions.java @@ -0,0 +1,100 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config; + +import java.net.URL; + +import com.google.common.base.MoreObjects; +import net.consensys.linea.LineaCliOptions; +import picocli.CommandLine.Option; + +/** The Linea Rejected Transaction Reporting CLI options. */ +public class LineaRejectedTxReportingCliOptions implements LineaCliOptions { + /** + * The configuration key used in AbstractLineaPrivateOptionsPlugin to identify the cli options. + */ + public static final String CONFIG_KEY = "rejected-tx-reporting-config"; + + /** The rejected transaction endpoint. */ + public static final String REJECTED_TX_ENDPOINT = "--plugin-linea-rejected-tx-endpoint"; + + /** The Linea node type. */ + public static final String LINEA_NODE_TYPE = "--plugin-linea-node-type"; + + @Option( + names = {REJECTED_TX_ENDPOINT}, + hidden = true, + paramLabel = "", + description = + "Endpoint URI for reporting rejected transactions. Specify a valid URI to enable reporting.") + URL rejectedTxEndpoint = null; + + @Option( + names = {LINEA_NODE_TYPE}, + hidden = true, + paramLabel = "", + description = + "Linea Node type to use when reporting rejected transactions. (Valid values: ${COMPLETION-CANDIDATES})") + LineaNodeType lineaNodeType = null; + + /** Default constructor. */ + private LineaRejectedTxReportingCliOptions() {} + + /** + * Create Linea Rejected Transaction Reporting CLI options. + * + * @return the Linea Rejected Transaction Reporting CLI options + */ + public static LineaRejectedTxReportingCliOptions create() { + return new LineaRejectedTxReportingCliOptions(); + } + + /** + * Instantiates a new Linea rejected tx reporting cli options from Configuration object + * + * @param config An instance of LineaRejectedTxReportingConfiguration + */ + public static LineaRejectedTxReportingCliOptions fromConfig( + final LineaRejectedTxReportingConfiguration config) { + final LineaRejectedTxReportingCliOptions options = create(); + options.rejectedTxEndpoint = config.rejectedTxEndpoint(); + options.lineaNodeType = config.lineaNodeType(); + return options; + } + + @Override + public LineaRejectedTxReportingConfiguration toDomainObject() { + // perform validation here, if endpoint is specified then node type is required. + // We can ignore node type if endpoint is not specified. + if (rejectedTxEndpoint != null && lineaNodeType == null) { + throw new IllegalArgumentException( + "Error: Missing required argument(s): " + LINEA_NODE_TYPE + "="); + } + + return LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(rejectedTxEndpoint) + .lineaNodeType(lineaNodeType) + .build(); + } + + @Override + public String toString() { + + return MoreObjects.toStringHelper(this) + .add(REJECTED_TX_ENDPOINT, rejectedTxEndpoint) + .add(LINEA_NODE_TYPE, lineaNodeType) + .toString(); + } +} diff --git a/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingConfiguration.java b/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingConfiguration.java new file mode 100644 index 00000000..e03de424 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaRejectedTxReportingConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package net.consensys.linea.config; + +import java.net.URL; + +import lombok.Builder; +import net.consensys.linea.LineaOptionsConfiguration; + +/** Linea Rejected Transactions Reporting Configuration */ +@Builder(toBuilder = true) +public record LineaRejectedTxReportingConfiguration( + URL rejectedTxEndpoint, LineaNodeType lineaNodeType) implements LineaOptionsConfiguration {} diff --git a/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java b/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java index d962592a..20bc1803 100644 --- a/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java @@ -15,8 +15,6 @@ package net.consensys.linea.config; -import java.net.URI; - import com.google.common.base.MoreObjects; import jakarta.validation.constraints.Positive; import net.consensys.linea.LineaCliOptions; @@ -41,8 +39,6 @@ public class LineaTransactionSelectorCliOptions implements LineaCliOptions { public static final String UNPROFITABLE_RETRY_LIMIT = "--plugin-linea-unprofitable-retry-limit"; public static final int DEFAULT_UNPROFITABLE_RETRY_LIMIT = 10; - public static final String REJECTED_TX_ENDPOINT = "--plugin-linea-rejected-tx-endpoint"; - @Positive @CommandLine.Option( names = {MAX_BLOCK_CALLDATA_SIZE}, @@ -86,13 +82,6 @@ public class LineaTransactionSelectorCliOptions implements LineaCliOptions { "Max number of unprofitable transactions we retry on each block creation (default: ${DEFAULT-VALUE})") private int unprofitableRetryLimit = DEFAULT_UNPROFITABLE_RETRY_LIMIT; - @CommandLine.Option( - names = {REJECTED_TX_ENDPOINT}, - hidden = true, - paramLabel = "", - description = "Endpoint URI for reporting rejected transactions (default: ${DEFAULT-VALUE})") - private URI rejectedTxEndpoint = null; - private LineaTransactionSelectorCliOptions() {} /** @@ -118,7 +107,6 @@ public static LineaTransactionSelectorCliOptions fromConfig( options.maxGasPerBlock = config.maxGasPerBlock(); options.unprofitableCacheSize = config.unprofitableCacheSize(); options.unprofitableRetryLimit = config.unprofitableRetryLimit(); - options.rejectedTxEndpoint = config.rejectedTxEndpoint(); return options; } @@ -135,7 +123,6 @@ public LineaTransactionSelectorConfiguration toDomainObject() { .maxGasPerBlock(maxGasPerBlock) .unprofitableCacheSize(unprofitableCacheSize) .unprofitableRetryLimit(unprofitableRetryLimit) - .rejectedTxEndpoint(rejectedTxEndpoint) .build(); } @@ -147,7 +134,6 @@ public String toString() { .add(MAX_GAS_PER_BLOCK, maxGasPerBlock) .add(UNPROFITABLE_CACHE_SIZE, unprofitableCacheSize) .add(UNPROFITABLE_RETRY_LIMIT, unprofitableRetryLimit) - .add(REJECTED_TX_ENDPOINT, rejectedTxEndpoint) .toString(); } } diff --git a/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java b/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java index cf83dc67..73c35b02 100644 --- a/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java @@ -15,8 +15,6 @@ package net.consensys.linea.config; -import java.net.URI; - import lombok.Builder; import net.consensys.linea.LineaOptionsConfiguration; @@ -27,6 +25,5 @@ public record LineaTransactionSelectorConfiguration( int overLinesLimitCacheSize, long maxGasPerBlock, int unprofitableCacheSize, - int unprofitableRetryLimit, - URI rejectedTxEndpoint) + int unprofitableRetryLimit) implements LineaOptionsConfiguration {} diff --git a/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java b/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java index 7fe0aa98..ed100ec3 100644 --- a/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/extradata/LineaExtraDataPlugin.java @@ -23,7 +23,6 @@ import org.hyperledger.besu.plugin.BesuContext; import org.hyperledger.besu.plugin.BesuPlugin; import org.hyperledger.besu.plugin.services.BesuEvents; -import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.RpcEndpointService; /** This plugin registers handlers that are activated when new blocks are imported */ @@ -33,7 +32,6 @@ public class LineaExtraDataPlugin extends AbstractLineaRequiredPlugin { public static final String NAME = "linea"; private BesuContext besuContext; private RpcEndpointService rpcEndpointService; - private BlockchainService blockchainService; @Override public Optional getName() { @@ -50,13 +48,6 @@ public void doRegister(final BesuContext context) { () -> new RuntimeException( "Failed to obtain RpcEndpointService from the BesuContext.")); - blockchainService = - context - .getService(BlockchainService.class) - .orElseThrow( - () -> - new RuntimeException( - "Failed to obtain BlockchainService from the BesuContext.")); } /** diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java index c2a5b530..7110bcc9 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -17,7 +17,6 @@ import java.io.IOException; import java.io.UncheckedIOException; -import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; @@ -30,6 +29,8 @@ import java.util.Map; import java.util.TreeSet; import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -39,7 +40,10 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; +import lombok.NonNull; import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; @@ -52,26 +56,37 @@ public class JsonRpcManager { private static final Duration INITIAL_RETRY_DELAY_DURATION = Duration.ofSeconds(1); private static final Duration MAX_RETRY_DURATION = Duration.ofHours(2); private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + static final String JSON_RPC_DIR = "rej-tx-rpc"; + static final String DISCARDED_DIR = "discarded"; private final OkHttpClient client = new OkHttpClient(); private final ObjectMapper objectMapper = new ObjectMapper(); private final Map fileStartTimes = new ConcurrentHashMap<>(); - private final Path rejTxRpcDirectory; - private final URI rejectedTxEndpoint; + private final Path jsonRpcDir; + private final LineaRejectedTxReportingConfiguration reportingConfiguration; private final ExecutorService executorService; private final ScheduledExecutorService retrySchedulerService; /** * Creates a new JSON-RPC manager. * + * @param pluginIdentifier The plugin identifier will be created as a sub-directory under + * rej-tx-rpc. The rejected transactions will be stored under it for each plugin that uses it. * @param besuDataDir Path to Besu data directory. The json-rpc files will be stored here under * rej-tx-rpc subdirectory. - * @param rejectedTxEndpoint The endpoint to send rejected transactions to + * @param reportingConfiguration Instance of LineaRejectedTxReportingConfiguration containing the + * endpoint URI and node type. */ - public JsonRpcManager(final Path besuDataDir, final URI rejectedTxEndpoint) { - this.rejTxRpcDirectory = besuDataDir.resolve("rej_tx_rpc"); - this.rejectedTxEndpoint = rejectedTxEndpoint; + public JsonRpcManager( + @NonNull final String pluginIdentifier, + @NonNull final Path besuDataDir, + @NonNull final LineaRejectedTxReportingConfiguration reportingConfiguration) { + if (reportingConfiguration.rejectedTxEndpoint() == null) { + throw new IllegalStateException("Rejected transaction endpoint URI is required"); + } + this.jsonRpcDir = besuDataDir.resolve(JSON_RPC_DIR).resolve(pluginIdentifier); + this.reportingConfiguration = reportingConfiguration; this.executorService = Executors.newVirtualThreadPerTaskExecutor(); this.retrySchedulerService = Executors.newSingleThreadScheduledExecutor(); } @@ -79,14 +94,14 @@ public JsonRpcManager(final Path besuDataDir, final URI rejectedTxEndpoint) { /** Load existing JSON-RPC and submit them. */ public JsonRpcManager start() { try { - // Create the rej_tx_rpc/discarded directories if it doesn't exist - Files.createDirectories(rejTxRpcDirectory.resolve("discarded")); + // Create the rej-tx-rpc/pluginIdentifier/discarded directories if it doesn't exist + Files.createDirectories(jsonRpcDir.resolve(DISCARDED_DIR)); // Load existing JSON files processExistingJsonFiles(); return this; } catch (final IOException e) { - log.error("Failed to create or access directory: {}", rejTxRpcDirectory, e); + log.error("Failed to create or access directories under: {}", jsonRpcDir, e); throw new UncheckedIOException(e); } } @@ -102,25 +117,37 @@ public void shutdown() { * * @param jsonContent The JSON content to submit */ - public void submitNewJsonRpcCall(final String jsonContent) { - final Path jsonFile; - try { - jsonFile = saveJsonToDir(jsonContent, rejTxRpcDirectory); - } catch (final IOException e) { - log.error("Failed to save JSON-RPC content", e); - return; - } + public void submitNewJsonRpcCallAsync(final String jsonContent) { + CompletableFuture.supplyAsync( + () -> { + try { + Path jsonFile = saveJsonToDir(jsonContent, jsonRpcDir); + fileStartTimes.put(jsonFile, Instant.now()); + return jsonFile; + } catch (final IOException e) { + log.error("Failed to save JSON-RPC content", e); + throw new CompletionException(e); + } + }, + executorService) + .thenAcceptAsync( + jsonFile -> submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY_DURATION), executorService) + .exceptionally( + e -> { + log.error("Error in submitNewJsonRpcCall", e); + return null; + }); + } - fileStartTimes.put(jsonFile, Instant.now()); - submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY_DURATION); + public LineaNodeType getNodeType() { + return reportingConfiguration.lineaNodeType(); } private void processExistingJsonFiles() { try { final TreeSet sortedFiles = new TreeSet<>(Comparator.comparing(Path::getFileName)); - try (DirectoryStream stream = - Files.newDirectoryStream(rejTxRpcDirectory, "rpc_*.json")) { + try (DirectoryStream stream = Files.newDirectoryStream(jsonRpcDir, "rpc_*.json")) { for (Path path : stream) { sortedFiles.add(path); } @@ -155,7 +182,7 @@ private void submitJsonRpcCall(final Path jsonFile, final Duration nextDelay) { log.error( "Failed to send JSON-RPC file {} to {}, Scheduling retry ...", jsonFile, - rejectedTxEndpoint); + reportingConfiguration.rejectedTxEndpoint()); scheduleRetry(jsonFile, nextDelay); } } catch (final Exception e) { @@ -189,8 +216,7 @@ private void scheduleRetry(final Path jsonFile, final Duration currentDelay) { TimeUnit.MILLISECONDS); } else { log.error("Exceeded maximum retry duration for JSON-RPC file: {}.", jsonFile); - final Path destination = - rejTxRpcDirectory.resolve("discarded").resolve(jsonFile.getFileName()); + final Path destination = jsonRpcDir.resolve(DISCARDED_DIR).resolve(jsonFile.getFileName()); try { Files.move(jsonFile, destination, StandardCopyOption.REPLACE_EXISTING); @@ -209,7 +235,8 @@ private void scheduleRetry(final Path jsonFile, final Duration currentDelay) { private boolean sendJsonRpcCall(final String jsonContent) { final RequestBody body = RequestBody.create(jsonContent, JSON); final Request request = - new Request.Builder().url(rejectedTxEndpoint.toString()).post(body).build(); + new Request.Builder().url(reportingConfiguration.rejectedTxEndpoint()).post(body).build(); + try (final Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { log.error("Unexpected response code from rejected-tx endpoint: {}", response.code()); @@ -242,7 +269,10 @@ private boolean sendJsonRpcCall(final String jsonContent) { log.warn("Unexpected rejected-tx JSON-RPC response format: {}", responseBody); return false; } catch (final IOException e) { - log.error("Failed to send JSON-RPC call to rejected-tx endpoint {}", rejectedTxEndpoint, e); + log.error( + "Failed to send JSON-RPC call to rejected-tx endpoint {}", + reportingConfiguration.rejectedTxEndpoint(), + e); return false; } } diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java index 26906eea..4bd4dc69 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java @@ -16,57 +16,80 @@ package net.consensys.linea.jsonrpc; import java.time.Instant; +import java.util.List; +import java.util.Optional; import java.util.concurrent.atomic.AtomicLong; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import org.hyperledger.besu.datatypes.PendingTransaction; -import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; -import org.hyperledger.besu.plugin.data.TransactionSelectionResult; -import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.sequencer.modulelimit.ModuleLimitsValidationResult; +import org.hyperledger.besu.datatypes.Transaction; -/** Helper class to build JSON-RPC requests for rejected transactions. */ +/** + * Helper class to build JSON-RPC requests for rejected transactions. + * + *
+ * {@code linea_saveRejectedTransactionV1({
+ *         "txRejectionStage": "SEQUENCER/RPC/P2P",
+ *         "timestamp": "2024-08-22T09:18:51Z", # ISO8601 UTC+0 when tx was rejected by node, usefull if P2P edge node.
+ *         "blockNumber": "base 10 number",
+ *         "transactionRLP": "transaction as the user sent in eth_sendRawTransaction",
+ *         "reason": "Transaction line count for module ADD=402 is above the limit 70"
+ *         "overflows": [{
+ *           "module": "ADD",
+ *           "count": 402,
+ *           "limit": 70
+ *         }, {
+ *           "module": "MUL",
+ *           "count": 587,
+ *           "limit": 400
+ *         }]
+ *     })
+ * }
+ * 
+ */ public class JsonRpcRequestBuilder { private static final AtomicLong idCounter = new AtomicLong(1); /** + * Generate linea_saveRejectedTransactionV1 JSON-RPC request from given arguments. * - * - *
-   * {@code linea_saveRejectedTransactionV1({
-   *         "txRejectionStage": "SEQUENCER/RPC/P2P",
-   *         "timestamp": "2024-08-22T09:18:51Z", # ISO8601 UTC+0 when tx was rejected by node, usefull if P2P edge node.
-   *         "blockNumber": "base 10 number",
-   *         "transactionRLP": "transaction as the user sent in eth_sendRawTransaction",
-   *         "reason": "Transaction line count for module ADD=402 is above the limit 70"
-   *         "overflows": [{
-   *           "module": "ADD",
-   *           "count": 402,
-   *           "limit": 70
-   *         }, {
-   *           "module": "MUL",
-   *           "count": 587,
-   *           "limit": 400
-   *         }]
-   *     })
-   * }
-   * 
+ * @param lineaNodeType Linea node type which is reporting the rejected transaction. + * @param transaction The rejected transaction. The encoded transaction RLP is used in the + * JSON-RPC request. + * @param timestamp The timestamp when the transaction was rejected. + * @param blockNumber Optional block number where the transaction was rejected. Used for sequencer + * node. + * @param reasonMessage The reason message for the rejection. + * @return JSON-RPC request as a string. */ - public static String buildRejectedTxRequest( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResult transactionSelectionResult, - final Instant timestamp) { - final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); - final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); - - // Build JSON-RPC request + public static String generateSaveRejectedTxJsonRpc( + final LineaNodeType lineaNodeType, + final Transaction transaction, + final Instant timestamp, + final Optional blockNumber, + final String reasonMessage, + final List overflowValidationResults) { final JsonObject params = new JsonObject(); - params.addProperty("txRejectionStage", "SEQUENCER"); + params.addProperty("txRejectionStage", lineaNodeType.name()); params.addProperty("timestamp", timestamp.toString()); - params.addProperty("blockNumber", pendingBlockHeader.getNumber()); - params.addProperty( - "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); - params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); + blockNumber.ifPresent(number -> params.addProperty("blockNumber", number)); + params.addProperty("transactionRLP", transaction.encoded().toHexString()); + params.addProperty("reasonMessage", reasonMessage); + + // overflows + final JsonArray overflows = new JsonArray(); + for (ModuleLimitsValidationResult result : overflowValidationResults) { + JsonObject overflow = new JsonObject(); + overflow.addProperty("module", result.getModuleName()); + overflow.addProperty("count", result.getModuleLineCount()); + overflow.addProperty("limit", result.getModuleLineLimit()); + overflows.add(overflow); + } + params.add("overflows", overflows); + // request final JsonObject request = new JsonObject(); request.addProperty("jsonrpc", "2.0"); request.addProperty("method", "linea_saveRejectedTransactionV1"); diff --git a/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaEstimateGasEndpointPlugin.java b/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaEstimateGasEndpointPlugin.java index 0ac2f2ed..6d80e523 100644 --- a/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaEstimateGasEndpointPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/rpc/services/LineaEstimateGasEndpointPlugin.java @@ -24,7 +24,6 @@ import org.hyperledger.besu.plugin.BesuContext; import org.hyperledger.besu.plugin.BesuPlugin; import org.hyperledger.besu.plugin.services.BesuConfiguration; -import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.RpcEndpointService; import org.hyperledger.besu.plugin.services.TransactionSimulationService; @@ -35,7 +34,6 @@ public class LineaEstimateGasEndpointPlugin extends AbstractLineaRequiredPlugin private BesuConfiguration besuConfiguration; private RpcEndpointService rpcEndpointService; private TransactionSimulationService transactionSimulationService; - private BlockchainService blockchainService; private LineaEstimateGas lineaEstimateGasMethod; /** @@ -69,14 +67,6 @@ public void doRegister(final BesuContext context) { new RuntimeException( "Failed to obtain TransactionSimulatorService from the BesuContext.")); - blockchainService = - context - .getService(BlockchainService.class) - .orElseThrow( - () -> - new RuntimeException( - "Failed to obtain BlockchainService from the BesuContext.")); - lineaEstimateGasMethod = new LineaEstimateGas( besuConfiguration, transactionSimulationService, blockchainService, rpcEndpointService); diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java index 431a4f7c..8d7b88cc 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorFactory.java @@ -23,6 +23,7 @@ import net.consensys.linea.config.LineaL1L2BridgeSharedConfiguration; import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; import net.consensys.linea.sequencer.txpoolvalidation.validators.AllowedAddressValidator; import net.consensys.linea.sequencer.txpoolvalidation.validators.CalldataValidator; import net.consensys.linea.sequencer.txpoolvalidation.validators.GasLimitValidator; @@ -46,6 +47,7 @@ public class LineaTransactionPoolValidatorFactory implements PluginTransactionPo private final Set
denied; private final Map moduleLineLimitsMap; private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + private final Optional rejectedTxJsonRpcManager; public LineaTransactionPoolValidatorFactory( final BesuConfiguration besuConfiguration, @@ -55,7 +57,8 @@ public LineaTransactionPoolValidatorFactory( final LineaProfitabilityConfiguration profitabilityConf, final Set
deniedAddresses, final Map moduleLineLimitsMap, - final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration) { + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final Optional rejectedTxJsonRpcManager) { this.besuConfiguration = besuConfiguration; this.blockchainService = blockchainService; this.transactionSimulationService = transactionSimulationService; @@ -64,6 +67,7 @@ public LineaTransactionPoolValidatorFactory( this.denied = deniedAddresses; this.moduleLineLimitsMap = moduleLineLimitsMap; this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; } /** @@ -76,16 +80,18 @@ public LineaTransactionPoolValidatorFactory( public PluginTransactionPoolValidator createTransactionValidator() { final var validators = new PluginTransactionPoolValidator[] { - new AllowedAddressValidator(denied), - new GasLimitValidator(txPoolValidatorConf), - new CalldataValidator(txPoolValidatorConf), - new ProfitabilityValidator(besuConfiguration, blockchainService, profitabilityConf), + new AllowedAddressValidator(denied, rejectedTxJsonRpcManager), + new GasLimitValidator(txPoolValidatorConf, rejectedTxJsonRpcManager), + new CalldataValidator(txPoolValidatorConf, rejectedTxJsonRpcManager), + new ProfitabilityValidator( + besuConfiguration, blockchainService, profitabilityConf, rejectedTxJsonRpcManager), new SimulationValidator( blockchainService, transactionSimulationService, txPoolValidatorConf, moduleLineLimitsMap, - l1L2BridgeConfiguration) + l1L2BridgeConfiguration, + rejectedTxJsonRpcManager) }; return (transaction, isLocal, hasPriority) -> diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java index a0b28aa3..98c45653 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/LineaTransactionPoolValidatorPlugin.java @@ -28,11 +28,12 @@ import com.google.auto.service.AutoService; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.plugin.BesuContext; import org.hyperledger.besu.plugin.BesuPlugin; import org.hyperledger.besu.plugin.services.BesuConfiguration; -import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.TransactionPoolValidatorService; import org.hyperledger.besu.plugin.services.TransactionSimulationService; @@ -47,9 +48,9 @@ public class LineaTransactionPoolValidatorPlugin extends AbstractLineaRequiredPlugin { public static final String NAME = "linea"; private BesuConfiguration besuConfiguration; - private BlockchainService blockchainService; private TransactionPoolValidatorService transactionPoolValidatorService; private TransactionSimulationService transactionSimulationService; + private Optional rejectedTxJsonRpcManager = Optional.empty(); @Override public Optional getName() { @@ -66,14 +67,6 @@ public void doRegister(final BesuContext context) { new RuntimeException( "Failed to obtain BesuConfiguration from the BesuContext.")); - blockchainService = - context - .getService(BlockchainService.class) - .orElseThrow( - () -> - new RuntimeException( - "Failed to obtain BlockchainService from the BesuContext.")); - transactionPoolValidatorService = context .getService(TransactionPoolValidatorService.class) @@ -94,12 +87,26 @@ public void doRegister(final BesuContext context) { @Override public void start() { super.start(); + try (Stream lines = Files.lines( Path.of(new File(transactionPoolValidatorConfiguration().denyListPath()).toURI()))) { final Set
deniedAddresses = lines.map(l -> Address.fromHexString(l.trim())).collect(Collectors.toUnmodifiableSet()); + // start the optional json rpc manager for rejected tx reporting + final LineaRejectedTxReportingConfiguration lineaRejectedTxReportingConfiguration = + rejectedTxReportingConfiguration(); + rejectedTxJsonRpcManager = + Optional.ofNullable(lineaRejectedTxReportingConfiguration.rejectedTxEndpoint()) + .map( + endpoint -> + new JsonRpcManager( + "linea-tx-pool-validator-plugin", + besuConfiguration.getDataPath(), + lineaRejectedTxReportingConfiguration) + .start()); + transactionPoolValidatorService.registerPluginTransactionValidatorFactory( new LineaTransactionPoolValidatorFactory( besuConfiguration, @@ -109,10 +116,17 @@ public void start() { profitabilityConfiguration(), deniedAddresses, createLimitModules(tracerConfiguration()), - l1L2BridgeSharedConfiguration())); + l1L2BridgeSharedConfiguration(), + rejectedTxJsonRpcManager)); } catch (Exception e) { throw new RuntimeException(e); } } + + @Override + public void stop() { + super.stop(); + rejectedTxJsonRpcManager.ifPresent(JsonRpcManager::shutdown); + } } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidator.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidator.java index 489f9ede..a272ea7f 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidator.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidator.java @@ -14,11 +14,15 @@ */ package net.consensys.linea.sequencer.txpoolvalidation.validators; +import java.time.Instant; +import java.util.List; import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; @@ -44,15 +48,15 @@ public class AllowedAddressValidator implements PluginTransactionPoolValidator { Address.fromHexString("0x000000000000000000000000000000000000000a")); private final Set
denied; + private final Optional rejectedTxJsonRpcManager; @Override public Optional validateTransaction( final Transaction transaction, final boolean isLocal, final boolean hasPriority) { - final var maybeValidSender = validateSender(transaction); - if (maybeValidSender.isEmpty()) { - return validateRecipient(transaction); - } - return maybeValidSender; + final Optional errMsg = + validateSender(transaction).or(() -> validateRecipient(transaction)); + errMsg.ifPresent(reason -> reportRejectedTransaction(transaction, reason)); + return errMsg; } private Optional validateRecipient(final Transaction transaction) { @@ -86,4 +90,19 @@ private Optional validateSender(final Transaction transaction) { } return Optional.empty(); } + + private void reportRejectedTransaction(final Transaction transaction, final String reason) { + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + transaction, + Instant.now(), + Optional.empty(), // block number is not available + reason, + List.of()); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + }); + } } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidator.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidator.java index 44eba275..7ecaf125 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidator.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidator.java @@ -14,11 +14,15 @@ */ package net.consensys.linea.sequencer.txpoolvalidation.validators; +import java.time.Instant; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; @@ -27,6 +31,7 @@ @RequiredArgsConstructor public class CalldataValidator implements PluginTransactionPoolValidator { final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; + final Optional rejectedTxJsonRpcManager; @Override public Optional validateTransaction( @@ -36,8 +41,24 @@ public Optional validateTransaction( "Calldata of transaction is greater than the allowed max of " + txPoolValidatorConf.maxTxCalldataSize(); log.debug(errMsg); + reportRejectedTransaction(transaction, errMsg); return Optional.of(errMsg); } return Optional.empty(); } + + private void reportRejectedTransaction(final Transaction transaction, final String reason) { + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + transaction, + Instant.now(), + Optional.empty(), // block number is not available + reason, + List.of()); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + }); + } } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidator.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidator.java index f86e2844..7e2f9042 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidator.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidator.java @@ -14,11 +14,15 @@ */ package net.consensys.linea.sequencer.txpoolvalidation.validators; +import java.time.Instant; +import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.plugin.services.txvalidator.PluginTransactionPoolValidator; @@ -30,6 +34,7 @@ @RequiredArgsConstructor public class GasLimitValidator implements PluginTransactionPoolValidator { final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; + final Optional rejectedTxJsonRpcManager; @Override public Optional validateTransaction( @@ -39,8 +44,24 @@ public Optional validateTransaction( "Gas limit of transaction is greater than the allowed max of " + txPoolValidatorConf.maxTxGasLimit(); log.debug(errMsg); + reportRejectedTransaction(transaction, errMsg); return Optional.of(errMsg); } return Optional.empty(); } + + private void reportRejectedTransaction(final Transaction transaction, final String reason) { + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + transaction, + Instant.now(), + Optional.empty(), // block number is not available + reason, + List.of()); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + }); + } } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidator.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidator.java index 74040fdb..8b91452f 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidator.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidator.java @@ -14,11 +14,15 @@ */ package net.consensys.linea.sequencer.txpoolvalidation.validators; +import java.time.Instant; +import java.util.List; import java.util.Optional; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.bl.TransactionProfitabilityCalculator; import net.consensys.linea.config.LineaProfitabilityConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import org.apache.tuweni.units.bigints.UInt256s; import org.hyperledger.besu.datatypes.Transaction; import org.hyperledger.besu.datatypes.Wei; @@ -37,15 +41,18 @@ public class ProfitabilityValidator implements PluginTransactionPoolValidator { final BlockchainService blockchainService; final LineaProfitabilityConfiguration profitabilityConf; final TransactionProfitabilityCalculator profitabilityCalculator; + final Optional rejectedTxJsonRpcManager; public ProfitabilityValidator( final BesuConfiguration besuConfiguration, final BlockchainService blockchainService, - final LineaProfitabilityConfiguration profitabilityConf) { + final LineaProfitabilityConfiguration profitabilityConf, + final Optional rejectedTxJsonRpcManager) { this.besuConfiguration = besuConfiguration; this.blockchainService = blockchainService; this.profitabilityConf = profitabilityConf; this.profitabilityCalculator = new TransactionProfitabilityCalculator(profitabilityConf); + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; } @Override @@ -61,16 +68,19 @@ public Optional validateTransaction( .getNextBlockBaseFee() .orElseThrow(() -> new RuntimeException("We only support a base fee market")); - return profitabilityCalculator.isProfitable( - "Txpool", - transaction, - profitabilityConf.txPoolMinMargin(), - baseFee, - calculateUpfrontGasPrice(transaction, baseFee), - transaction.getGasLimit(), - besuConfiguration.getMinGasPrice()) - ? Optional.empty() - : Optional.of("Gas price too low"); + final Optional errMsg = + profitabilityCalculator.isProfitable( + "Txpool", + transaction, + profitabilityConf.txPoolMinMargin(), + baseFee, + calculateUpfrontGasPrice(transaction, baseFee), + transaction.getGasLimit(), + besuConfiguration.getMinGasPrice()) + ? Optional.empty() + : Optional.of("Gas price too low"); + errMsg.ifPresent(s -> reportRejectedTransaction(transaction, s)); + return errMsg; } return Optional.empty(); @@ -88,4 +98,19 @@ private Wei calculateUpfrontGasPrice(final Transaction transaction, final Wei ba baseFee.add(Wei.fromQuantity(transaction.getMaxPriorityFeePerGas().get())))) .orElseGet(() -> Wei.fromQuantity(transaction.getGasPrice().get())); } + + private void reportRejectedTransaction(final Transaction transaction, final String reason) { + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + transaction, + Instant.now(), + Optional.empty(), // block number is not available + reason, + List.of()); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + }); + } } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java index bfefbe75..df675b4e 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidator.java @@ -17,12 +17,16 @@ import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.MODULE_NOT_DEFINED; import static net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator.ModuleLineCountResult.TX_MODULE_LINE_COUNT_OVERFLOW; +import java.time.Instant; +import java.util.List; import java.util.Map; import java.util.Optional; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.config.LineaL1L2BridgeSharedConfiguration; import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import net.consensys.linea.sequencer.modulelimit.ModuleLimitsValidationResult; import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; import net.consensys.linea.zktracer.ZkTracer; @@ -44,18 +48,21 @@ public class SimulationValidator implements PluginTransactionPoolValidator { private final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf; private final Map moduleLineLimitsMap; private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; + private final Optional rejectedTxJsonRpcManager; public SimulationValidator( final BlockchainService blockchainService, final TransactionSimulationService transactionSimulationService, final LineaTransactionPoolValidatorConfiguration txPoolValidatorConf, final Map moduleLineLimitsMap, - final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration) { + final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, + final Optional rejectedTxJsonRpcManager) { this.blockchainService = blockchainService; this.transactionSimulationService = transactionSimulationService; this.txPoolValidatorConf = txPoolValidatorConf; this.moduleLineLimitsMap = moduleLineLimitsMap; this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; } @Override @@ -91,7 +98,9 @@ public Optional validateTransaction( transaction, isLocal, hasPriority, maybeSimulationResults, moduleLimitResult); if (moduleLimitResult.getResult() != ModuleLineCountValidator.ModuleLineCountResult.VALID) { - return Optional.of(handleModuleOverLimit(transaction, moduleLimitResult)); + final String reason = handleModuleOverLimit(transaction, moduleLimitResult); + reportRejectedTransaction(transaction, reason); + return Optional.of(reason); } if (maybeSimulationResults.isPresent()) { @@ -101,6 +110,7 @@ public Optional validateTransaction( "Invalid transaction" + simulationResult.getInvalidReason().map(ir -> ": " + ir).orElse(""); log.debug(errMsg); + reportRejectedTransaction(transaction, errMsg); return Optional.of(errMsg); } if (!simulationResult.isSuccessful()) { @@ -111,6 +121,7 @@ public Optional validateTransaction( .map(rr -> ": " + rr.toHexString()) .orElse(""); log.debug(errMsg); + reportRejectedTransaction(transaction, errMsg); return Optional.of(errMsg); } } @@ -127,6 +138,21 @@ public Optional validateTransaction( return Optional.empty(); } + private void reportRejectedTransaction(final Transaction transaction, final String reason) { + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + final String jsonRpcCall = + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + transaction, + Instant.now(), + Optional.empty(), // block number is not available + reason, + List.of()); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); + }); + } + private void logSimulationResult( final Transaction transaction, final boolean isLocal, diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java index 1ca96d48..a84754e7 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorPlugin.java @@ -22,12 +22,12 @@ import com.google.auto.service.AutoService; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.AbstractLineaRequiredPlugin; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; import net.consensys.linea.jsonrpc.JsonRpcManager; import org.hyperledger.besu.plugin.BesuContext; import org.hyperledger.besu.plugin.BesuPlugin; import org.hyperledger.besu.plugin.services.BesuConfiguration; -import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.TransactionSelectionService; /** @@ -40,7 +40,6 @@ public class LineaTransactionSelectorPlugin extends AbstractLineaRequiredPlugin { public static final String NAME = "linea"; private TransactionSelectionService transactionSelectionService; - private BlockchainService blockchainService; private Optional rejectedTxJsonRpcManager = Optional.empty(); private BesuConfiguration besuConfiguration; @@ -59,14 +58,6 @@ public void doRegister(final BesuContext context) { new RuntimeException( "Failed to obtain TransactionSelectionService from the BesuContext.")); - blockchainService = - context - .getService(BlockchainService.class) - .orElseThrow( - () -> - new RuntimeException( - "Failed to obtain BlockchainService from the BesuContext.")); - besuConfiguration = context .getService(BesuConfiguration.class) @@ -79,11 +70,20 @@ public void doRegister(final BesuContext context) { @Override public void start() { super.start(); + final LineaTransactionSelectorConfiguration txSelectorConfiguration = transactionSelectorConfiguration(); + final LineaRejectedTxReportingConfiguration lineaRejectedTxReportingConfiguration = + rejectedTxReportingConfiguration(); rejectedTxJsonRpcManager = - Optional.ofNullable(txSelectorConfiguration.rejectedTxEndpoint()) - .map(endpoint -> new JsonRpcManager(besuConfiguration.getDataPath(), endpoint).start()); + Optional.ofNullable(lineaRejectedTxReportingConfiguration.rejectedTxEndpoint()) + .map( + endpoint -> + new JsonRpcManager( + "linea-tx-selector-plugin", + besuConfiguration.getDataPath(), + lineaRejectedTxReportingConfiguration) + .start()); transactionSelectionService.registerPluginTransactionSelectorFactory( new LineaTransactionSelectorFactory( blockchainService, diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java index 411592b3..097ce898 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/LineaTransactionSelector.java @@ -160,9 +160,14 @@ public void onTransactionNotSelected( rejectedTxJsonRpcManager.ifPresent( jsonRpcManager -> { if (transactionSelectionResult.discard()) { - jsonRpcManager.submitNewJsonRpcCall( - JsonRpcRequestBuilder.buildRejectedTxRequest( - evaluationContext, transactionSelectionResult, Instant.now())); + jsonRpcManager.submitNewJsonRpcCallAsync( + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + jsonRpcManager.getNodeType(), + evaluationContext.getPendingTransaction().getTransaction(), + Instant.now(), + Optional.of(evaluationContext.getPendingBlockHeader().getNumber()), + transactionSelectionResult.toString(), + List.of())); } }); } diff --git a/sequencer/src/test/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptionsTest.java b/sequencer/src/test/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptionsTest.java new file mode 100644 index 00000000..12124f17 --- /dev/null +++ b/sequencer/src/test/java/net/consensys/linea/config/LineaRejectedTxReportingCliOptionsTest.java @@ -0,0 +1,129 @@ +/* + * Copyright Consensys Software Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package net.consensys.linea.config; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatNoException; + +import java.net.MalformedURLException; +import java.net.URI; + +import org.hyperledger.besu.services.PicoCLIOptionsImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; + +class LineaRejectedTxReportingCliOptionsTest { + + @Command + static final class MockLineaBesuCommand { + @Option(names = "--mock-option") + String mockOption; + } + + private MockLineaBesuCommand command; + private CommandLine commandLine; + private LineaRejectedTxReportingCliOptions txReportingCliOptions; + + @BeforeEach + public void setup() { + command = new MockLineaBesuCommand(); + commandLine = new CommandLine(command); + + // add mixin option before parseArgs is called + final PicoCLIOptionsImpl picoCliService = new PicoCLIOptionsImpl(commandLine); + txReportingCliOptions = LineaRejectedTxReportingCliOptions.create(); + picoCliService.addPicoCLIOptions("linea", txReportingCliOptions); + } + + @Test + void emptyLineaRejectedTxReportingCliOptions() { + commandLine.parseArgs("--mock-option", "mockValue"); + + assertThat(command.mockOption).isEqualTo("mockValue"); + assertThat(txReportingCliOptions.rejectedTxEndpoint).isNull(); + assertThat(txReportingCliOptions.lineaNodeType).isNull(); + } + + @ParameterizedTest + @EnumSource(LineaNodeType.class) + void lineaRejectedTxOptionBothOptionsRequired(final LineaNodeType lineaNodeType) + throws MalformedURLException { + commandLine.parseArgs( + "--plugin-linea-rejected-tx-endpoint", + "http://localhost:8080", + "--plugin-linea-node-type", + lineaNodeType.name()); + + // parse args would not throw an exception, toDomainObject will perform the validation + assertThat(txReportingCliOptions.rejectedTxEndpoint) + .isEqualTo(URI.create("http://localhost:8080").toURL()); + assertThat(txReportingCliOptions.lineaNodeType).isEqualTo(lineaNodeType); + assertThatNoException().isThrownBy(() -> txReportingCliOptions.toDomainObject()); + } + + @Test + void lineaRejectedTxReportingCliOptionsOnlyEndpointCauseException() { + commandLine.parseArgs("--plugin-linea-rejected-tx-endpoint", "http://localhost:8080"); + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> txReportingCliOptions.toDomainObject()) + .withMessageContaining( + "Error: Missing required argument(s): --plugin-linea-node-type="); + } + + @Test + void lineaRejectedTxReportingCliOptionsOnlyNodeTypeParsesWithoutProblem() { + commandLine.parseArgs("--plugin-linea-node-type", LineaNodeType.SEQUENCER.name()); + assertThatNoException().isThrownBy(() -> txReportingCliOptions.toDomainObject()); + } + + @Test + void lineaRejectedTxReportingInvalidNodeTypeCauseException() { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy( + () -> + commandLine.parseArgs( + "--plugin-linea-rejected-tx-endpoint", + "http://localhost:8080", + "--plugin-linea-node-type", + "INVALID_NODE_TYPE")) + .withMessageContaining( + "Invalid value for option '--plugin-linea-node-type': expected one of [SEQUENCER, RPC, P2P] (case-sensitive) but was 'INVALID_NODE_TYPE'"); + } + + @ParameterizedTest + @ValueSource(strings = {"", "http://localhost:8080:8080", "invalid"}) + void lineaRejectedTxReportingCliOptionsInvalidEndpointCauseException(final String endpoint) { + assertThatExceptionOfType(CommandLine.ParameterException.class) + .isThrownBy( + () -> + commandLine.parseArgs( + "--plugin-linea-rejected-tx-endpoint", + endpoint, + "--plugin-linea-node-type", + "SEQUENCER")) + .withMessageContaining( + "Invalid value for option '--plugin-linea-rejected-tx-endpoint': cannot convert '" + + endpoint + + "' to URL (java.net.MalformedURLException:"); + } +} diff --git a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java index 98c6be2a..5601d35b 100644 --- a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java @@ -30,14 +30,15 @@ import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.List; +import java.util.Optional; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import net.consensys.linea.sequencer.txselection.selectors.TestTransactionEvaluationContext; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import org.apache.tuweni.bytes.Bytes; -import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.datatypes.Transaction; -import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -53,35 +54,42 @@ public class JsonRpcManagerStartTest { @TempDir private Path tempDataDir; private JsonRpcManager jsonRpcManager; private final Bytes randomEncodedBytes = Bytes.random(32); - @Mock private PendingTransaction pendingTransaction; - @Mock private ProcessableBlockHeader pendingBlockHeader; @Mock private Transaction transaction; + static final String PLUGIN_IDENTIFIER = "linea-start-test-plugin"; @BeforeEach void init(final WireMockRuntimeInfo wmInfo) throws IOException { // create temp directories - final Path rejectedTxDir = tempDataDir.resolve("rej_tx_rpc"); - Files.createDirectories(rejectedTxDir); + final Path jsonRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); + Files.createDirectories(jsonRpcDir); // mock stubbing - when(pendingBlockHeader.getNumber()).thenReturn(1L); - when(pendingTransaction.getTransaction()).thenReturn(transaction); when(transaction.encoded()).thenReturn(randomEncodedBytes); // save rejected transaction in tempDataDir so that they are processed by the // JsonRpcManager.start for (int i = 0; i < 3; i++) { - final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test" + i); final Instant timestamp = Instant.now(); final String jsonRpcCall = - JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.toString(), + List.of()); - JsonRpcManager.saveJsonToDir(jsonRpcCall, rejectedTxDir); + JsonRpcManager.saveJsonToDir(jsonRpcCall, jsonRpcDir); } - jsonRpcManager = new JsonRpcManager(tempDataDir, URI.create(wmInfo.getHttpBaseUrl())); + final LineaRejectedTxReportingConfiguration config = + LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(URI.create(wmInfo.getHttpBaseUrl()).toURL()) + .lineaNodeType(LineaNodeType.SEQUENCER) + .build(); + jsonRpcManager = new JsonRpcManager(PLUGIN_IDENTIFIER, tempDataDir, config); } @AfterEach @@ -90,7 +98,7 @@ void cleanup() { } @Test - void existingJsonRpcFilesAreProcessedOnStart() throws InterruptedException { + void existingJsonRpcFilesAreProcessedOnStart() { stubFor( post(urlEqualTo("/")) .willReturn( diff --git a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java index d0f567c9..b73845aa 100644 --- a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java @@ -29,21 +29,23 @@ import static org.mockito.Mockito.when; import java.io.IOException; +import java.net.MalformedURLException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +import java.util.List; +import java.util.Optional; import java.util.stream.Stream; import com.github.tomakehurst.wiremock.http.Fault; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import com.github.tomakehurst.wiremock.stubbing.Scenario; -import net.consensys.linea.sequencer.txselection.selectors.TestTransactionEvaluationContext; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import org.apache.tuweni.bytes.Bytes; -import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.datatypes.Transaction; -import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -56,21 +58,22 @@ @WireMockTest @ExtendWith(MockitoExtension.class) class JsonRpcManagerTest { + static final String PLUGIN_IDENTIFIER = "linea-json-test-plugin"; @TempDir private Path tempDataDir; private JsonRpcManager jsonRpcManager; private final Bytes randomEncodedBytes = Bytes.random(32); - @Mock private PendingTransaction pendingTransaction; - @Mock private ProcessableBlockHeader pendingBlockHeader; @Mock private Transaction transaction; @BeforeEach - void init(final WireMockRuntimeInfo wmInfo) { + void init(final WireMockRuntimeInfo wmInfo) throws MalformedURLException { // mock stubbing - when(pendingBlockHeader.getNumber()).thenReturn(1L); - when(pendingTransaction.getTransaction()).thenReturn(transaction); when(transaction.encoded()).thenReturn(randomEncodedBytes); - - jsonRpcManager = new JsonRpcManager(tempDataDir, URI.create(wmInfo.getHttpBaseUrl())); + final LineaRejectedTxReportingConfiguration config = + LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(URI.create(wmInfo.getHttpBaseUrl()).toURL()) + .lineaNodeType(LineaNodeType.SEQUENCER) + .build(); + jsonRpcManager = new JsonRpcManager(PLUGIN_IDENTIFIER, tempDataDir, config); jsonRpcManager.start(); } @@ -80,7 +83,7 @@ void cleanup() { } @Test - void rejectedTxIsReported() throws InterruptedException { + void rejectedTxIsReported() { // json-rpc stubbing stubFor( post(urlEqualTo("/")) @@ -91,15 +94,20 @@ void rejectedTxIsReported() throws InterruptedException { .withBody( "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); - final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); // method under test final String jsonRpcCall = - JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); - jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); + + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); // Use Awaitility to wait for the condition to be met await() @@ -137,17 +145,21 @@ void firstCallErrorSecondCallSuccessScenario() throws InterruptedException, IOEx "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); // Prepare test data - final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); // Generate JSON-RPC call final String jsonRpcCall = - JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); // Submit the call, the scheduler will retry the failed call - jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); // Use Awaitility to wait for the condition to be met await() @@ -160,7 +172,8 @@ void firstCallErrorSecondCallSuccessScenario() throws InterruptedException, IOEx // Verify that the JSON file no longer exists in the directory (as the second call was // successful) - Path rejTxRpcDir = tempDataDir.resolve("rej_tx_rpc"); + final Path rejTxRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); try (Stream files = Files.list(rejTxRpcDir)) { long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); assertThat(fileCount).isEqualTo(0); @@ -168,7 +181,7 @@ void firstCallErrorSecondCallSuccessScenario() throws InterruptedException, IOEx } @Test - void serverRespondingWithErrorScenario() throws InterruptedException, IOException { + void serverRespondingWithErrorScenario() throws IOException { // Stub for error response stubFor( post(urlEqualTo("/")) @@ -180,17 +193,21 @@ void serverRespondingWithErrorScenario() throws InterruptedException, IOExceptio "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Internal error\"},\"id\":1}"))); // Prepare test data - final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); // Generate JSON-RPC call final String jsonRpcCall = - JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); // Submit the call - jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); // Use Awaitility to wait for the condition to be met await() @@ -202,7 +219,8 @@ void serverRespondingWithErrorScenario() throws InterruptedException, IOExceptio postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); // Verify that the JSON file still exists in the directory (as the call was unsuccessful) - final Path rejTxRpcDir = tempDataDir.resolve("rej_tx_rpc"); + final Path rejTxRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); try (Stream files = Files.list(rejTxRpcDir)) { long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); assertThat(fileCount).as("JSON file should exist as server responded with error").isOne(); @@ -242,17 +260,21 @@ void firstTwoCallsErrorThenLastCallSuccessScenario() throws InterruptedException "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); // Prepare test data - final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); // Generate JSON-RPC call final String jsonRpcCall = - JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + JsonRpcRequestBuilder.generateSaveRejectedTxJsonRpc( + LineaNodeType.SEQUENCER, + transaction, + timestamp, + Optional.of(1L), + result.maybeInvalidReason().orElse(""), + List.of()); // Submit the call, the scheduler will retry the failed calls - jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); + jsonRpcManager.submitNewJsonRpcCallAsync(jsonRpcCall); // Use Awaitility to wait for the condition to be met await() @@ -265,7 +287,8 @@ void firstTwoCallsErrorThenLastCallSuccessScenario() throws InterruptedException // Verify that the JSON file no longer exists in the directory (as the second call was // successful) - Path rejTxRpcDir = tempDataDir.resolve("rej_tx_rpc"); + final Path rejTxRpcDir = + tempDataDir.resolve(JsonRpcManager.JSON_RPC_DIR).resolve(PLUGIN_IDENTIFIER); try (Stream files = Files.list(rejTxRpcDir)) { long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); assertThat(fileCount).isEqualTo(0); diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidatorTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidatorTest.java index c2f25f68..85bc1b3e 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidatorTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/AllowedAddressValidatorTest.java @@ -40,7 +40,7 @@ public class AllowedAddressValidatorTest { @BeforeEach public void initialize() { Set
denied = Set.of(DENIED); - allowedAddressValidator = new AllowedAddressValidator(denied); + allowedAddressValidator = new AllowedAddressValidator(denied, Optional.empty()); } @Test diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidatorTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidatorTest.java index fc775580..e30a6d5f 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidatorTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/CalldataValidatorTest.java @@ -38,7 +38,8 @@ public void initialize() { new CalldataValidator( LineaTransactionPoolValidatorCliOptions.create().toDomainObject().toBuilder() .maxTxCalldataSize(MAX_TX_CALLDATA_SIZE) - .build()); + .build(), + Optional.empty()); } @Test diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidatorTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidatorTest.java index 219e62f4..e7c79907 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidatorTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/GasLimitValidatorTest.java @@ -38,7 +38,8 @@ public void initialize() { new GasLimitValidator( LineaTransactionPoolValidatorCliOptions.create().toDomainObject().toBuilder() .maxTxGasLimit(MAX_TX_GAS_LIMIT) - .build()); + .build(), + Optional.empty()); } @Test diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java index 3178a5ec..de333edb 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/ProfitabilityValidatorTest.java @@ -87,7 +87,8 @@ public void initialize() { profitabilityConfBuilder .txPoolCheckP2pEnabled(true) .txPoolCheckApiEnabled(true) - .build()); + .build(), + Optional.empty()); profitabilityValidatorNever = new ProfitabilityValidator( @@ -96,7 +97,8 @@ public void initialize() { profitabilityConfBuilder .txPoolCheckP2pEnabled(false) .txPoolCheckApiEnabled(false) - .build()); + .build(), + Optional.empty()); profitabilityValidatorOnlyApi = new ProfitabilityValidator( @@ -105,7 +107,8 @@ public void initialize() { profitabilityConfBuilder .txPoolCheckP2pEnabled(false) .txPoolCheckApiEnabled(true) - .build()); + .build(), + Optional.empty()); profitabilityValidatorOnlyP2p = new ProfitabilityValidator( @@ -114,7 +117,8 @@ public void initialize() { profitabilityConfBuilder .txPoolCheckP2pEnabled(true) .txPoolCheckApiEnabled(false) - .build()); + .build(), + Optional.empty()); } @Test diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java index 4ac156e0..90588557 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txpoolvalidation/validators/SimulationValidatorTest.java @@ -15,23 +15,41 @@ package net.consensys.linea.sequencer.txpoolvalidation.validators; +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +import static com.github.tomakehurst.wiremock.client.WireMock.matchingJsonPath; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; +import static com.github.tomakehurst.wiremock.client.WireMock.verify; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.io.IOException; import java.math.BigInteger; +import java.net.MalformedURLException; +import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.config.LineaL1L2BridgeSharedConfiguration; +import net.consensys.linea.config.LineaNodeType; +import net.consensys.linea.config.LineaRejectedTxReportingConfiguration; import net.consensys.linea.config.LineaTracerConfiguration; import net.consensys.linea.config.LineaTransactionPoolValidatorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; import net.consensys.linea.sequencer.modulelimit.ModuleLineCountValidator; import net.consensys.linea.sequencer.txselection.selectors.TraceLineLimitTransactionSelectorTest; import org.apache.tuweni.bytes.Bytes; @@ -44,6 +62,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.TransactionSimulationService; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,6 +73,7 @@ @Slf4j @RequiredArgsConstructor +@WireMockTest @ExtendWith(MockitoExtension.class) public class SimulationValidatorTest { private static final String MODULE_LINE_LIMITS_RESOURCE_NAME = "/sequencer/line-limits.toml"; @@ -87,7 +107,8 @@ public class SimulationValidatorTest { @Mock BlockchainService blockchainService; @Mock TransactionSimulationService transactionSimulationService; - + private JsonRpcManager jsonRpcManager; + @TempDir private Path tempDataDir; @TempDir static Path tempDir; static Path lineLimitsConfPath; @@ -101,7 +122,7 @@ public static void beforeAll() throws IOException { } @BeforeEach - public void initialize() { + public void initialize(final WireMockRuntimeInfo wmInfo) throws MalformedURLException { final var tracerConf = LineaTracerConfiguration.builder() .moduleLimitsFilePath(lineLimitsConfPath.toString()) @@ -110,6 +131,29 @@ public void initialize() { final var blockHeader = mock(BlockHeader.class); when(blockHeader.getBaseFee()).thenReturn(Optional.of(BASE_FEE)); when(blockchainService.getChainHeadHeader()).thenReturn(blockHeader); + + final var rejectedTxReportingConf = + LineaRejectedTxReportingConfiguration.builder() + .rejectedTxEndpoint(URI.create(wmInfo.getHttpBaseUrl()).toURL()) + .lineaNodeType(LineaNodeType.P2P) + .build(); + jsonRpcManager = + new JsonRpcManager("simulation-test", tempDataDir, rejectedTxReportingConf).start(); + + // rejected tx json-rpc stubbing + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + } + + @AfterEach + void cleanup() { + jsonRpcManager.shutdown(); } private SimulationValidator createSimulationValidator( @@ -127,7 +171,8 @@ private SimulationValidator createSimulationValidator( LineaL1L2BridgeSharedConfiguration.builder() .contract(BRIDGE_CONTRACT) .topic(BRIDGE_LOG_TOPIC) - .build()); + .build(), + Optional.of(jsonRpcManager)); } @Test @@ -147,7 +192,7 @@ public void successfulTransactionIsValid() { } @Test - public void moduleLineCountOverflowTransactionIsInvalid() { + public void moduleLineCountOverflowTransactionIsInvalidAndReported() { lineCountLimits.put("ADD", 1); final var simulationValidator = createSimulationValidator(lineCountLimits, true, false); final org.hyperledger.besu.ethereum.core.Transaction transaction = @@ -160,8 +205,25 @@ public void moduleLineCountOverflowTransactionIsInvalid() { .value(Wei.ONE) .signature(FAKE_SIGNATURE) .build(); + final var expectedReasonMessage = + "Transaction 0xbf668c5dc926c008d5b34f347e1842b94911b46f4a36b668812f821e20303322 line count for module ADD=2 is above the limit 1"; assertThat(simulationValidator.validateTransaction(transaction, true, false)) - .contains( - "Transaction 0xbf668c5dc926c008d5b34f347e1842b94911b46f4a36b668812f821e20303322 line count for module ADD=2 is above the limit 1"); + .contains(expectedReasonMessage); + + // assert that wiremock received 1 post request for rejected tx. + // Use Awaitility to wait for the condition to be met + await() + .atMost(6, SECONDS) + .untilAsserted( + () -> + verify( + exactly(1), + postRequestedFor(urlEqualTo("/")) + .withRequestBody( + matchingJsonPath( + "$.params.txRejectionStage", equalTo(LineaNodeType.P2P.name()))) + .withRequestBody( + matchingJsonPath( + "$.params.reasonMessage", equalTo(expectedReasonMessage))))); } }