From d8fa72a6e47e66de7156943d8db9196e38855c84 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 20 Aug 2024 10:01:30 +1000 Subject: [PATCH 01/26] feat: Report discarded transactions to external endpoint --- .../LineaTransactionSelectorPlugin.java | 4 ++-- .../selectors/LineaTransactionSelector.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) 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 3e938a5a..000b0671 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 @@ -64,8 +64,8 @@ public void doRegister(final BesuContext context) { } @Override - public void beforeExternalServices() { - super.beforeExternalServices(); + public void start() { + super.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 ea9e15f3..9714d234 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 @@ -149,6 +149,26 @@ public void onTransactionNotSelected( selectors.forEach( selector -> selector.onTransactionNotSelected(evaluationContext, transactionSelectionResult)); + + notifyDiscardedTransaction(evaluationContext, transactionSelectionResult); + } + + private void notifyDiscardedTransaction( + TransactionEvaluationContext evaluationContext, + TransactionSelectionResult transactionSelectionResult) { + if (transactionSelectionResult.discard()) { + log.debug( + "Discarding transaction {} because of {}", + evaluationContext.getPendingTransaction().getTransaction().getHash(), + transactionSelectionResult); + // Once Besu + // https://github.com/hyperledger/besu/commit/19e1a9aaf6f00eb79b70eff13e2d33963f377cf0 is + // released, + // we can use the following line + // evaluationContext.getPendingBlockHeader(); + + // TODO: Submit the details to provided endpoint API + } } /** From 68fb015276abeee18a7c3ca0fd275c14793c1f83 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 22 Aug 2024 09:54:36 +1000 Subject: [PATCH 02/26] Use PendingBlockHeader --- .../selectors/LineaTransactionSelector.java | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) 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 9714d234..b3ed0c37 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 @@ -23,6 +23,7 @@ import net.consensys.linea.config.LineaTransactionSelectorConfiguration; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import org.hyperledger.besu.datatypes.PendingTransaction; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.data.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; import org.hyperledger.besu.plugin.services.BlockchainService; @@ -157,17 +158,32 @@ private void notifyDiscardedTransaction( TransactionEvaluationContext evaluationContext, TransactionSelectionResult transactionSelectionResult) { if (transactionSelectionResult.discard()) { + final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); + final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); + log.debug( - "Discarding transaction {} because of {}", - evaluationContext.getPendingTransaction().getTransaction().getHash(), - transactionSelectionResult); - // Once Besu - // https://github.com/hyperledger/besu/commit/19e1a9aaf6f00eb79b70eff13e2d33963f377cf0 is - // released, - // we can use the following line - // evaluationContext.getPendingBlockHeader(); + "Discarding transaction {} because of {}. Block number: {}", + pendingTransaction.getTransaction().getHash(), + transactionSelectionResult, pendingBlockHeader.getNumber()); + // TODO: Submit the details to provided endpoint API + /* + linea_saveRejectedTransaction({ + "blockNumber": "base 10 number", + "transactionRLP": "transaction as the user sent in eth_sendRawTransaction", + "reasonMessage": "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 + }] + }) + */ } } From da8a2cfb8f7b37fdb4f040164fc8ad2864148ef9 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 22 Aug 2024 09:55:36 +1000 Subject: [PATCH 03/26] simplify discard condition --- .../selectors/LineaTransactionSelector.java | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) 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 b3ed0c37..508e2df7 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 @@ -157,34 +157,37 @@ public void onTransactionNotSelected( private void notifyDiscardedTransaction( TransactionEvaluationContext evaluationContext, TransactionSelectionResult transactionSelectionResult) { - if (transactionSelectionResult.discard()) { - final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); - final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); - - log.debug( - "Discarding transaction {} because of {}. Block number: {}", - pendingTransaction.getTransaction().getHash(), - transactionSelectionResult, pendingBlockHeader.getNumber()); - - - // TODO: Submit the details to provided endpoint API - /* - linea_saveRejectedTransaction({ - "blockNumber": "base 10 number", - "transactionRLP": "transaction as the user sent in eth_sendRawTransaction", - "reasonMessage": "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 - }] - }) - */ + if (!transactionSelectionResult.discard()) { + return; } + + final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); + final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); + + log.debug( + "Discarding transaction {} because of {}. Block number: {}", + pendingTransaction.getTransaction().getHash(), + transactionSelectionResult, + pendingBlockHeader.getNumber()); + + // TODO: Submit the details to provided endpoint API + /* + linea_saveRejectedTransaction({ + "blockNumber": "base 10 number", + "transactionRLP": "transaction as the user sent in eth_sendRawTransaction", + "reasonMessage": "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 + }] + }) + */ + } /** From 69710a02e4ecde3b05a5b4163c143c43e1865f79 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 27 Aug 2024 20:43:36 +1000 Subject: [PATCH 04/26] feat: Make json-rpc call for discarded tx --- .../linea/jsonrpc/JsonRpcClient.java | 65 +++++++++++++++++++ .../linea/jsonrpc/JsonRpcRequestBuilder.java | 14 ++++ .../selectors/LineaTransactionSelector.java | 17 ++++- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java create mode 100644 sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java new file mode 100644 index 00000000..f4c89701 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java @@ -0,0 +1,65 @@ +package net.consensys.linea.jsonrpc; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Scanner; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +public class JsonRpcClient { + private static final int MAX_RETRIES = 3; + private static final ExecutorService executorService = Executors.newCachedThreadPool(); + + public static String sendRequest(String urlString, String jsonInputString) throws Exception { + HttpURLConnection conn = getHttpURLConnection(urlString, jsonInputString); + + int responseCode = conn.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_OK) { + try (Scanner scanner = new Scanner(conn.getInputStream(), StandardCharsets.UTF_8)) { + return scanner.useDelimiter("\\A").next(); + } + } else { + throw new RuntimeException("Failed : HTTP error code : " + responseCode); + } + } + + private static HttpURLConnection getHttpURLConnection(String urlString, String jsonInputString) + throws IOException { + URL url = new URL(urlString); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/json; utf-8"); + conn.setRequestProperty("Accept", "application/json"); + conn.setDoOutput(true); + + try (OutputStream os = conn.getOutputStream()) { + byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); + os.write(input, 0, input.length); + } + return conn; + } + + public static Future sendRequestWithRetries(String urlString, String jsonInputString) { + Callable task = + () -> { + int attempt = 0; + while (attempt < MAX_RETRIES) { + try { + return sendRequest(urlString, jsonInputString); + } catch (Exception e) { + attempt++; + if (attempt >= MAX_RETRIES) { + throw e; + } + } + } + return null; + }; + return executorService.submit(task); + } +} diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java new file mode 100644 index 00000000..5b345241 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java @@ -0,0 +1,14 @@ +package net.consensys.linea.jsonrpc; + +import com.google.gson.JsonObject; + +public class JsonRpcRequestBuilder { + public static String buildRequest(final String method, final JsonObject params, int id) { + JsonObject request = new JsonObject(); + request.addProperty("jsonrpc", "2.0"); + request.addProperty("method", method); + request.add("params", params); + request.addProperty("id", id); + return request.toString(); + } +} 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 508e2df7..5ba3208d 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 @@ -17,10 +17,13 @@ import java.util.List; import java.util.Map; +import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTracerConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcClient; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; @@ -170,7 +173,6 @@ private void notifyDiscardedTransaction( transactionSelectionResult, pendingBlockHeader.getNumber()); - // TODO: Submit the details to provided endpoint API /* linea_saveRejectedTransaction({ "blockNumber": "base 10 number", @@ -187,7 +189,18 @@ private void notifyDiscardedTransaction( }] }) */ - + // Build JSON-RPC request + JsonObject params = new JsonObject(); + params.addProperty("blockNumber", pendingBlockHeader.getNumber()); + params.addProperty( + "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); + params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); + + String jsonRequest = + JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransaction", params, 1); + + // Send JSON-RPC request with retries in a new thread + JsonRpcClient.sendRequestWithRetries("http://your-json-rpc-endpoint", jsonRequest); } /** From 957b3dab8bf87d298c6895ef9e347270b9ee32ea Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 27 Aug 2024 21:04:45 +1000 Subject: [PATCH 05/26] cli option for reporting rejected tx endpoint --- .../LineaTransactionSelectorCliOptions.java | 14 +++++++++++ ...LineaTransactionSelectorConfiguration.java | 5 +++- .../linea/jsonrpc/JsonRpcClient.java | 23 +++++++++++-------- .../selectors/LineaTransactionSelector.java | 22 +++++++++++------- 4 files changed, 45 insertions(+), 19 deletions(-) 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 e5b20e30..3eadcaa3 100644 --- a/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorCliOptions.java @@ -15,6 +15,8 @@ package net.consensys.linea.config; +import java.net.URI; + import com.google.common.base.MoreObjects; import jakarta.validation.constraints.Positive; import net.consensys.linea.plugins.LineaCliOptions; @@ -39,6 +41,8 @@ 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}, @@ -82,6 +86,13 @@ 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() {} /** @@ -107,6 +118,7 @@ public static LineaTransactionSelectorCliOptions fromConfig( options.maxGasPerBlock = config.maxGasPerBlock(); options.unprofitableCacheSize = config.unprofitableCacheSize(); options.unprofitableRetryLimit = config.unprofitableRetryLimit(); + options.rejectedTxEndpoint = config.rejectedTxEndpoint(); return options; } @@ -123,6 +135,7 @@ public LineaTransactionSelectorConfiguration toDomainObject() { .maxGasPerBlock(maxGasPerBlock) .unprofitableCacheSize(unprofitableCacheSize) .unprofitableRetryLimit(unprofitableRetryLimit) + .rejectedTxEndpoint(rejectedTxEndpoint) .build(); } @@ -134,6 +147,7 @@ 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 6c178797..726c8932 100644 --- a/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java +++ b/sequencer/src/main/java/net/consensys/linea/config/LineaTransactionSelectorConfiguration.java @@ -15,6 +15,8 @@ package net.consensys.linea.config; +import java.net.URI; + import lombok.Builder; import net.consensys.linea.plugins.LineaOptionsConfiguration; @@ -25,5 +27,6 @@ public record LineaTransactionSelectorConfiguration( int overLinesLimitCacheSize, long maxGasPerBlock, int unprofitableCacheSize, - int unprofitableRetryLimit) + int unprofitableRetryLimit, + URI rejectedTxEndpoint) implements LineaOptionsConfiguration {} diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java index f4c89701..64c6cbb1 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; +import java.net.URI; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Scanner; @@ -15,8 +16,9 @@ public class JsonRpcClient { private static final int MAX_RETRIES = 3; private static final ExecutorService executorService = Executors.newCachedThreadPool(); - public static String sendRequest(String urlString, String jsonInputString) throws Exception { - HttpURLConnection conn = getHttpURLConnection(urlString, jsonInputString); + public static String sendRequest(final URI endpoint, final String jsonInputString) + throws Exception { + HttpURLConnection conn = getHttpURLConnection(endpoint, jsonInputString); int responseCode = conn.getResponseCode(); if (responseCode == HttpURLConnection.HTTP_OK) { @@ -28,29 +30,30 @@ public static String sendRequest(String urlString, String jsonInputString) throw } } - private static HttpURLConnection getHttpURLConnection(String urlString, String jsonInputString) - throws IOException { - URL url = new URL(urlString); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + private static HttpURLConnection getHttpURLConnection( + final URI endpoint, final String jsonInputString) throws IOException { + final URL url = endpoint.toURL(); + final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json; utf-8"); conn.setRequestProperty("Accept", "application/json"); conn.setDoOutput(true); - try (OutputStream os = conn.getOutputStream()) { - byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); + try (final OutputStream os = conn.getOutputStream()) { + final byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); os.write(input, 0, input.length); } return conn; } - public static Future sendRequestWithRetries(String urlString, String jsonInputString) { + public static Future sendRequestWithRetries( + final URI endpoint, final String jsonInputString) { Callable task = () -> { int attempt = 0; while (attempt < MAX_RETRIES) { try { - return sendRequest(urlString, jsonInputString); + return sendRequest(endpoint, jsonInputString); } catch (Exception e) { attempt++; if (attempt >= MAX_RETRIES) { 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 5ba3208d..a6d8c17f 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 @@ -14,8 +14,10 @@ */ package net.consensys.linea.sequencer.txselection.selectors; +import java.net.URI; import java.util.List; import java.util.Map; +import java.util.Optional; import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; @@ -40,6 +42,7 @@ public class LineaTransactionSelector implements PluginTransactionSelector { private TraceLineLimitTransactionSelector traceLineLimitTransactionSelector; private final List selectors; + private final Optional rejectedTxEndpoint; public LineaTransactionSelector( final BlockchainService blockchainService, @@ -48,7 +51,8 @@ public LineaTransactionSelector( final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, final Map limitsMap) { - this.selectors = + rejectedTxEndpoint = Optional.ofNullable(txSelectorConfiguration.rejectedTxEndpoint()); + selectors = createTransactionSelectors( blockchainService, txSelectorConfiguration, @@ -154,12 +158,14 @@ public void onTransactionNotSelected( selector -> selector.onTransactionNotSelected(evaluationContext, transactionSelectionResult)); - notifyDiscardedTransaction(evaluationContext, transactionSelectionResult); + rejectedTxEndpoint.ifPresent( + uri -> notifyDiscardedTransaction(evaluationContext, transactionSelectionResult, uri)); } - private void notifyDiscardedTransaction( - TransactionEvaluationContext evaluationContext, - TransactionSelectionResult transactionSelectionResult) { + private static void notifyDiscardedTransaction( + final TransactionEvaluationContext evaluationContext, + final TransactionSelectionResult transactionSelectionResult, + final URI rejectedTxEndpoint) { if (!transactionSelectionResult.discard()) { return; } @@ -190,17 +196,17 @@ private void notifyDiscardedTransaction( }) */ // Build JSON-RPC request - JsonObject params = new JsonObject(); + final JsonObject params = new JsonObject(); params.addProperty("blockNumber", pendingBlockHeader.getNumber()); params.addProperty( "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); - String jsonRequest = + final String jsonRequest = JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransaction", params, 1); // Send JSON-RPC request with retries in a new thread - JsonRpcClient.sendRequestWithRetries("http://your-json-rpc-endpoint", jsonRequest); + JsonRpcClient.sendRequestWithRetries(rejectedTxEndpoint, jsonRequest); } /** From d38b539cfb133d18061a54b024f20f503cee075f Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 28 Aug 2024 08:52:13 +1000 Subject: [PATCH 06/26] add spdx header --- .../consensys/linea/jsonrpc/JsonRpcClient.java | 15 +++++++++++++++ .../linea/jsonrpc/JsonRpcRequestBuilder.java | 15 +++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java index 64c6cbb1..ef8f2c05 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java @@ -1,3 +1,18 @@ +/* + * 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.jsonrpc; import java.io.IOException; 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 5b345241..f047c701 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java @@ -1,3 +1,18 @@ +/* + * 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.jsonrpc; import com.google.gson.JsonObject; From 7a8d380df50ebf82ca5d471761be87bf4f480ae3 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Mon, 2 Sep 2024 10:40:56 +1000 Subject: [PATCH 07/26] build: Add wiremock dependency --- gradle/common-dependencies.gradle | 2 ++ gradle/dependency-management.gradle | 2 ++ 2 files changed, 4 insertions(+) diff --git a/gradle/common-dependencies.gradle b/gradle/common-dependencies.gradle index 3289c148..03663674 100644 --- a/gradle/common-dependencies.gradle +++ b/gradle/common-dependencies.gradle @@ -34,4 +34,6 @@ dependencies { testImplementation 'org.mockito:mockito-core' testImplementation 'org.mockito:mockito-junit-jupiter' + + testImplementation "org.wiremock:wiremock" } diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index b8104a56..f4401380 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -157,5 +157,7 @@ dependencyManagement { entry 'core' entry 'crypto' } + + dependency "org.wiremock:wiremock:3.9.1" } } From 425f05cb9271488bc46b05efdc15a7956eb20d30 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 4 Sep 2024 17:46:47 +1000 Subject: [PATCH 08/26] Add unit test --- sequencer/build.gradle | 1 + .../selectors/LineaTransactionSelector.java | 53 +----------- .../selectors/ReportRejectedTransaction.java | 73 ++++++++++++++++ .../selectors/ReportDroppedTxTest.java | 83 +++++++++++++++++++ .../TestTransactionEvaluationContext.java | 13 +++ 5 files changed, 172 insertions(+), 51 deletions(-) create mode 100644 sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ReportRejectedTransaction.java create mode 100644 sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ReportDroppedTxTest.java diff --git a/sequencer/build.gradle b/sequencer/build.gradle index 5a536fa9..b503fe05 100644 --- a/sequencer/build.gradle +++ b/sequencer/build.gradle @@ -68,6 +68,7 @@ dependencies { testImplementation "${besuArtifactGroup}.internal:core" testImplementation "${besuArtifactGroup}.internal:rlp" testImplementation "${besuArtifactGroup}.internal:core" + testImplementation "${besuArtifactGroup}:plugin-api" // workaround for bug https://github.com/dnsjava/dnsjava/issues/329, remove when upgraded upstream testImplementation 'dnsjava:dnsjava:3.6.1' 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 a6d8c17f..8318054e 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 @@ -14,21 +14,19 @@ */ package net.consensys.linea.sequencer.txselection.selectors; +import static net.consensys.linea.sequencer.txselection.selectors.ReportRejectedTransaction.notifyDiscardedTransaction; + import java.net.URI; import java.util.List; import java.util.Map; import java.util.Optional; -import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTracerConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; -import net.consensys.linea.jsonrpc.JsonRpcClient; -import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import org.hyperledger.besu.datatypes.PendingTransaction; -import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.data.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; import org.hyperledger.besu.plugin.services.BlockchainService; @@ -162,53 +160,6 @@ public void onTransactionNotSelected( uri -> notifyDiscardedTransaction(evaluationContext, transactionSelectionResult, uri)); } - private static void notifyDiscardedTransaction( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResult transactionSelectionResult, - final URI rejectedTxEndpoint) { - if (!transactionSelectionResult.discard()) { - return; - } - - final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); - final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); - - log.debug( - "Discarding transaction {} because of {}. Block number: {}", - pendingTransaction.getTransaction().getHash(), - transactionSelectionResult, - pendingBlockHeader.getNumber()); - - /* - linea_saveRejectedTransaction({ - "blockNumber": "base 10 number", - "transactionRLP": "transaction as the user sent in eth_sendRawTransaction", - "reasonMessage": "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 - }] - }) - */ - // Build JSON-RPC request - final JsonObject params = new JsonObject(); - params.addProperty("blockNumber", pendingBlockHeader.getNumber()); - params.addProperty( - "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); - params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); - - final String jsonRequest = - JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransaction", params, 1); - - // Send JSON-RPC request with retries in a new thread - JsonRpcClient.sendRequestWithRetries(rejectedTxEndpoint, jsonRequest); - } - /** * Returns the operation tracer to be used while processing the transactions for the block. * diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ReportRejectedTransaction.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ReportRejectedTransaction.java new file mode 100644 index 00000000..84c8af23 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ReportRejectedTransaction.java @@ -0,0 +1,73 @@ +/* + * 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.sequencer.txselection.selectors; + +import java.net.URI; + +import com.google.gson.JsonObject; +import net.consensys.linea.jsonrpc.JsonRpcClient; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; +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; + +/** + * + * + *
+ * {@code linea_saveRejectedTransaction({
+ *         "blockNumber": "base 10 number",
+ *         "transactionRLP": "transaction as the user sent in eth_sendRawTransaction",
+ *         "reasonMessage": "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 ReportRejectedTransaction { + static void notifyDiscardedTransaction( + final TransactionEvaluationContext evaluationContext, + final TransactionSelectionResult transactionSelectionResult, + final URI rejectedTxEndpoint) { + if (!transactionSelectionResult.discard()) { + return; + } + + final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); + final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); + + // Build JSON-RPC request + final JsonObject params = new JsonObject(); + params.addProperty("blockNumber", pendingBlockHeader.getNumber()); + params.addProperty( + "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); + params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); + + final String jsonRequest = + JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransaction", params, 1); + + // Send JSON-RPC request with retries in a new thread + JsonRpcClient.sendRequestWithRetries(rejectedTxEndpoint, jsonRequest); + } +} diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ReportDroppedTxTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ReportDroppedTxTest.java new file mode 100644 index 00000000..08906a81 --- /dev/null +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ReportDroppedTxTest.java @@ -0,0 +1,83 @@ +/* + * 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.sequencer.txselection.selectors; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +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 org.mockito.Mockito.when; + +import java.net.URI; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +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.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@WireMockTest +@ExtendWith(MockitoExtension.class) +class ReportDroppedTxTest { + @Mock PendingTransaction pendingTransaction; + @Mock ProcessableBlockHeader pendingBlockHeader; + @Mock Transaction transaction; + + @Test + void droppedTxIsReported(final WireMockRuntimeInfo wmInfo) throws Exception { + when(pendingBlockHeader.getNumber()).thenReturn(1L); + when(pendingTransaction.getTransaction()).thenReturn(transaction); + Bytes randomEncodedBytes = Bytes.random(32); + when(transaction.encoded()).thenReturn(randomEncodedBytes); + + // json-rpc stubbing + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"jsonrpc\":\"2.0\",\"result\":true,\"id\":1}"))); + + TestTransactionEvaluationContext context = + new TestTransactionEvaluationContext(pendingTransaction) + .setPendingBlockHeader(pendingBlockHeader); + TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + + ReportRejectedTransaction.notifyDiscardedTransaction( + context, result, URI.create(wmInfo.getHttpBaseUrl())); + + // assert wiremock was called + Thread.sleep(1000); + + verify( + postRequestedFor(urlEqualTo("/")) + .withRequestBody( + equalToJson( + "{\"jsonrpc\":\"2.0\",\"method\":\"linea_saveRejectedTransaction\",\"params\":{\"blockNumber\":1,\"transactionRLP\":\"" + + randomEncodedBytes.toHexString() + + "\",\"reasonMessage\":\"test\"},\"id\":1}"))); + } +} diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java index 5ee9700f..4edce437 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java @@ -17,12 +17,14 @@ import com.google.common.base.Stopwatch; import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; class TestTransactionEvaluationContext implements TransactionEvaluationContext { private PendingTransaction pendingTransaction; private Wei transactionGasPrice; private Wei minGasPrice; + private ProcessableBlockHeader pendingBlockHeader; public TestTransactionEvaluationContext( final PendingTransaction pendingTransaction, @@ -37,6 +39,11 @@ public TestTransactionEvaluationContext(final PendingTransaction pendingTransact this(pendingTransaction, Wei.ONE, Wei.ONE); } + @Override + public ProcessableBlockHeader getPendingBlockHeader() { + return pendingBlockHeader; + } + @Override public PendingTransaction getPendingTransaction() { return pendingTransaction; @@ -72,4 +79,10 @@ public TestTransactionEvaluationContext setTransactionGasPrice(final Wei transac this.transactionGasPrice = transactionGasPrice; return this; } + + public TestTransactionEvaluationContext setPendingBlockHeader( + final ProcessableBlockHeader pendingBlockHeader) { + this.pendingBlockHeader = pendingBlockHeader; + return this; + } } From 0ce850dcbdcfee5f99fc0f97fec42c73e05a7dd8 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 5 Sep 2024 09:05:26 +1000 Subject: [PATCH 09/26] Update json-rpc method and unit tests --- .../selectors/LineaTransactionSelector.java | 7 ++- ....java => RejectedTransactionNotifier.java} | 17 +++++--- ...a => RejectedTransactionNotifierTest.java} | 43 ++++++++++++------- 3 files changed, 45 insertions(+), 22 deletions(-) rename sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/{ReportRejectedTransaction.java => RejectedTransactionNotifier.java} (81%) rename sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/{ReportDroppedTxTest.java => RejectedTransactionNotifierTest.java} (65%) 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 8318054e..d7bb598c 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 @@ -14,9 +14,10 @@ */ package net.consensys.linea.sequencer.txselection.selectors; -import static net.consensys.linea.sequencer.txselection.selectors.ReportRejectedTransaction.notifyDiscardedTransaction; +import static net.consensys.linea.sequencer.txselection.selectors.RejectedTransactionNotifier.notifyDiscardedTransactionAsync; import java.net.URI; +import java.time.Instant; import java.util.List; import java.util.Map; import java.util.Optional; @@ -157,7 +158,9 @@ public void onTransactionNotSelected( selector.onTransactionNotSelected(evaluationContext, transactionSelectionResult)); rejectedTxEndpoint.ifPresent( - uri -> notifyDiscardedTransaction(evaluationContext, transactionSelectionResult, uri)); + uri -> + notifyDiscardedTransactionAsync( + evaluationContext, transactionSelectionResult, Instant.now(), uri)); } /** diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ReportRejectedTransaction.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java similarity index 81% rename from sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ReportRejectedTransaction.java rename to sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java index 84c8af23..c5403c14 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/ReportRejectedTransaction.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java @@ -15,6 +15,7 @@ package net.consensys.linea.sequencer.txselection.selectors; import java.net.URI; +import java.time.Instant; import com.google.gson.JsonObject; import net.consensys.linea.jsonrpc.JsonRpcClient; @@ -28,10 +29,12 @@ * * *
- * {@code linea_saveRejectedTransaction({
+ * {@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",
- *         "reasonMessage": "Transaction line count for module ADD=402 is above the limit 70"
+ *         "reason": "Transaction line count for module ADD=402 is above the limit 70"
  *         "overflows": [{
  *           "module": "ADD",
  *           "count": 402,
@@ -45,10 +48,12 @@
  * }
  * 
*/ -public class ReportRejectedTransaction { - static void notifyDiscardedTransaction( +public class RejectedTransactionNotifier { + + static void notifyDiscardedTransactionAsync( final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult transactionSelectionResult, + final Instant timestamp, final URI rejectedTxEndpoint) { if (!transactionSelectionResult.discard()) { return; @@ -59,13 +64,15 @@ static void notifyDiscardedTransaction( // Build JSON-RPC request final JsonObject params = new JsonObject(); + params.addProperty("txRejectionStage", "SEQUENCER"); + params.addProperty("timestamp", timestamp.toString()); params.addProperty("blockNumber", pendingBlockHeader.getNumber()); params.addProperty( "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); final String jsonRequest = - JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransaction", params, 1); + JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransactionV1", params, 1); // Send JSON-RPC request with retries in a new thread JsonRpcClient.sendRequestWithRetries(rejectedTxEndpoint, jsonRequest); diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ReportDroppedTxTest.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifierTest.java similarity index 65% rename from sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ReportDroppedTxTest.java rename to sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifierTest.java index 08906a81..cae53913 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/ReportDroppedTxTest.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifierTest.java @@ -25,9 +25,12 @@ import static org.mockito.Mockito.when; import java.net.URI; +import java.time.Instant; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import com.google.gson.JsonObject; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.datatypes.Transaction; @@ -40,16 +43,17 @@ @WireMockTest @ExtendWith(MockitoExtension.class) -class ReportDroppedTxTest { +class RejectedTransactionNotifierTest { @Mock PendingTransaction pendingTransaction; @Mock ProcessableBlockHeader pendingBlockHeader; @Mock Transaction transaction; @Test void droppedTxIsReported(final WireMockRuntimeInfo wmInfo) throws Exception { + // mock stubbing when(pendingBlockHeader.getNumber()).thenReturn(1L); when(pendingTransaction.getTransaction()).thenReturn(transaction); - Bytes randomEncodedBytes = Bytes.random(32); + final Bytes randomEncodedBytes = Bytes.random(32); when(transaction.encoded()).thenReturn(randomEncodedBytes); // json-rpc stubbing @@ -59,25 +63,34 @@ void droppedTxIsReported(final WireMockRuntimeInfo wmInfo) throws Exception { aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") - .withBody("{\"jsonrpc\":\"2.0\",\"result\":true,\"id\":1}"))); + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); - TestTransactionEvaluationContext context = + final TestTransactionEvaluationContext context = new TestTransactionEvaluationContext(pendingTransaction) .setPendingBlockHeader(pendingBlockHeader); - TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); - ReportRejectedTransaction.notifyDiscardedTransaction( - context, result, URI.create(wmInfo.getHttpBaseUrl())); + // call the method under test + RejectedTransactionNotifier.notifyDiscardedTransactionAsync( + context, result, timestamp, URI.create(wmInfo.getHttpBaseUrl())); - // assert wiremock was called + // sleep a bit to allow async processing Thread.sleep(1000); - verify( - postRequestedFor(urlEqualTo("/")) - .withRequestBody( - equalToJson( - "{\"jsonrpc\":\"2.0\",\"method\":\"linea_saveRejectedTransaction\",\"params\":{\"blockNumber\":1,\"transactionRLP\":\"" - + randomEncodedBytes.toHexString() - + "\",\"reasonMessage\":\"test\"},\"id\":1}"))); + // build expected json-rpc request + final JsonObject params = new JsonObject(); + params.addProperty("txRejectionStage", "SEQUENCER"); + params.addProperty("timestamp", timestamp.toString()); + params.addProperty("blockNumber", 1); + params.addProperty("transactionRLP", randomEncodedBytes.toHexString()); + params.addProperty("reasonMessage", "test"); + + final String jsonRequest = + JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransactionV1", params, 1); + + // assert that the expected json-rpc request was sent to WireMock + verify(postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRequest))); } } From 5e01ae9ba0854e04e40121ca99b023ced7bb076b Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 10 Sep 2024 15:46:44 +1000 Subject: [PATCH 10/26] Adding JsonRpcManager --- .../linea/jsonrpc/JsonRpcManager.java | 138 ++++++++++++++++++ .../LineaTransactionSelectorFactory.java | 5 + .../LineaTransactionSelectorPlugin.java | 11 ++ .../selectors/LineaTransactionSelector.java | 10 +- .../RejectedTransactionNotifier.java | 4 +- 5 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java new file mode 100644 index 00000000..f69da6a6 --- /dev/null +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -0,0 +1,138 @@ +/* + * 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.jsonrpc; + +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * This class is responsible for managing JSON-RPC requests for reporting rejected transactions + */ +@Slf4j +public class JsonRpcManager { + private static final int MAX_THREADS = Math.min(32, Runtime.getRuntime().availableProcessors() * 2); + private static final long MAX_RETRY_DURATION = TimeUnit.HOURS.toMillis(2); + + private final Path jsonDirectory; + private final URI rejectedTxEndpoint; + private final ExecutorService executorService; + private final ScheduledExecutorService schedulerService; + + /** + * Creates a new JSON-RPC manager. + * @param jsonDirectory The directory to store and load JSON files containing json-rpc calls + * @param rejectedTxEndpoint The endpoint to send rejected transactions to + */ + public JsonRpcManager(final Path jsonDirectory, final URI rejectedTxEndpoint) { + this.jsonDirectory = jsonDirectory; + this.rejectedTxEndpoint = rejectedTxEndpoint; + this.executorService = Executors.newFixedThreadPool(MAX_THREADS); + this.schedulerService = Executors.newScheduledThreadPool(1); + } + + /** + * Load existing JSON-RPC and submit them. + */ + public void start() { + loadExistingJsonFiles(); + } + + /** + * Shuts down the executor service and scheduler service. + */ + public void shutdown() { + executorService.shutdown(); + schedulerService.shutdown(); + } + + /** + * Submits a new JSON-RPC call. + * @param jsonContent The JSON content to submit + */ + public void submitNewJsonRpcCall(final String jsonContent) { + try { + final Path jsonFile = saveJsonToFile(jsonContent); + submitJsonRpcCall(jsonFile); + } catch (final IOException e) { + log.error("Failed to save JSON content", e); + } + } + + private void loadExistingJsonFiles() { + try (final DirectoryStream stream = Files.newDirectoryStream(jsonDirectory, "rpc_*.json")) { + for (Path path : stream) { + submitJsonRpcCall(path); + } + } catch (IOException e) { + log.error("Failed to load existing JSON files", e); + } + } + + private void submitJsonRpcCall(final Path jsonFile) { + executorService.submit(() -> { + if (!Files.exists(jsonFile)) { + log.debug("json-rpc file no longer exists, skipping processing: {}", jsonFile); + return; + } + try { + final String jsonContent = new String(Files.readAllBytes(jsonFile)); + final boolean success = sendJsonRpcCall(jsonContent); + if (success) { + Files.deleteIfExists(jsonFile); + } else { + log.warn("Failed to send JSON-RPC call to {}, retrying: {}", rejectedTxEndpoint, jsonFile); + scheduleRetry(jsonFile, System.currentTimeMillis(), 1000); + } + } catch (final IOException e) { + log.error("Failed to process json-rpc file: {}", jsonFile, e); + } + }); + } + + private void scheduleRetry(final Path jsonFile, final long startTime, final long delay) { + // check if we're still within the maximum retry duration + if (System.currentTimeMillis() - startTime < MAX_RETRY_DURATION) { + // schedule a retry + schedulerService.schedule(() -> submitJsonRpcCall(jsonFile), delay, TimeUnit.MILLISECONDS); + + // exponential backoff with a maximum delay of 1 minute + long nextDelay = Math.min(delay * 2, TimeUnit.MINUTES.toMillis(1)); + scheduleRetry(jsonFile, startTime, nextDelay); + } + } + + private boolean sendJsonRpcCall(final String jsonContent) { + // Implement your JSON-RPC call logic here + // Return true if successful, false otherwise + return false; // Placeholder + } + + private Path saveJsonToFile(final String jsonContent) throws IOException { + final String fileName = "rpc_" + System.currentTimeMillis() + ".json"; + final Path filePath = jsonDirectory.resolve(fileName); + Files.write(filePath, jsonContent.getBytes()); + return filePath; + } +} diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java index d71ddd89..d325f31c 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java @@ -22,6 +22,7 @@ import net.consensys.linea.config.LineaTransactionSelectorConfiguration; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import net.consensys.linea.sequencer.txselection.selectors.LineaTransactionSelector; +import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; @@ -32,6 +33,7 @@ */ public class LineaTransactionSelectorFactory implements PluginTransactionSelectorFactory { private final BlockchainService blockchainService; + private final BesuConfiguration besuConfiguration; private final LineaTransactionSelectorConfiguration txSelectorConfiguration; private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; private final LineaProfitabilityConfiguration profitabilityConfiguration; @@ -41,12 +43,14 @@ public class LineaTransactionSelectorFactory implements PluginTransactionSelecto public LineaTransactionSelectorFactory( final BlockchainService blockchainService, + final BesuConfiguration besuConfiguration, final LineaTransactionSelectorConfiguration txSelectorConfiguration, final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, final Map limitsMap) { this.blockchainService = blockchainService; + this.besuConfiguration = besuConfiguration; this.txSelectorConfiguration = txSelectorConfiguration; this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; this.profitabilityConfiguration = profitabilityConfiguration; @@ -58,6 +62,7 @@ public LineaTransactionSelectorFactory( public PluginTransactionSelector create() { return new LineaTransactionSelector( blockchainService, + besuConfiguration, txSelectorConfiguration, l1L2BridgeConfiguration, profitabilityConfiguration, 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 000b0671..d142995c 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 @@ -24,6 +24,7 @@ import net.consensys.linea.AbstractLineaRequiredPlugin; 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; @@ -38,6 +39,7 @@ public class LineaTransactionSelectorPlugin extends AbstractLineaRequiredPlugin public static final String NAME = "linea"; private TransactionSelectionService transactionSelectionService; private BlockchainService blockchainService; + private BesuConfiguration besuConfiguration; @Override public Optional getName() { @@ -61,6 +63,14 @@ public void doRegister(final BesuContext context) { () -> new RuntimeException( "Failed to obtain BlockchainService from the BesuContext.")); + + besuConfiguration = + context + .getService(BesuConfiguration.class) + .orElseThrow( + () -> + new RuntimeException( + "Failed to obtain BesuConfiguration from the BesuContext.")); } @Override @@ -69,6 +79,7 @@ public void start() { transactionSelectionService.registerPluginTransactionSelectorFactory( new LineaTransactionSelectorFactory( blockchainService, + besuConfiguration, transactionSelectorConfiguration(), l1L2BridgeSharedConfiguration(), profitabilityConfiguration(), 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 d7bb598c..fbf23a2d 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 @@ -30,6 +30,7 @@ import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.plugin.data.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; +import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; @@ -41,15 +42,18 @@ public class LineaTransactionSelector implements PluginTransactionSelector { private TraceLineLimitTransactionSelector traceLineLimitTransactionSelector; private final List selectors; + private final BesuConfiguration besuConfiguration; private final Optional rejectedTxEndpoint; public LineaTransactionSelector( final BlockchainService blockchainService, + final BesuConfiguration besuConfiguration, final LineaTransactionSelectorConfiguration txSelectorConfiguration, final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, final Map limitsMap) { + this.besuConfiguration = besuConfiguration; rejectedTxEndpoint = Optional.ofNullable(txSelectorConfiguration.rejectedTxEndpoint()); selectors = createTransactionSelectors( @@ -160,7 +164,11 @@ public void onTransactionNotSelected( rejectedTxEndpoint.ifPresent( uri -> notifyDiscardedTransactionAsync( - evaluationContext, transactionSelectionResult, Instant.now(), uri)); + evaluationContext, + transactionSelectionResult, + Instant.now(), + uri, + this.besuConfiguration.getDataPath())); } /** diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java index c5403c14..587781cb 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java @@ -15,6 +15,7 @@ package net.consensys.linea.sequencer.txselection.selectors; import java.net.URI; +import java.nio.file.Path; import java.time.Instant; import com.google.gson.JsonObject; @@ -54,7 +55,8 @@ static void notifyDiscardedTransactionAsync( final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult transactionSelectionResult, final Instant timestamp, - final URI rejectedTxEndpoint) { + final URI rejectedTxEndpoint, + final Path besuDataPath) { if (!transactionSelectionResult.discard()) { return; } From 6fb16d651e8a333bd1a70a0e53f45874fd8daeda Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 10 Sep 2024 16:23:55 +1000 Subject: [PATCH 11/26] Adding compileOnly okhttp dependency --- gradle/dependency-management.gradle | 2 + sequencer/build.gradle | 2 + .../linea/jsonrpc/JsonRpcManager.java | 185 +++++++++--------- .../linea/jsonrpc/JsonRpcRequestBuilder.java | 57 +++++- .../LineaTransactionSelectorFactory.java | 15 +- .../LineaTransactionSelectorPlugin.java | 23 ++- .../selectors/LineaTransactionSelector.java | 36 ++-- 7 files changed, 194 insertions(+), 126 deletions(-) diff --git a/gradle/dependency-management.gradle b/gradle/dependency-management.gradle index f4401380..2891965b 100644 --- a/gradle/dependency-management.gradle +++ b/gradle/dependency-management.gradle @@ -104,6 +104,8 @@ dependencyManagement { entry 'picocli-codegen' } + dependency 'com.squareup.okhttp3:okhttp:4.12.0' + dependencySet(group: 'io.tmio', version: '2.4.2') { entry 'tuweni-bytes' entry 'tuweni-net' diff --git a/sequencer/build.gradle b/sequencer/build.gradle index b503fe05..782b9938 100644 --- a/sequencer/build.gradle +++ b/sequencer/build.gradle @@ -47,6 +47,8 @@ dependencies { compileOnly 'io.vertx:vertx-core' + compileOnly 'com.squareup.okhttp3:okhttp' + implementation project(":native:compress") implementation 'com.fasterxml.jackson.core:jackson-databind' 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 f69da6a6..bbd541ec 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -15,8 +15,6 @@ package net.consensys.linea.jsonrpc; -import lombok.extern.slf4j.Slf4j; - import java.io.IOException; import java.net.URI; import java.nio.file.DirectoryStream; @@ -27,112 +25,115 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -/** - * This class is responsible for managing JSON-RPC requests for reporting rejected transactions - */ +import lombok.extern.slf4j.Slf4j; + +/** This class is responsible for managing JSON-RPC requests for reporting rejected transactions */ @Slf4j public class JsonRpcManager { - private static final int MAX_THREADS = Math.min(32, Runtime.getRuntime().availableProcessors() * 2); - private static final long MAX_RETRY_DURATION = TimeUnit.HOURS.toMillis(2); + private static final int MAX_THREADS = + Math.min(32, Runtime.getRuntime().availableProcessors() * 2); + private static final long MAX_RETRY_DURATION = TimeUnit.HOURS.toMillis(2); - private final Path jsonDirectory; - private final URI rejectedTxEndpoint; - private final ExecutorService executorService; - private final ScheduledExecutorService schedulerService; + private final Path jsonDirectory; + private final URI rejectedTxEndpoint; + private final ExecutorService executorService; + private final ScheduledExecutorService schedulerService; - /** - * Creates a new JSON-RPC manager. - * @param jsonDirectory The directory to store and load JSON files containing json-rpc calls - * @param rejectedTxEndpoint The endpoint to send rejected transactions to - */ - public JsonRpcManager(final Path jsonDirectory, final URI rejectedTxEndpoint) { - this.jsonDirectory = jsonDirectory; - this.rejectedTxEndpoint = rejectedTxEndpoint; - this.executorService = Executors.newFixedThreadPool(MAX_THREADS); - this.schedulerService = Executors.newScheduledThreadPool(1); - } + /** + * Creates a new JSON-RPC manager. + * + * @param jsonDirectory The directory to store and load JSON files containing json-rpc calls + * @param rejectedTxEndpoint The endpoint to send rejected transactions to + */ + public JsonRpcManager(final Path jsonDirectory, final URI rejectedTxEndpoint) { + this.jsonDirectory = jsonDirectory; + this.rejectedTxEndpoint = rejectedTxEndpoint; + this.executorService = Executors.newFixedThreadPool(MAX_THREADS); + this.schedulerService = Executors.newScheduledThreadPool(1); + } - /** - * Load existing JSON-RPC and submit them. - */ - public void start() { - loadExistingJsonFiles(); - } + /** Load existing JSON-RPC and submit them. */ + public JsonRpcManager start() { + loadExistingJsonFiles(); + return this; + } - /** - * Shuts down the executor service and scheduler service. - */ - public void shutdown() { - executorService.shutdown(); - schedulerService.shutdown(); - } + /** Shuts down the executor service and scheduler service. */ + public void shutdown() { + executorService.shutdown(); + schedulerService.shutdown(); + } - /** - * Submits a new JSON-RPC call. - * @param jsonContent The JSON content to submit - */ - public void submitNewJsonRpcCall(final String jsonContent) { - try { - final Path jsonFile = saveJsonToFile(jsonContent); - submitJsonRpcCall(jsonFile); - } catch (final IOException e) { - log.error("Failed to save JSON content", e); - } + /** + * Submits a new JSON-RPC call. + * + * @param jsonContent The JSON content to submit + */ + public void submitNewJsonRpcCall(final String jsonContent) { + try { + final Path jsonFile = saveJsonToFile(jsonContent); + submitJsonRpcCall(jsonFile); + } catch (final IOException e) { + log.error("Failed to save JSON content", e); } + } - private void loadExistingJsonFiles() { - try (final DirectoryStream stream = Files.newDirectoryStream(jsonDirectory, "rpc_*.json")) { - for (Path path : stream) { - submitJsonRpcCall(path); - } - } catch (IOException e) { - log.error("Failed to load existing JSON files", e); - } + private void loadExistingJsonFiles() { + try (final DirectoryStream stream = + Files.newDirectoryStream(jsonDirectory, "rpc_*.json")) { + for (Path path : stream) { + submitJsonRpcCall(path); + } + } catch (IOException e) { + log.error("Failed to load existing JSON files", e); } + } - private void submitJsonRpcCall(final Path jsonFile) { - executorService.submit(() -> { - if (!Files.exists(jsonFile)) { - log.debug("json-rpc file no longer exists, skipping processing: {}", jsonFile); - return; - } - try { - final String jsonContent = new String(Files.readAllBytes(jsonFile)); - final boolean success = sendJsonRpcCall(jsonContent); - if (success) { - Files.deleteIfExists(jsonFile); - } else { - log.warn("Failed to send JSON-RPC call to {}, retrying: {}", rejectedTxEndpoint, jsonFile); - scheduleRetry(jsonFile, System.currentTimeMillis(), 1000); - } - } catch (final IOException e) { - log.error("Failed to process json-rpc file: {}", jsonFile, e); + private void submitJsonRpcCall(final Path jsonFile) { + executorService.submit( + () -> { + if (!Files.exists(jsonFile)) { + log.debug("json-rpc file no longer exists, skipping processing: {}", jsonFile); + return; + } + try { + final String jsonContent = new String(Files.readAllBytes(jsonFile)); + final boolean success = sendJsonRpcCall(jsonContent); + if (success) { + Files.deleteIfExists(jsonFile); + } else { + log.warn( + "Failed to send JSON-RPC call to {}, retrying: {}", rejectedTxEndpoint, jsonFile); + scheduleRetry(jsonFile, System.currentTimeMillis(), 1000); } + } catch (final IOException e) { + log.error("Failed to process json-rpc file: {}", jsonFile, e); + } }); - } + } - private void scheduleRetry(final Path jsonFile, final long startTime, final long delay) { - // check if we're still within the maximum retry duration - if (System.currentTimeMillis() - startTime < MAX_RETRY_DURATION) { - // schedule a retry - schedulerService.schedule(() -> submitJsonRpcCall(jsonFile), delay, TimeUnit.MILLISECONDS); + private void scheduleRetry(final Path jsonFile, final long startTime, final long delay) { + // check if we're still within the maximum retry duration + if (System.currentTimeMillis() - startTime < MAX_RETRY_DURATION) { + // schedule a retry + schedulerService.schedule(() -> submitJsonRpcCall(jsonFile), delay, TimeUnit.MILLISECONDS); - // exponential backoff with a maximum delay of 1 minute - long nextDelay = Math.min(delay * 2, TimeUnit.MINUTES.toMillis(1)); - scheduleRetry(jsonFile, startTime, nextDelay); - } + // exponential backoff with a maximum delay of 1 minute + long nextDelay = Math.min(delay * 2, TimeUnit.MINUTES.toMillis(1)); + scheduleRetry(jsonFile, startTime, nextDelay); } + } - private boolean sendJsonRpcCall(final String jsonContent) { - // Implement your JSON-RPC call logic here - // Return true if successful, false otherwise - return false; // Placeholder - } + private boolean sendJsonRpcCall(final String jsonContent) { + // Implement your JSON-RPC call logic here + // Return true if successful, false otherwise + return false; // Placeholder + } - private Path saveJsonToFile(final String jsonContent) throws IOException { - final String fileName = "rpc_" + System.currentTimeMillis() + ".json"; - final Path filePath = jsonDirectory.resolve(fileName); - Files.write(filePath, jsonContent.getBytes()); - return filePath; - } + private Path saveJsonToFile(final String jsonContent) throws IOException { + final String fileName = "rpc_" + System.currentTimeMillis() + ".json"; + final Path filePath = jsonDirectory.resolve(fileName); + Files.write(filePath, jsonContent.getBytes()); + return filePath; + } } 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 f047c701..d6cd0abd 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java @@ -15,15 +15,64 @@ package net.consensys.linea.jsonrpc; +import java.time.Instant; + 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; +/** Helper class to build JSON-RPC requests for rejected transactions. */ public class JsonRpcRequestBuilder { - public static String buildRequest(final String method, final JsonObject params, int id) { - JsonObject request = new JsonObject(); + /** + * + * + *
+   * {@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 static String buildRejectedTxRequest( + final TransactionEvaluationContext evaluationContext, + final TransactionSelectionResult transactionSelectionResult, + final Instant timestamp) { + // if (!transactionSelectionResult.discard()) { + // return; + // } + + final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); + final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); + + // Build JSON-RPC request + final JsonObject params = new JsonObject(); + params.addProperty("txRejectionStage", "SEQUENCER"); + params.addProperty("timestamp", timestamp.toString()); + params.addProperty("blockNumber", pendingBlockHeader.getNumber()); + params.addProperty( + "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); + params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); + + final JsonObject request = new JsonObject(); request.addProperty("jsonrpc", "2.0"); - request.addProperty("method", method); + request.addProperty("method", "linea_saveRejectedTransactionV1"); request.add("params", params); - request.addProperty("id", id); + request.addProperty("id", 1); return request.toString(); } } diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java index d325f31c..0d5fc6ff 100644 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java +++ b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/LineaTransactionSelectorFactory.java @@ -16,13 +16,14 @@ package net.consensys.linea.sequencer.txselection; import java.util.Map; +import java.util.Optional; import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTracerConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import net.consensys.linea.sequencer.txselection.selectors.LineaTransactionSelector; -import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelectorFactory; @@ -33,7 +34,7 @@ */ public class LineaTransactionSelectorFactory implements PluginTransactionSelectorFactory { private final BlockchainService blockchainService; - private final BesuConfiguration besuConfiguration; + private final Optional rejectedTxJsonRpcManager; private final LineaTransactionSelectorConfiguration txSelectorConfiguration; private final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration; private final LineaProfitabilityConfiguration profitabilityConfiguration; @@ -43,30 +44,30 @@ public class LineaTransactionSelectorFactory implements PluginTransactionSelecto public LineaTransactionSelectorFactory( final BlockchainService blockchainService, - final BesuConfiguration besuConfiguration, final LineaTransactionSelectorConfiguration txSelectorConfiguration, final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, - final Map limitsMap) { + final Map limitsMap, + final Optional rejectedTxJsonRpcManager) { this.blockchainService = blockchainService; - this.besuConfiguration = besuConfiguration; this.txSelectorConfiguration = txSelectorConfiguration; this.l1L2BridgeConfiguration = l1L2BridgeConfiguration; this.profitabilityConfiguration = profitabilityConfiguration; this.tracerConfiguration = tracerConfiguration; this.limitsMap = limitsMap; + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; } @Override public PluginTransactionSelector create() { return new LineaTransactionSelector( blockchainService, - besuConfiguration, txSelectorConfiguration, l1L2BridgeConfiguration, profitabilityConfiguration, tracerConfiguration, - limitsMap); + limitsMap, + rejectedTxJsonRpcManager); } } 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 d142995c..1bf48bc5 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,6 +22,8 @@ import com.google.auto.service.AutoService; import lombok.extern.slf4j.Slf4j; import net.consensys.linea.AbstractLineaRequiredPlugin; +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; @@ -39,6 +41,7 @@ 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; @Override @@ -76,14 +79,28 @@ public void doRegister(final BesuContext context) { @Override public void start() { super.start(); + final LineaTransactionSelectorConfiguration txSelectorConfiguration = + transactionSelectorConfiguration(); + rejectedTxJsonRpcManager = + Optional.ofNullable(txSelectorConfiguration.rejectedTxEndpoint()) + .map( + endpoint -> + new JsonRpcManager( + besuConfiguration.getDataPath().resolve("rej_tx_rpc"), endpoint)); transactionSelectionService.registerPluginTransactionSelectorFactory( new LineaTransactionSelectorFactory( blockchainService, - besuConfiguration, - transactionSelectorConfiguration(), + txSelectorConfiguration, l1L2BridgeSharedConfiguration(), profitabilityConfiguration(), tracerConfiguration(), - createLimitModules(tracerConfiguration()))); + createLimitModules(tracerConfiguration()), + rejectedTxJsonRpcManager)); + } + + @Override + public void stop() { + super.stop(); + rejectedTxJsonRpcManager.ifPresent(JsonRpcManager::shutdown); } } 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 fbf23a2d..d532501e 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 @@ -14,9 +14,6 @@ */ package net.consensys.linea.sequencer.txselection.selectors; -import static net.consensys.linea.sequencer.txselection.selectors.RejectedTransactionNotifier.notifyDiscardedTransactionAsync; - -import java.net.URI; import java.time.Instant; import java.util.List; import java.util.Map; @@ -26,11 +23,12 @@ import net.consensys.linea.config.LineaProfitabilityConfiguration; import net.consensys.linea.config.LineaTracerConfiguration; import net.consensys.linea.config.LineaTransactionSelectorConfiguration; +import net.consensys.linea.jsonrpc.JsonRpcManager; +import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; import net.consensys.linea.plugins.config.LineaL1L2BridgeSharedConfiguration; import org.hyperledger.besu.datatypes.PendingTransaction; import org.hyperledger.besu.plugin.data.TransactionProcessingResult; import org.hyperledger.besu.plugin.data.TransactionSelectionResult; -import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.plugin.services.BlockchainService; import org.hyperledger.besu.plugin.services.tracer.BlockAwareOperationTracer; import org.hyperledger.besu.plugin.services.txselection.PluginTransactionSelector; @@ -42,19 +40,17 @@ public class LineaTransactionSelector implements PluginTransactionSelector { private TraceLineLimitTransactionSelector traceLineLimitTransactionSelector; private final List selectors; - private final BesuConfiguration besuConfiguration; - private final Optional rejectedTxEndpoint; + private final Optional rejectedTxJsonRpcManager; public LineaTransactionSelector( final BlockchainService blockchainService, - final BesuConfiguration besuConfiguration, final LineaTransactionSelectorConfiguration txSelectorConfiguration, final LineaL1L2BridgeSharedConfiguration l1L2BridgeConfiguration, final LineaProfitabilityConfiguration profitabilityConfiguration, final LineaTracerConfiguration tracerConfiguration, - final Map limitsMap) { - this.besuConfiguration = besuConfiguration; - rejectedTxEndpoint = Optional.ofNullable(txSelectorConfiguration.rejectedTxEndpoint()); + final Map limitsMap, + final Optional rejectedTxJsonRpcManager) { + this.rejectedTxJsonRpcManager = rejectedTxJsonRpcManager; selectors = createTransactionSelectors( blockchainService, @@ -68,9 +64,9 @@ public LineaTransactionSelector( /** * Creates a list of selectors based on Linea configuration. * - * @param blockchainService + * @param blockchainService Blockchain service. * @param txSelectorConfiguration The configuration to use. - * @param profitabilityConfiguration + * @param profitabilityConfiguration The profitability configuration. * @param limitsMap The limits map. * @return A list of selectors. */ @@ -161,14 +157,14 @@ public void onTransactionNotSelected( selector -> selector.onTransactionNotSelected(evaluationContext, transactionSelectionResult)); - rejectedTxEndpoint.ifPresent( - uri -> - notifyDiscardedTransactionAsync( - evaluationContext, - transactionSelectionResult, - Instant.now(), - uri, - this.besuConfiguration.getDataPath())); + rejectedTxJsonRpcManager.ifPresent( + jsonRpcManager -> { + if (transactionSelectionResult.discard()) { + jsonRpcManager.submitNewJsonRpcCall( + JsonRpcRequestBuilder.buildRejectedTxRequest( + evaluationContext, transactionSelectionResult, Instant.now())); + } + }); } /** From 4af7d4a0694ae3aaca5ed0c918c8110e19d7edb7 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 10 Sep 2024 17:04:14 +1000 Subject: [PATCH 12/26] Updated JsonRPCManagerTests --- .../linea/jsonrpc/JsonRpcManager.java | 84 ++++++++++++++++--- .../LineaTransactionSelectorPlugin.java | 5 +- .../RejectedTransactionNotifier.java | 82 ------------------ .../JsonRpcManagerTest.java} | 68 +++++++-------- .../TestTransactionEvaluationContext.java | 3 +- 5 files changed, 104 insertions(+), 138 deletions(-) delete mode 100644 sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java rename sequencer/src/test/java/net/consensys/linea/{sequencer/txselection/selectors/RejectedTransactionNotifierTest.java => jsonrpc/JsonRpcManagerTest.java} (57%) 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 bbd541ec..b01db026 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -16,6 +16,7 @@ package net.consensys.linea.jsonrpc; import java.io.IOException; +import java.io.UncheckedIOException; import java.net.URI; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -25,7 +26,14 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; /** This class is responsible for managing JSON-RPC requests for reporting rejected transactions */ @Slf4j @@ -33,8 +41,12 @@ public class JsonRpcManager { private static final int MAX_THREADS = Math.min(32, Runtime.getRuntime().availableProcessors() * 2); private static final long MAX_RETRY_DURATION = TimeUnit.HOURS.toMillis(2); + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - private final Path jsonDirectory; + private final OkHttpClient client = new OkHttpClient(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final Path rejTxRpcDirectory; private final URI rejectedTxEndpoint; private final ExecutorService executorService; private final ScheduledExecutorService schedulerService; @@ -42,11 +54,12 @@ public class JsonRpcManager { /** * Creates a new JSON-RPC manager. * - * @param jsonDirectory The directory to store and load JSON files containing json-rpc calls + * @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 */ - public JsonRpcManager(final Path jsonDirectory, final URI rejectedTxEndpoint) { - this.jsonDirectory = jsonDirectory; + public JsonRpcManager(final Path besuDataDir, final URI rejectedTxEndpoint) { + this.rejTxRpcDirectory = besuDataDir.resolve("rej_tx_rpc"); this.rejectedTxEndpoint = rejectedTxEndpoint; this.executorService = Executors.newFixedThreadPool(MAX_THREADS); this.schedulerService = Executors.newScheduledThreadPool(1); @@ -54,8 +67,17 @@ public JsonRpcManager(final Path jsonDirectory, final URI rejectedTxEndpoint) { /** Load existing JSON-RPC and submit them. */ public JsonRpcManager start() { - loadExistingJsonFiles(); - return this; + try { + // Create the rej-tx-rpc directory if it doesn't exist + Files.createDirectories(rejTxRpcDirectory); + + // Load existing JSON files + loadExistingJsonFiles(); + return this; + } catch (IOException e) { + log.error("Failed to create or access rej-tx-rpc directory", e); + throw new UncheckedIOException(e); + } } /** Shuts down the executor service and scheduler service. */ @@ -80,11 +102,11 @@ public void submitNewJsonRpcCall(final String jsonContent) { private void loadExistingJsonFiles() { try (final DirectoryStream stream = - Files.newDirectoryStream(jsonDirectory, "rpc_*.json")) { - for (Path path : stream) { + Files.newDirectoryStream(rejTxRpcDirectory, "rpc_*.json")) { + for (final Path path : stream) { submitJsonRpcCall(path); } - } catch (IOException e) { + } catch (final IOException e) { log.error("Failed to load existing JSON files", e); } } @@ -125,14 +147,50 @@ private void scheduleRetry(final Path jsonFile, final long startTime, final long } private boolean sendJsonRpcCall(final String jsonContent) { - // Implement your JSON-RPC call logic here - // Return true if successful, false otherwise - return false; // Placeholder + final RequestBody body = RequestBody.create(jsonContent, JSON); + final Request request = + new Request.Builder().url(rejectedTxEndpoint.toString()).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()); + return false; + } + + // process the response body here ... + final String responseBody = response.body() != null ? response.body().string() : null; + if (responseBody == null) { + log.error("Unexpected empty response body from rejected-tx endpoint"); + return false; + } + + final JsonNode jsonNode = objectMapper.readTree(responseBody); + if (jsonNode == null) { + log.error("Failed to parse JSON response from rejected-tx endpoint: {}", responseBody); + return false; + } + if (jsonNode.has("error")) { + log.error("Error response from rejected-tx endpoint: {}", jsonNode.get("error")); + return false; + } + // Check for result + if (jsonNode.has("result")) { + String status = jsonNode.get("result").get("status").asText(); + log.debug("Rejected-tx JSON-RPC call successful. Status: {}", status); + return true; + } + + 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); + return false; + } } private Path saveJsonToFile(final String jsonContent) throws IOException { final String fileName = "rpc_" + System.currentTimeMillis() + ".json"; - final Path filePath = jsonDirectory.resolve(fileName); + final Path filePath = rejTxRpcDirectory.resolve(fileName); Files.write(filePath, jsonContent.getBytes()); return filePath; } 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 1bf48bc5..1ca96d48 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 @@ -83,10 +83,7 @@ public void start() { transactionSelectorConfiguration(); rejectedTxJsonRpcManager = Optional.ofNullable(txSelectorConfiguration.rejectedTxEndpoint()) - .map( - endpoint -> - new JsonRpcManager( - besuConfiguration.getDataPath().resolve("rej_tx_rpc"), endpoint)); + .map(endpoint -> new JsonRpcManager(besuConfiguration.getDataPath(), endpoint).start()); transactionSelectionService.registerPluginTransactionSelectorFactory( new LineaTransactionSelectorFactory( blockchainService, diff --git a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java b/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java deleted file mode 100644 index 587781cb..00000000 --- a/sequencer/src/main/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifier.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.sequencer.txselection.selectors; - -import java.net.URI; -import java.nio.file.Path; -import java.time.Instant; - -import com.google.gson.JsonObject; -import net.consensys.linea.jsonrpc.JsonRpcClient; -import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; -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; - -/** - * - * - *
- * {@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 RejectedTransactionNotifier { - - static void notifyDiscardedTransactionAsync( - final TransactionEvaluationContext evaluationContext, - final TransactionSelectionResult transactionSelectionResult, - final Instant timestamp, - final URI rejectedTxEndpoint, - final Path besuDataPath) { - if (!transactionSelectionResult.discard()) { - return; - } - - final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); - final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); - - // Build JSON-RPC request - final JsonObject params = new JsonObject(); - params.addProperty("txRejectionStage", "SEQUENCER"); - params.addProperty("timestamp", timestamp.toString()); - params.addProperty("blockNumber", pendingBlockHeader.getNumber()); - params.addProperty( - "transactionRLP", pendingTransaction.getTransaction().encoded().toHexString()); - params.addProperty("reasonMessage", transactionSelectionResult.maybeInvalidReason().orElse("")); - - final String jsonRequest = - JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransactionV1", params, 1); - - // Send JSON-RPC request with retries in a new thread - JsonRpcClient.sendRequestWithRetries(rejectedTxEndpoint, jsonRequest); - } -} diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifierTest.java b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java similarity index 57% rename from sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifierTest.java rename to sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java index cae53913..43bae5b8 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/RejectedTransactionNotifierTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java @@ -1,19 +1,4 @@ -/* - * 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.sequencer.txselection.selectors; +package net.consensys.linea.jsonrpc; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; @@ -25,31 +10,48 @@ import static org.mockito.Mockito.when; import java.net.URI; +import java.nio.file.Path; import java.time.Instant; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import com.google.gson.JsonObject; -import net.consensys.linea.jsonrpc.JsonRpcRequestBuilder; +import net.consensys.linea.sequencer.txselection.selectors.TestTransactionEvaluationContext; 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; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @WireMockTest @ExtendWith(MockitoExtension.class) -class RejectedTransactionNotifierTest { - @Mock PendingTransaction pendingTransaction; - @Mock ProcessableBlockHeader pendingBlockHeader; - @Mock Transaction transaction; +class JsonRpcManagerTest { + @TempDir private Path tempDataDir; + private JsonRpcManager jsonRpcManager; + + @Mock private PendingTransaction pendingTransaction; + @Mock private ProcessableBlockHeader pendingBlockHeader; + @Mock private Transaction transaction; + + @BeforeEach + void init(final WireMockRuntimeInfo wmInfo) { + jsonRpcManager = new JsonRpcManager(tempDataDir, URI.create(wmInfo.getHttpBaseUrl())); + jsonRpcManager.start(); + } + + @AfterEach + void cleanup() { + jsonRpcManager.shutdown(); + } @Test - void droppedTxIsReported(final WireMockRuntimeInfo wmInfo) throws Exception { + void rejectedTxIsReported() throws InterruptedException { // mock stubbing when(pendingBlockHeader.getNumber()).thenReturn(1L); when(pendingTransaction.getTransaction()).thenReturn(transaction); @@ -72,25 +74,15 @@ void droppedTxIsReported(final WireMockRuntimeInfo wmInfo) throws Exception { final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); - // call the method under test - RejectedTransactionNotifier.notifyDiscardedTransactionAsync( - context, result, timestamp, URI.create(wmInfo.getHttpBaseUrl())); + // method under test + final String jsonRpcCall = + JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); // sleep a bit to allow async processing Thread.sleep(1000); - // build expected json-rpc request - final JsonObject params = new JsonObject(); - params.addProperty("txRejectionStage", "SEQUENCER"); - params.addProperty("timestamp", timestamp.toString()); - params.addProperty("blockNumber", 1); - params.addProperty("transactionRLP", randomEncodedBytes.toHexString()); - params.addProperty("reasonMessage", "test"); - - final String jsonRequest = - JsonRpcRequestBuilder.buildRequest("linea_saveRejectedTransactionV1", params, 1); - // assert that the expected json-rpc request was sent to WireMock - verify(postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRequest))); + verify(postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); } } diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java index 4edce437..bc9a48ba 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java @@ -20,7 +20,8 @@ import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; -class TestTransactionEvaluationContext implements TransactionEvaluationContext { +public class TestTransactionEvaluationContext + implements TransactionEvaluationContext { private PendingTransaction pendingTransaction; private Wei transactionGasPrice; private Wei minGasPrice; From 5dcf745b3991edbb7567ecc89b164d6b935c424d Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 10 Sep 2024 17:31:36 +1000 Subject: [PATCH 13/26] Adding license header --- .../linea/jsonrpc/JsonRpcManagerTest.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 43bae5b8..ea4e7a2e 100644 --- a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java @@ -1,3 +1,18 @@ +/* + * 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.jsonrpc; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; From 3eb843d0668c7bc9409747e30b5eb58ce7dc7252 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 10 Sep 2024 17:32:59 +1000 Subject: [PATCH 14/26] Removing initial implementation of json rpc client --- .../linea/jsonrpc/JsonRpcClient.java | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java diff --git a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java deleted file mode 100644 index ef8f2c05..00000000 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcClient.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * 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.jsonrpc; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.Scanner; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; - -public class JsonRpcClient { - private static final int MAX_RETRIES = 3; - private static final ExecutorService executorService = Executors.newCachedThreadPool(); - - public static String sendRequest(final URI endpoint, final String jsonInputString) - throws Exception { - HttpURLConnection conn = getHttpURLConnection(endpoint, jsonInputString); - - int responseCode = conn.getResponseCode(); - if (responseCode == HttpURLConnection.HTTP_OK) { - try (Scanner scanner = new Scanner(conn.getInputStream(), StandardCharsets.UTF_8)) { - return scanner.useDelimiter("\\A").next(); - } - } else { - throw new RuntimeException("Failed : HTTP error code : " + responseCode); - } - } - - private static HttpURLConnection getHttpURLConnection( - final URI endpoint, final String jsonInputString) throws IOException { - final URL url = endpoint.toURL(); - final HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setRequestMethod("POST"); - conn.setRequestProperty("Content-Type", "application/json; utf-8"); - conn.setRequestProperty("Accept", "application/json"); - conn.setDoOutput(true); - - try (final OutputStream os = conn.getOutputStream()) { - final byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8); - os.write(input, 0, input.length); - } - return conn; - } - - public static Future sendRequestWithRetries( - final URI endpoint, final String jsonInputString) { - Callable task = - () -> { - int attempt = 0; - while (attempt < MAX_RETRIES) { - try { - return sendRequest(endpoint, jsonInputString); - } catch (Exception e) { - attempt++; - if (attempt >= MAX_RETRIES) { - throw e; - } - } - } - return null; - }; - return executorService.submit(task); - } -} From 7a6a78a4b802dcc0916d2428f90b7ee39f41e83e Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Tue, 10 Sep 2024 21:45:48 +1000 Subject: [PATCH 15/26] Updating retry schedule logic --- .../linea/jsonrpc/JsonRpcManager.java | 84 ++++++--- .../linea/jsonrpc/JsonRpcManagerTest.java | 177 +++++++++++++++++- 2 files changed, 229 insertions(+), 32 deletions(-) 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 b01db026..0db2203f 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -19,8 +19,14 @@ import java.io.UncheckedIOException; import java.net.URI; import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Comparator; +import java.util.Map; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -38,6 +44,7 @@ /** This class is responsible for managing JSON-RPC requests for reporting rejected transactions */ @Slf4j public class JsonRpcManager { + private static final long INITIAL_RETRY_DELAY = 1000L; private static final int MAX_THREADS = Math.min(32, Runtime.getRuntime().availableProcessors() * 2); private static final long MAX_RETRY_DURATION = TimeUnit.HOURS.toMillis(2); @@ -45,11 +52,12 @@ public class JsonRpcManager { 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 ExecutorService executorService; - private final ScheduledExecutorService schedulerService; + private final ScheduledExecutorService retrySchedulerService; /** * Creates a new JSON-RPC manager. @@ -62,7 +70,7 @@ public JsonRpcManager(final Path besuDataDir, final URI rejectedTxEndpoint) { this.rejTxRpcDirectory = besuDataDir.resolve("rej_tx_rpc"); this.rejectedTxEndpoint = rejectedTxEndpoint; this.executorService = Executors.newFixedThreadPool(MAX_THREADS); - this.schedulerService = Executors.newScheduledThreadPool(1); + this.retrySchedulerService = Executors.newScheduledThreadPool(1); } /** Load existing JSON-RPC and submit them. */ @@ -83,7 +91,7 @@ public JsonRpcManager start() { /** Shuts down the executor service and scheduler service. */ public void shutdown() { executorService.shutdown(); - schedulerService.shutdown(); + retrySchedulerService.shutdown(); } /** @@ -94,28 +102,41 @@ public void shutdown() { public void submitNewJsonRpcCall(final String jsonContent) { try { final Path jsonFile = saveJsonToFile(jsonContent); - submitJsonRpcCall(jsonFile); + fileStartTimes.put(jsonFile, System.currentTimeMillis()); + submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY); } catch (final IOException e) { log.error("Failed to save JSON content", e); } } private void loadExistingJsonFiles() { - try (final DirectoryStream stream = - Files.newDirectoryStream(rejTxRpcDirectory, "rpc_*.json")) { - for (final Path path : stream) { - submitJsonRpcCall(path); + try { + final TreeSet sortedFiles = new TreeSet<>(Comparator.comparing(Path::getFileName)); + + try (DirectoryStream stream = + Files.newDirectoryStream(rejTxRpcDirectory, "rpc_*.json")) { + for (Path path : stream) { + sortedFiles.add(path); + } + } + + for (Path path : sortedFiles) { + fileStartTimes.put(path, System.currentTimeMillis()); + submitJsonRpcCall(path, INITIAL_RETRY_DELAY); } + + log.info("Loaded {} existing JSON files for rej-tx reporting", sortedFiles.size()); } catch (final IOException e) { log.error("Failed to load existing JSON files", e); } } - private void submitJsonRpcCall(final Path jsonFile) { + private void submitJsonRpcCall(final Path jsonFile, final long nextDelay) { executorService.submit( () -> { if (!Files.exists(jsonFile)) { log.debug("json-rpc file no longer exists, skipping processing: {}", jsonFile); + fileStartTimes.remove(jsonFile); return; } try { @@ -123,26 +144,35 @@ private void submitJsonRpcCall(final Path jsonFile) { final boolean success = sendJsonRpcCall(jsonContent); if (success) { Files.deleteIfExists(jsonFile); + fileStartTimes.remove(jsonFile); } else { - log.warn( + log.error( "Failed to send JSON-RPC call to {}, retrying: {}", rejectedTxEndpoint, jsonFile); - scheduleRetry(jsonFile, System.currentTimeMillis(), 1000); + scheduleRetry(jsonFile, nextDelay); } - } catch (final IOException e) { + } catch (final Exception e) { log.error("Failed to process json-rpc file: {}", jsonFile, e); + scheduleRetry(jsonFile, nextDelay); } }); } - private void scheduleRetry(final Path jsonFile, final long startTime, final long delay) { + private void scheduleRetry(final Path jsonFile, final long currentDelay) { + final Long startTime = fileStartTimes.get(jsonFile); + if (startTime == null) { + log.debug("No start time found for file: {}. Skipping retry.", jsonFile); + return; + } + // check if we're still within the maximum retry duration if (System.currentTimeMillis() - startTime < MAX_RETRY_DURATION) { - // schedule a retry - schedulerService.schedule(() -> submitJsonRpcCall(jsonFile), delay, TimeUnit.MILLISECONDS); - - // exponential backoff with a maximum delay of 1 minute - long nextDelay = Math.min(delay * 2, TimeUnit.MINUTES.toMillis(1)); - scheduleRetry(jsonFile, startTime, nextDelay); + // schedule a retry with exponential backoff + long nextDelay = Math.min(currentDelay * 2, TimeUnit.MINUTES.toMillis(1)); // Cap at 1 minute + retrySchedulerService.schedule( + () -> submitJsonRpcCall(jsonFile, nextDelay), currentDelay, TimeUnit.MILLISECONDS); + } else { + log.error("Exceeded maximum retry duration for rej-tx json-rpc file: {}", jsonFile); + fileStartTimes.remove(jsonFile); } } @@ -150,7 +180,6 @@ 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(); - try (final Response response = client.newCall(request).execute()) { if (!response.isSuccessful()) { log.error("Unexpected response code from rejected-tx endpoint: {}", response.code()); @@ -189,9 +218,16 @@ private boolean sendJsonRpcCall(final String jsonContent) { } private Path saveJsonToFile(final String jsonContent) throws IOException { - final String fileName = "rpc_" + System.currentTimeMillis() + ".json"; - final Path filePath = rejTxRpcDirectory.resolve(fileName); - Files.write(filePath, jsonContent.getBytes()); - return filePath; + long timestamp = System.currentTimeMillis(); + for (int attempt = 0; attempt < 100; attempt++) { + final String fileName = String.format("rpc_%d_%d.json", timestamp, attempt); + final Path filePath = rejTxRpcDirectory.resolve(fileName); + try { + return Files.writeString(filePath, jsonContent, StandardOpenOption.CREATE_NEW); + } catch (final FileAlreadyExistsException e) { + log.trace("File already exists {}, retrying.", filePath); + } + } + throw new IOException("Failed to save JSON content after 100 attempts"); } } 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 ea4e7a2e..efd57c6e 100644 --- a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java @@ -17,19 +17,26 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; 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 org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; +import java.io.IOException; import java.net.URI; +import java.nio.file.Files; import java.nio.file.Path; import java.time.Instant; +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 org.apache.tuweni.bytes.Bytes; import org.hyperledger.besu.datatypes.PendingTransaction; @@ -49,13 +56,18 @@ class JsonRpcManagerTest { @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) { + // 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())); jsonRpcManager.start(); } @@ -67,12 +79,6 @@ void cleanup() { @Test void rejectedTxIsReported() throws InterruptedException { - // mock stubbing - when(pendingBlockHeader.getNumber()).thenReturn(1L); - when(pendingTransaction.getTransaction()).thenReturn(transaction); - final Bytes randomEncodedBytes = Bytes.random(32); - when(transaction.encoded()).thenReturn(randomEncodedBytes); - // json-rpc stubbing stubFor( post(urlEqualTo("/")) @@ -98,6 +104,161 @@ void rejectedTxIsReported() throws InterruptedException { Thread.sleep(1000); // assert that the expected json-rpc request was sent to WireMock - verify(postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + verify(exactly(1), postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + } + + @Test + void firstCallErrorSecondCallSuccessScenario() throws InterruptedException, IOException { + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Internal error\"},\"id\":1}")) + .willSetStateTo("Second Call")); + + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs("Second Call") + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + + // Prepare test data + final TestTransactionEvaluationContext context = + new TestTransactionEvaluationContext(pendingTransaction) + .setPendingBlockHeader(pendingBlockHeader); + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); + + // Generate JSON-RPC call + final String jsonRpcCall = + JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + + // Submit the call, the scheduler will retry the failed call + jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); + + // Sleep to allow async processing + Thread.sleep(2000); // Increased sleep time to allow for two calls + + // Verify that two requests were made + verify(exactly(2), postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + + // Verify that the JSON file no longer exists in the directory (as the second call was + // successful) + Path rejTxRpcDir = tempDataDir.resolve("rej_tx_rpc"); + try (Stream files = Files.list(rejTxRpcDir)) { + long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); + assertThat(fileCount).isEqualTo(0); + } + } + + @Test + void serverRespondingWithErrorScenario() throws InterruptedException, IOException { + // Stub for error response + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Internal error\"},\"id\":1}"))); + + // Prepare test data + final TestTransactionEvaluationContext context = + new TestTransactionEvaluationContext(pendingTransaction) + .setPendingBlockHeader(pendingBlockHeader); + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); + + // Generate JSON-RPC call + final String jsonRpcCall = + JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + + // Submit the call + jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); + + // Sleep to allow async processing + Thread.sleep(1000); + + // Verify that the request was made + verify(exactly(1), 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"); + 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(); + } + } + + @Test + void firstTwoCallsErrorThenLastCallSuccessScenario() throws InterruptedException, IOException { + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs(Scenario.STARTED) + .willReturn(aResponse().withFault(Fault.MALFORMED_RESPONSE_CHUNK)) + .willSetStateTo("Second Call")); + + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs("Second Call") + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"error\":{\"code\":-32000,\"message\":\"Internal error\"},\"id\":1}")) + .willSetStateTo("Third Call")); + + stubFor( + post(urlEqualTo("/")) + .inScenario("RPC Calls") + .whenScenarioStateIs("Third Call") + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + + // Prepare test data + final TestTransactionEvaluationContext context = + new TestTransactionEvaluationContext(pendingTransaction) + .setPendingBlockHeader(pendingBlockHeader); + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); + final Instant timestamp = Instant.now(); + + // Generate JSON-RPC call + final String jsonRpcCall = + JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + + // Submit the call, the scheduler will retry the failed calls + jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); + + // Sleep to allow async processing + Thread.sleep(6000); // Increased sleep time to allow for three calls + + // Verify that two requests were made + verify(exactly(3), postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + + // Verify that the JSON file no longer exists in the directory (as the second call was + // successful) + Path rejTxRpcDir = tempDataDir.resolve("rej_tx_rpc"); + try (Stream files = Files.list(rejTxRpcDir)) { + long fileCount = files.filter(path -> path.toString().endsWith(".json")).count(); + assertThat(fileCount).isEqualTo(0); + } } } From 386008a7ef0dd770c91f2d420abc1f75e042693c Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 09:25:38 +1000 Subject: [PATCH 16/26] review suggestions --- .../java/net/consensys/linea/jsonrpc/JsonRpcManager.java | 4 ++-- .../consensys/linea/jsonrpc/JsonRpcRequestBuilder.java | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) 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 0db2203f..c9b38605 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -80,7 +80,7 @@ public JsonRpcManager start() { Files.createDirectories(rejTxRpcDirectory); // Load existing JSON files - loadExistingJsonFiles(); + processExistingJsonFiles(); return this; } catch (IOException e) { log.error("Failed to create or access rej-tx-rpc directory", e); @@ -109,7 +109,7 @@ public void submitNewJsonRpcCall(final String jsonContent) { } } - private void loadExistingJsonFiles() { + private void processExistingJsonFiles() { try { final TreeSet sortedFiles = new TreeSet<>(Comparator.comparing(Path::getFileName)); 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 d6cd0abd..26906eea 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcRequestBuilder.java @@ -16,6 +16,7 @@ package net.consensys.linea.jsonrpc; import java.time.Instant; +import java.util.concurrent.atomic.AtomicLong; import com.google.gson.JsonObject; import org.hyperledger.besu.datatypes.PendingTransaction; @@ -25,6 +26,8 @@ /** Helper class to build JSON-RPC requests for rejected transactions. */ public class JsonRpcRequestBuilder { + private static final AtomicLong idCounter = new AtomicLong(1); + /** * * @@ -52,10 +55,6 @@ public static String buildRejectedTxRequest( final TransactionEvaluationContext evaluationContext, final TransactionSelectionResult transactionSelectionResult, final Instant timestamp) { - // if (!transactionSelectionResult.discard()) { - // return; - // } - final PendingTransaction pendingTransaction = evaluationContext.getPendingTransaction(); final ProcessableBlockHeader pendingBlockHeader = evaluationContext.getPendingBlockHeader(); @@ -72,7 +71,7 @@ public static String buildRejectedTxRequest( request.addProperty("jsonrpc", "2.0"); request.addProperty("method", "linea_saveRejectedTransactionV1"); request.add("params", params); - request.addProperty("id", 1); + request.addProperty("id", idCounter.getAndIncrement()); return request.toString(); } } From f32b3797d0a33860ddf1b64119e50fc1ee5803ea Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 09:42:57 +1000 Subject: [PATCH 17/26] Changing json-rpc ExecutorService to use virtual threads --- .../java/net/consensys/linea/jsonrpc/JsonRpcManager.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 c9b38605..036d8ae9 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -45,8 +45,6 @@ @Slf4j public class JsonRpcManager { private static final long INITIAL_RETRY_DELAY = 1000L; - private static final int MAX_THREADS = - Math.min(32, Runtime.getRuntime().availableProcessors() * 2); private static final long MAX_RETRY_DURATION = TimeUnit.HOURS.toMillis(2); private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); @@ -69,8 +67,8 @@ public class JsonRpcManager { public JsonRpcManager(final Path besuDataDir, final URI rejectedTxEndpoint) { this.rejTxRpcDirectory = besuDataDir.resolve("rej_tx_rpc"); this.rejectedTxEndpoint = rejectedTxEndpoint; - this.executorService = Executors.newFixedThreadPool(MAX_THREADS); - this.retrySchedulerService = Executors.newScheduledThreadPool(1); + this.executorService = Executors.newVirtualThreadPerTaskExecutor(); + this.retrySchedulerService = Executors.newSingleThreadScheduledExecutor(); } /** Load existing JSON-RPC and submit them. */ From 140cb48027545ce3554f6fa396272d90434357ce Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 10:01:31 +1000 Subject: [PATCH 18/26] Adding comments to saveJsonToFile method --- .../linea/jsonrpc/JsonRpcManager.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) 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 036d8ae9..52697829 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -215,10 +215,21 @@ private boolean sendJsonRpcCall(final String jsonContent) { } } + /** + * Saves the JSON content to a file in the rej-tx-rpc directory. The file name is generated based + * on a timestamp and an attempt number to allow sorted loading in case of restart. + * + * @param jsonContent The JSON content to save + * @return The path to the saved file + * @throws IOException If an I/O error occurs or failed to save JSON content after 100 attempts + * due to collision + */ private Path saveJsonToFile(final String jsonContent) throws IOException { - long timestamp = System.currentTimeMillis(); + final String timestamp = generateTimestampWithNanos(); + // there is a very low chance of collision that multiple rejected tx being notified within same + // current timestamp, but we'll retry up to 100 times for (int attempt = 0; attempt < 100; attempt++) { - final String fileName = String.format("rpc_%d_%d.json", timestamp, attempt); + final String fileName = String.format("rpc_%s_%d.json", timestamp, attempt); final Path filePath = rejTxRpcDirectory.resolve(fileName); try { return Files.writeString(filePath, jsonContent, StandardOpenOption.CREATE_NEW); @@ -228,4 +239,12 @@ private Path saveJsonToFile(final String jsonContent) throws IOException { } throw new IOException("Failed to save JSON content after 100 attempts"); } + + private static String generateTimestampWithNanos() { + long millis = System.currentTimeMillis(); + long nanos = System.nanoTime(); + long millisPart = millis % 1000; + long nanosPart = nanos % 1_000_000; + return String.format("%d%03d%06d", millis / 1000, millisPart, nanosPart); + } } From 80de637a8db8f39001af796c7f207c2e69889983 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 10:17:58 +1000 Subject: [PATCH 19/26] Use high-precision timestamp and a UUID to ensure uniqueness --- .../linea/jsonrpc/JsonRpcManager.java | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) 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 52697829..181f7180 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -26,6 +26,7 @@ import java.util.Comparator; import java.util.Map; import java.util.TreeSet; +import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -216,28 +217,29 @@ private boolean sendJsonRpcCall(final String jsonContent) { } /** - * Saves the JSON content to a file in the rej-tx-rpc directory. The file name is generated based - * on a timestamp and an attempt number to allow sorted loading in case of restart. + * Saves the given JSON content to a file in the rejected transactions RPC directory. The filename + * is generated using a high-precision timestamp and a UUID to ensure uniqueness. * - * @param jsonContent The JSON content to save - * @return The path to the saved file - * @throws IOException If an I/O error occurs or failed to save JSON content after 100 attempts - * due to collision + *

The file naming format is: rpc_[timestamp]_[uuid].json

+ * + * @param jsonContent The JSON string to be written to the file. + * @return The Path object representing the newly created file. + * @throws IOException If an I/O error occurs while writing the file, including unexpected file + * collisions. */ private Path saveJsonToFile(final String jsonContent) throws IOException { final String timestamp = generateTimestampWithNanos(); - // there is a very low chance of collision that multiple rejected tx being notified within same - // current timestamp, but we'll retry up to 100 times - for (int attempt = 0; attempt < 100; attempt++) { - final String fileName = String.format("rpc_%s_%d.json", timestamp, attempt); - final Path filePath = rejTxRpcDirectory.resolve(fileName); - try { - return Files.writeString(filePath, jsonContent, StandardOpenOption.CREATE_NEW); - } catch (final FileAlreadyExistsException e) { - log.trace("File already exists {}, retrying.", filePath); - } + final String uuid = UUID.randomUUID().toString(); + final String fileName = String.format("rpc_%s_%s.json", timestamp, uuid); + final Path filePath = rejTxRpcDirectory.resolve(fileName); + + try { + return Files.writeString(filePath, jsonContent, StandardOpenOption.CREATE_NEW); + } catch (final FileAlreadyExistsException e) { + // This should never happen with UUID, but just in case + log.warn("Unexpected file collision occurred: {}", filePath); + throw new IOException("Unexpected file name collision", e); } - throw new IOException("Failed to save JSON content after 100 attempts"); } private static String generateTimestampWithNanos() { From 3122a927f836a03bf21cb6679ef6efcc7f5531fb Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 10:36:22 +1000 Subject: [PATCH 20/26] Use Duration and Instant instead of long --- .../linea/jsonrpc/JsonRpcManager.java | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) 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 181f7180..a597bd02 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -23,6 +23,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.time.Duration; +import java.time.Instant; import java.util.Comparator; import java.util.Map; import java.util.TreeSet; @@ -45,13 +47,13 @@ /** This class is responsible for managing JSON-RPC requests for reporting rejected transactions */ @Slf4j public class JsonRpcManager { - private static final long INITIAL_RETRY_DELAY = 1000L; - private static final long MAX_RETRY_DURATION = TimeUnit.HOURS.toMillis(2); + 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"); private final OkHttpClient client = new OkHttpClient(); private final ObjectMapper objectMapper = new ObjectMapper(); - private final Map fileStartTimes = new ConcurrentHashMap<>(); + private final Map fileStartTimes = new ConcurrentHashMap<>(); private final Path rejTxRpcDirectory; private final URI rejectedTxEndpoint; @@ -101,8 +103,8 @@ public void shutdown() { public void submitNewJsonRpcCall(final String jsonContent) { try { final Path jsonFile = saveJsonToFile(jsonContent); - fileStartTimes.put(jsonFile, System.currentTimeMillis()); - submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY); + fileStartTimes.put(jsonFile, Instant.now()); + submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY_DURATION); } catch (final IOException e) { log.error("Failed to save JSON content", e); } @@ -120,8 +122,8 @@ private void processExistingJsonFiles() { } for (Path path : sortedFiles) { - fileStartTimes.put(path, System.currentTimeMillis()); - submitJsonRpcCall(path, INITIAL_RETRY_DELAY); + fileStartTimes.put(path, Instant.now()); + submitJsonRpcCall(path, INITIAL_RETRY_DELAY_DURATION); } log.info("Loaded {} existing JSON files for rej-tx reporting", sortedFiles.size()); @@ -130,7 +132,7 @@ private void processExistingJsonFiles() { } } - private void submitJsonRpcCall(final Path jsonFile, final long nextDelay) { + private void submitJsonRpcCall(final Path jsonFile, final Duration nextDelay) { executorService.submit( () -> { if (!Files.exists(jsonFile)) { @@ -156,22 +158,29 @@ private void submitJsonRpcCall(final Path jsonFile, final long nextDelay) { }); } - private void scheduleRetry(final Path jsonFile, final long currentDelay) { - final Long startTime = fileStartTimes.get(jsonFile); + private void scheduleRetry(final Path jsonFile, final Duration currentDelay) { + final Instant startTime = fileStartTimes.get(jsonFile); if (startTime == null) { log.debug("No start time found for file: {}. Skipping retry.", jsonFile); return; } - // check if we're still within the maximum retry duration - if (System.currentTimeMillis() - startTime < MAX_RETRY_DURATION) { - // schedule a retry with exponential backoff - long nextDelay = Math.min(currentDelay * 2, TimeUnit.MINUTES.toMillis(1)); // Cap at 1 minute + // Check if we're still within the maximum retry duration + if (Duration.between(startTime, Instant.now()).compareTo(MAX_RETRY_DURATION) < 0) { + // Calculate next delay with exponential backoff, capped at 1 minute + final Duration nextDelay = + Duration.ofMillis( + Math.min(currentDelay.multipliedBy(2).toMillis(), Duration.ofMinutes(1).toMillis())); + + // Schedule a retry retrySchedulerService.schedule( - () -> submitJsonRpcCall(jsonFile, nextDelay), currentDelay, TimeUnit.MILLISECONDS); + () -> submitJsonRpcCall(jsonFile, nextDelay), + currentDelay.toMillis(), + TimeUnit.MILLISECONDS); } else { log.error("Exceeded maximum retry duration for rej-tx json-rpc file: {}", jsonFile); fileStartTimes.remove(jsonFile); + // TODO (review suggestion) : Log that notification is discarded and not sent to endpoint } } @@ -220,7 +229,7 @@ private boolean sendJsonRpcCall(final String jsonContent) { * Saves the given JSON content to a file in the rejected transactions RPC directory. The filename * is generated using a high-precision timestamp and a UUID to ensure uniqueness. * - *

The file naming format is: rpc_[timestamp]_[uuid].json

+ *

The file naming format is: rpc_[timestamp]_[uuid].json * * @param jsonContent The JSON string to be written to the file. * @return The Path object representing the newly created file. From 9199fddf5cd1b00e10a27477fab21f16452a0e7b Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 17:20:00 +1000 Subject: [PATCH 21/26] Updating logs entry in submitJsonRpcCall --- .../consensys/linea/jsonrpc/JsonRpcManager.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 a597bd02..24aeb60e 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -136,7 +136,7 @@ private void submitJsonRpcCall(final Path jsonFile, final Duration nextDelay) { executorService.submit( () -> { if (!Files.exists(jsonFile)) { - log.debug("json-rpc file no longer exists, skipping processing: {}", jsonFile); + log.debug("JSON-RPC file {} no longer exists, skipping processing.", jsonFile); fileStartTimes.remove(jsonFile); return; } @@ -148,11 +148,16 @@ private void submitJsonRpcCall(final Path jsonFile, final Duration nextDelay) { fileStartTimes.remove(jsonFile); } else { log.error( - "Failed to send JSON-RPC call to {}, retrying: {}", rejectedTxEndpoint, jsonFile); + "Failed to send JSON-RPC file {} to {}, Scheduling retry ...", + jsonFile, + rejectedTxEndpoint); scheduleRetry(jsonFile, nextDelay); } } catch (final Exception e) { - log.error("Failed to process json-rpc file: {}", jsonFile, e); + log.error( + "Failed to process JSON-RPC file {} due to unexpected error: {}. Scheduling retry ...", + jsonFile, + e.getMessage()); scheduleRetry(jsonFile, nextDelay); } }); @@ -161,7 +166,7 @@ private void submitJsonRpcCall(final Path jsonFile, final Duration nextDelay) { private void scheduleRetry(final Path jsonFile, final Duration currentDelay) { final Instant startTime = fileStartTimes.get(jsonFile); if (startTime == null) { - log.debug("No start time found for file: {}. Skipping retry.", jsonFile); + log.debug("No start time found for JSON-RPC file: {}. Skipping retry.", jsonFile); return; } @@ -181,6 +186,7 @@ private void scheduleRetry(final Path jsonFile, final Duration currentDelay) { log.error("Exceeded maximum retry duration for rej-tx json-rpc file: {}", jsonFile); fileStartTimes.remove(jsonFile); // TODO (review suggestion) : Log that notification is discarded and not sent to endpoint + // TODO : Consider moving the file to `failed` directory for manual inspection } } From 45363ccbc10c174c0e095b915c370b312cc11bfb Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 17:45:12 +1000 Subject: [PATCH 22/26] Update logs message. Move discarded json-rpc calls to separate directory for manual inspection --- .../linea/jsonrpc/JsonRpcManager.java | 49 ++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) 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 24aeb60e..52187d03 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -22,6 +22,7 @@ import java.nio.file.FileAlreadyExistsException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.time.Duration; import java.time.Instant; @@ -77,14 +78,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 directory if it doesn't exist - Files.createDirectories(rejTxRpcDirectory); + // Create the rej_tx_rpc/discarded directories if it doesn't exist + Files.createDirectories(rejTxRpcDirectory.resolve("discarded")); // Load existing JSON files processExistingJsonFiles(); return this; - } catch (IOException e) { - log.error("Failed to create or access rej-tx-rpc directory", e); + } catch (final IOException e) { + log.error("Failed to create or access directory: {}", rejTxRpcDirectory, e); throw new UncheckedIOException(e); } } @@ -101,13 +102,16 @@ public void shutdown() { * @param jsonContent The JSON content to submit */ public void submitNewJsonRpcCall(final String jsonContent) { + final Path jsonFile; try { - final Path jsonFile = saveJsonToFile(jsonContent); - fileStartTimes.put(jsonFile, Instant.now()); - submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY_DURATION); + jsonFile = saveJsonToFile(jsonContent); } catch (final IOException e) { - log.error("Failed to save JSON content", e); + log.error("Failed to save JSON-RPC content", e); + return; } + + fileStartTimes.put(jsonFile, Instant.now()); + submitJsonRpcCall(jsonFile, INITIAL_RETRY_DELAY_DURATION); } private void processExistingJsonFiles() { @@ -121,14 +125,14 @@ private void processExistingJsonFiles() { } } + log.info("Loaded {} existing JSON-RPC files for reporting", sortedFiles.size()); + for (Path path : sortedFiles) { fileStartTimes.put(path, Instant.now()); submitJsonRpcCall(path, INITIAL_RETRY_DELAY_DURATION); } - - log.info("Loaded {} existing JSON files for rej-tx reporting", sortedFiles.size()); } catch (final IOException e) { - log.error("Failed to load existing JSON files", e); + log.error("Failed to load existing JSON-RPC files", e); } } @@ -183,10 +187,21 @@ private void scheduleRetry(final Path jsonFile, final Duration currentDelay) { currentDelay.toMillis(), TimeUnit.MILLISECONDS); } else { - log.error("Exceeded maximum retry duration for rej-tx json-rpc file: {}", jsonFile); - fileStartTimes.remove(jsonFile); - // TODO (review suggestion) : Log that notification is discarded and not sent to endpoint - // TODO : Consider moving the file to `failed` directory for manual inspection + log.error("Exceeded maximum retry duration for JSON-RPC file: {}.", jsonFile); + final Path destination = + rejTxRpcDirectory.resolve("discarded").resolve(jsonFile.getFileName()); + + try { + Files.move(jsonFile, destination, StandardCopyOption.REPLACE_EXISTING); + log.error( + "The JSON-RPC file {} has been moved to: {}. The tx notification has been discarded.", + jsonFile, + destination); + } catch (final IOException e) { + log.error("Failed to move JSON-RPC file to discarded directory: {}", jsonFile, e); + } finally { + fileStartTimes.remove(jsonFile); + } } } @@ -218,7 +233,7 @@ private boolean sendJsonRpcCall(final String jsonContent) { } // Check for result if (jsonNode.has("result")) { - String status = jsonNode.get("result").get("status").asText(); + final String status = jsonNode.get("result").get("status").asText(); log.debug("Rejected-tx JSON-RPC call successful. Status: {}", status); return true; } @@ -252,7 +267,7 @@ private Path saveJsonToFile(final String jsonContent) throws IOException { return Files.writeString(filePath, jsonContent, StandardOpenOption.CREATE_NEW); } catch (final FileAlreadyExistsException e) { // This should never happen with UUID, but just in case - log.warn("Unexpected file collision occurred: {}", filePath); + log.warn("Unexpected JSON-RPC filename collision occurred: {}", filePath); throw new IOException("Unexpected file name collision", e); } } From a784018fa8083b79e10d681a7524a825a7ff4df0 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Wed, 11 Sep 2024 18:33:53 +1000 Subject: [PATCH 23/26] Added unit test for processing json-rpc during start. Also introduced Awaitaility instead of Thread.sleep --- sequencer/build.gradle | 1 + .../linea/jsonrpc/JsonRpcManager.java | 8 +- .../jsonrpc/JsonRpcManagerStartTest.java | 111 ++++++++++++++++++ .../linea/jsonrpc/JsonRpcManagerTest.java | 54 +++++---- 4 files changed, 152 insertions(+), 22 deletions(-) create mode 100644 sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java diff --git a/sequencer/build.gradle b/sequencer/build.gradle index 782b9938..a68b8970 100644 --- a/sequencer/build.gradle +++ b/sequencer/build.gradle @@ -71,6 +71,7 @@ dependencies { testImplementation "${besuArtifactGroup}.internal:rlp" testImplementation "${besuArtifactGroup}.internal:core" testImplementation "${besuArtifactGroup}:plugin-api" + testImplementation "org.awaitility:awaitility" // workaround for bug https://github.com/dnsjava/dnsjava/issues/329, remove when upgraded upstream testImplementation 'dnsjava:dnsjava:3.6.1' 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 52187d03..a0331579 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -38,6 +38,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -104,7 +105,7 @@ public void shutdown() { public void submitNewJsonRpcCall(final String jsonContent) { final Path jsonFile; try { - jsonFile = saveJsonToFile(jsonContent); + jsonFile = saveJsonToDir(jsonContent, rejTxRpcDirectory); } catch (final IOException e) { log.error("Failed to save JSON-RPC content", e); return; @@ -253,11 +254,14 @@ private boolean sendJsonRpcCall(final String jsonContent) { *

The file naming format is: rpc_[timestamp]_[uuid].json * * @param jsonContent The JSON string to be written to the file. + * @param rejTxRpcDirectory The directory where the file should be saved. * @return The Path object representing the newly created file. * @throws IOException If an I/O error occurs while writing the file, including unexpected file * collisions. */ - private Path saveJsonToFile(final String jsonContent) throws IOException { + @VisibleForTesting + static Path saveJsonToDir(final String jsonContent, final Path rejTxRpcDirectory) + throws IOException { final String timestamp = generateTimestampWithNanos(); final String uuid = UUID.randomUUID().toString(); final String fileName = String.format("rpc_%s_%s.json", timestamp, uuid); diff --git a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java new file mode 100644 index 00000000..6119f4b0 --- /dev/null +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java @@ -0,0 +1,111 @@ +/* + * 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.jsonrpc; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.exactly; +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.awaitility.Awaitility.await; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; + +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import net.consensys.linea.sequencer.txselection.selectors.TestTransactionEvaluationContext; +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; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@WireMockTest +@ExtendWith(MockitoExtension.class) +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; + + @BeforeEach + void init(final WireMockRuntimeInfo wmInfo) throws IOException { + // create temp directories + final Path rejectedTxDir = tempDataDir.resolve("rej_tx_rpc"); + Files.createDirectories(rejectedTxDir); + + // 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(pendingTransaction) + .setPendingBlockHeader(pendingBlockHeader); + final TransactionSelectionResult result = TransactionSelectionResult.invalid("test" + i); + final Instant timestamp = Instant.now(); + final String jsonRpcCall = + JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); + + JsonRpcManager.saveJsonToDir(jsonRpcCall, rejectedTxDir); + } + + jsonRpcManager = new JsonRpcManager(tempDataDir, URI.create(wmInfo.getHttpBaseUrl())); + } + + @AfterEach + void cleanup() { + jsonRpcManager.shutdown(); + } + + @Test + void existingJsonRpcFilesAreProcessedOnStart() throws InterruptedException { + stubFor( + post(urlEqualTo("/")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); + // method under test + jsonRpcManager.start(); + + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted(() -> verify(exactly(3), postRequestedFor(urlEqualTo("/")))); + } +} 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 efd57c6e..d736505b 100644 --- a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java @@ -23,7 +23,9 @@ 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.when; import java.io.IOException; @@ -100,11 +102,14 @@ void rejectedTxIsReported() throws InterruptedException { JsonRpcRequestBuilder.buildRejectedTxRequest(context, result, timestamp); jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); - // sleep a bit to allow async processing - Thread.sleep(1000); - - // assert that the expected json-rpc request was sent to WireMock - verify(exactly(1), postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted( + () -> + verify( + exactly(1), + postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); } @Test @@ -146,11 +151,14 @@ void firstCallErrorSecondCallSuccessScenario() throws InterruptedException, IOEx // Submit the call, the scheduler will retry the failed call jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); - // Sleep to allow async processing - Thread.sleep(2000); // Increased sleep time to allow for two calls - - // Verify that two requests were made - verify(exactly(2), postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted( + () -> + verify( + exactly(2), + postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); // Verify that the JSON file no longer exists in the directory (as the second call was // successful) @@ -187,11 +195,14 @@ void serverRespondingWithErrorScenario() throws InterruptedException, IOExceptio // Submit the call jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); - // Sleep to allow async processing - Thread.sleep(1000); - - // Verify that the request was made - verify(exactly(1), postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + // Use Awaitility to wait for the condition to be met + await() + .atMost(2, SECONDS) + .untilAsserted( + () -> + verify( + exactly(1), + 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"); @@ -247,11 +258,14 @@ void firstTwoCallsErrorThenLastCallSuccessScenario() throws InterruptedException // Submit the call, the scheduler will retry the failed calls jsonRpcManager.submitNewJsonRpcCall(jsonRpcCall); - // Sleep to allow async processing - Thread.sleep(6000); // Increased sleep time to allow for three calls - - // Verify that two requests were made - verify(exactly(3), postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall))); + // Use Awaitility to wait for the condition to be met + await() + .atMost(6, SECONDS) + .untilAsserted( + () -> + verify( + exactly(3), + postRequestedFor(urlEqualTo("/")).withRequestBody(equalToJson(jsonRpcCall)))); // Verify that the JSON file no longer exists in the directory (as the second call was // successful) From 4cde3219163255c200fc497f1feaee6fe1622f13 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 12 Sep 2024 10:10:44 +1000 Subject: [PATCH 24/26] Use Instant.now to generate timestamp --- .../net/consensys/linea/jsonrpc/JsonRpcManager.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 a0331579..c2a5b530 100644 --- a/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java +++ b/sequencer/src/main/java/net/consensys/linea/jsonrpc/JsonRpcManager.java @@ -276,11 +276,10 @@ static Path saveJsonToDir(final String jsonContent, final Path rejTxRpcDirectory } } - private static String generateTimestampWithNanos() { - long millis = System.currentTimeMillis(); - long nanos = System.nanoTime(); - long millisPart = millis % 1000; - long nanosPart = nanos % 1_000_000; - return String.format("%d%03d%06d", millis / 1000, millisPart, nanosPart); + static String generateTimestampWithNanos() { + final Instant now = Instant.now(); + final long seconds = now.getEpochSecond(); + final int nanos = now.getNano(); + return String.format("%d%09d", seconds, nanos); } } From aabf838abe3d2cf914b22990f297351c9a18a7c7 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 12 Sep 2024 14:27:51 +1000 Subject: [PATCH 25/26] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5bcc41d..13b3f36f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +# Next release +* feat: Report rejected transactions to an external service [#69](https://github.com/Consensys/linea-sequencer/pull/69) + ## 0.3.0-rc2.1 * bump linea-arithmetization version to 0.3.0-rc2 [#62](https://github.com/Consensys/linea-sequencer/pull/62) * bump Linea-Besu version to 24.7-develop-c0029e6 (delivery-28) [#62](https://github.com/Consensys/linea-sequencer/pull/62) From 900ba408117fa0b7aa6610f745432ba09bcf3987 Mon Sep 17 00:00:00 2001 From: Usman Saleem Date: Thu, 12 Sep 2024 21:34:01 +1000 Subject: [PATCH 26/26] post merge --- .../linea/jsonrpc/JsonRpcManagerStartTest.java | 3 +-- .../consensys/linea/jsonrpc/JsonRpcManagerTest.java | 12 ++++-------- .../selectors/TestTransactionEvaluationContext.java | 4 ++-- 3 files changed, 7 insertions(+), 12 deletions(-) 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 6119f4b0..98c6be2a 100644 --- a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerStartTest.java @@ -72,8 +72,7 @@ void init(final WireMockRuntimeInfo wmInfo) throws IOException { // JsonRpcManager.start for (int i = 0; i < 3; i++) { final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingTransaction) - .setPendingBlockHeader(pendingBlockHeader); + new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test" + i); final Instant timestamp = Instant.now(); final String jsonRpcCall = 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 d736505b..d0f567c9 100644 --- a/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java +++ b/sequencer/src/test/java/net/consensys/linea/jsonrpc/JsonRpcManagerTest.java @@ -92,8 +92,7 @@ void rejectedTxIsReported() throws InterruptedException { "{\"jsonrpc\":\"2.0\",\"result\":{ \"status\": \"SAVED\"},\"id\":1}"))); final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingTransaction) - .setPendingBlockHeader(pendingBlockHeader); + new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); @@ -139,8 +138,7 @@ void firstCallErrorSecondCallSuccessScenario() throws InterruptedException, IOEx // Prepare test data final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingTransaction) - .setPendingBlockHeader(pendingBlockHeader); + new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); @@ -183,8 +181,7 @@ void serverRespondingWithErrorScenario() throws InterruptedException, IOExceptio // Prepare test data final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingTransaction) - .setPendingBlockHeader(pendingBlockHeader); + new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); @@ -246,8 +243,7 @@ void firstTwoCallsErrorThenLastCallSuccessScenario() throws InterruptedException // Prepare test data final TestTransactionEvaluationContext context = - new TestTransactionEvaluationContext(pendingTransaction) - .setPendingBlockHeader(pendingBlockHeader); + new TestTransactionEvaluationContext(pendingBlockHeader, pendingTransaction); final TransactionSelectionResult result = TransactionSelectionResult.invalid("test"); final Instant timestamp = Instant.now(); diff --git a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java index a08533ce..45782244 100644 --- a/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java +++ b/sequencer/src/test/java/net/consensys/linea/sequencer/txselection/selectors/TestTransactionEvaluationContext.java @@ -20,7 +20,8 @@ import org.hyperledger.besu.plugin.data.ProcessableBlockHeader; import org.hyperledger.besu.plugin.services.txselection.TransactionEvaluationContext; -class TestTransactionEvaluationContext implements TransactionEvaluationContext { +public class TestTransactionEvaluationContext + implements TransactionEvaluationContext { private ProcessableBlockHeader processableBlockHeader; private PendingTransaction pendingTransaction; private Wei transactionGasPrice; @@ -48,7 +49,6 @@ public ProcessableBlockHeader getPendingBlockHeader() { return processableBlockHeader; } - @Override public PendingTransaction getPendingTransaction() { return pendingTransaction;