From 5810882aede5279fe8e522d8c85f116fe587f8e9 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Mon, 9 Mar 2026 21:17:36 +0100 Subject: [PATCH] Implement `txpool_status` RPC method Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../besu/ethereum/api/jsonrpc/RpcMethod.java | 1 + .../internal/methods/TxPoolStatus.java | 47 +++++++ .../results/TransactionPoolStatusResult.java | 38 ++++++ .../jsonrpc/methods/TxPoolJsonRpcMethods.java | 4 +- .../internal/methods/TxPoolStatusTest.java | 115 ++++++++++++++++++ .../DisabledPendingTransactions.java | 5 + .../eth/transactions/PendingTransactions.java | 4 + .../eth/transactions/TransactionPool.java | 4 + .../AbstractSequentialTransactionsLayer.java | 8 ++ .../eth/transactions/layered/EndLayer.java | 6 + .../layered/LayeredPendingTransactions.java | 5 + .../layered/SparseTransactions.java | 18 +++ .../layered/TransactionsLayer.java | 3 + .../AbstractPendingTransactionsSorter.java | 14 +++ .../sorter/PendingTransactionsForSender.java | 22 ++-- .../LayeredPendingTransactionsTest.java | 73 +++++++++++ .../AbstractPendingTransactionsTestBase.java | 72 +++++++++++ 18 files changed, 428 insertions(+), 12 deletions(-) create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java create mode 100644 ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java create mode 100644 ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 49e3b77e978..b8d81f8ea1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - Plugin API: Allow the registration of multiple PluginTransactionPoolValidatorFactory [#9964](https://github.com/hyperledger/besu/pull/9964) - Add `-Pcases` case name filtering to JMH benchmark suite [#9982](https://github.com/hyperledger/besu/pull/9982) - Use JDK SHA-256 provider to leverage hardware SHA-NI instructions instead of BouncyCastle [#9924](https://github.com/hyperledger/besu/pull/9924) +- Implement `txpool_status` RPC method [#10002](https://github.com/hyperledger/besu/pull/10002) - Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists - Limit pooled tx requests by size and remove pre-eth/68 transaction announcement support [#9990](https://github.com/besu-eth/besu/pull/9990) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java index 679f1be52b1..e8f6bc7ce9e 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java @@ -168,6 +168,7 @@ public enum RpcMethod { TX_POOL_BESU_STATISTICS("txpool_besuStatistics"), TX_POOL_BESU_TRANSACTIONS("txpool_besuTransactions"), TX_POOL_BESU_PENDING_TRANSACTIONS("txpool_besuPendingTransactions"), + TX_POOL_STATUS("txpool_status"), WEB3_CLIENT_VERSION("web3_clientVersion"), WEB3_SHA3("web3_sha3"), PLUGINS_RELOAD_CONFIG("plugins_reloadPluginConfig"), diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java new file mode 100644 index 00000000000..0ff46a95359 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatus.java @@ -0,0 +1,47 @@ +/* + * Copyright contributors to Besu. + * + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; + +import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPoolStatusResult; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; + +public class TxPoolStatus implements JsonRpcMethod { + + private final TransactionPool transactionPool; + + public TxPoolStatus(final TransactionPool transactionPool) { + this.transactionPool = transactionPool; + } + + @Override + public String getName() { + return RpcMethod.TX_POOL_STATUS.getMethodName(); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), status()); + } + + private TransactionPoolStatusResult status() { + final PendingTransactions.Status status = transactionPool.getStatus(); + return new TransactionPoolStatusResult(status.pendingCount(), status.queuedCount()); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java new file mode 100644 index 00000000000..d491a1b2fbd --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolStatusResult.java @@ -0,0 +1,38 @@ +/* + * Copyright contributors to Besu. + * + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.internal.results; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class TransactionPoolStatusResult { + + private final long pending; + private final long queued; + + public TransactionPoolStatusResult(final long pending, final long queued) { + this.pending = pending; + this.queued = queued; + } + + @JsonGetter + public String getPending() { + return Quantity.create(pending); + } + + @JsonGetter + public String getQueued() { + return Quantity.create(queued); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java index a4ea64101b4..5a62028fce7 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java @@ -19,6 +19,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuPendingTransactions; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuStatistics; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuTransactions; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolStatus; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; import java.util.Map; @@ -41,6 +42,7 @@ protected Map create() { return mapOf( new TxPoolBesuTransactions(transactionPool), new TxPoolBesuPendingTransactions(transactionPool), - new TxPoolBesuStatistics(transactionPool)); + new TxPoolBesuStatistics(transactionPool), + new TxPoolStatus(transactionPool)); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java new file mode 100644 index 00000000000..68f4862306f --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolStatusTest.java @@ -0,0 +1,115 @@ +/* + * Copyright contributors to Besu. + * + * 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 org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPoolStatusResult; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class TxPoolStatusTest { + + @Mock private TransactionPool transactionPool; + private TxPoolStatus method; + private static final String JSON_RPC_VERSION = "2.0"; + private static final String TXPOOL_STATUS_METHOD = "txpool_status"; + + @BeforeEach + public void setUp() { + method = new TxPoolStatus(transactionPool); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(TXPOOL_STATUS_METHOD); + } + + @Test + public void shouldReturnZeroCountsWhenPoolIsEmpty() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(0, 0)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x0"); + assertThat(result.getQueued()).isEqualTo("0x0"); + } + + @Test + public void shouldReturnCorrectCountsWithPendingAndQueuedTransactions() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(10, 7)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0xa"); + assertThat(result.getQueued()).isEqualTo("0x7"); + } + + @Test + public void shouldReturnCorrectCountsWithOnlyPendingTransactions() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(5, 0)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x5"); + assertThat(result.getQueued()).isEqualTo("0x0"); + } + + @Test + public void shouldReturnCorrectCountsWithOnlyQueuedTransactions() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(0, 3)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x0"); + assertThat(result.getQueued()).isEqualTo("0x3"); + } + + @Test + public void shouldReturnHexEncodedLargeValues() { + when(transactionPool.getStatus()).thenReturn(new PendingTransactions.Status(256, 4096)); + + final JsonRpcRequestContext request = buildRequest(); + final JsonRpcSuccessResponse response = (JsonRpcSuccessResponse) method.response(request); + final TransactionPoolStatusResult result = (TransactionPoolStatusResult) response.getResult(); + + assertThat(result.getPending()).isEqualTo("0x100"); + assertThat(result.getQueued()).isEqualTo("0x1000"); + } + + private JsonRpcRequestContext buildRequest() { + return new JsonRpcRequestContext( + new JsonRpcRequest(JSON_RPC_VERSION, TXPOOL_STATUS_METHOD, new Object[] {})); + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java index a0610f679d8..bc7c6db516c 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java @@ -117,6 +117,11 @@ public String logStats() { return "Disabled"; } + @Override + public Status getStatus() { + return new Status(0, 0); + } + @Override public Optional restoreBlob(final Transaction transaction) { return Optional.empty(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java index 03bacc92e65..13642cb70dd 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java @@ -73,6 +73,8 @@ void manageBlockAdded( String logStats(); + Status getStatus(); + Optional restoreBlob(Transaction transaction); @FunctionalInterface @@ -80,4 +82,6 @@ interface PendingTransactionsSelector { Map evaluatePendingTransactions( List candidatePendingTransactions); } + + record Status(long pendingCount, long queuedCount) {} } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java index 30a6c2efa21..bceeb502e16 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java @@ -576,6 +576,10 @@ public String logStats() { return pendingTransactions.logStats(); } + public PendingTransactions.Status getStatus() { + return pendingTransactions.getStatus(); + } + @VisibleForTesting Class pendingTransactionsImplementation() { return pendingTransactions.getClass(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java index cf7195e690f..ca71ee998db 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractSequentialTransactionsLayer.java @@ -22,6 +22,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.transactions.BlobCache; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason; @@ -123,6 +124,13 @@ public OptionalLong getCurrentNonceFor(final Address sender) { return nextLayer.getCurrentNonceFor(sender); } + @Override + public PendingTransactions.Status getStatus() { + final PendingTransactions.Status nextLayerStatus = nextLayer.getStatus(); + return new PendingTransactions.Status( + nextLayerStatus.pendingCount() + pendingTransactions.size(), nextLayerStatus.queuedCount()); + } + @Override protected void internalNotifyAdded( final NavigableMap senderTxs, diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java index da1731b4256..e0ba12303c5 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/EndLayer.java @@ -24,6 +24,7 @@ import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason; @@ -183,6 +184,11 @@ public String logStats() { return "Dropped: " + droppedCount; } + @Override + public PendingTransactions.Status getStatus() { + return new PendingTransactions.Status(0, 0); + } + @Override public String logSender(final Address sender) { return ""; diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java index 5baf4a5672c..e514c35a8bb 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactions.java @@ -433,6 +433,11 @@ public synchronized String logStats() { return prioritizedTransactions.logStats(); } + @Override + public synchronized Status getStatus() { + return prioritizedTransactions.getStatus(); + } + @Override public Optional restoreBlob(final Transaction transaction) { return prioritizedTransactions.getBlobCache().restoreBlob(transaction); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java index 59d1c6de36a..78492749566 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SparseTransactions.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.eth.transactions.BlobCache; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; @@ -216,6 +217,23 @@ public List promote( return promotedTxs; } + @Override + public PendingTransactions.Status getStatus() { + final PendingTransactions.Status nextLayerStatus = nextLayer.getStatus(); + long pendingCount = nextLayerStatus.pendingCount(); + long queueCount = nextLayerStatus.queuedCount(); + for (final Map.Entry entry : gapBySender.entrySet()) { + final Address sender = entry.getKey(); + final int gap = entry.getValue(); + if (gap == 0) { + pendingCount += txsBySender.get(sender).size(); + } else { + queueCount += txsBySender.get(sender).size(); + } + } + return new PendingTransactions.Status(pendingCount, queueCount); + } + private NavigableMap getSequentialSubset( final NavigableMap senderTxs) { long lastSequentialNonce = senderTxs.firstKey(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java index 670ef4fd840..e37c5920579 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/TransactionsLayer.java @@ -21,6 +21,7 @@ import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.layered.LayeredRemovalReason.PoolRemovalReason; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; @@ -127,5 +128,7 @@ List promote( String logStats(); + PendingTransactions.Status getStatus(); + String logSender(Address sender); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java index 7d8f61886a2..9c4b4a71a40 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsSorter.java @@ -507,6 +507,20 @@ public String logStats() { return "Pending " + pendingTransactions.size(); } + @Override + public Status getStatus() { + long pendingCount = 0; + long queuedCount = 0; + synchronized (lock) { + for (final PendingTransactionsForSender pendingTxsForSender : transactionsBySender.values()) { + final Status accountStatus = pendingTxsForSender.getStatus(); + pendingCount += accountStatus.pendingCount(); + queuedCount += accountStatus.queuedCount(); + } + } + return new Status(pendingCount, queuedCount); + } + @Override public String toTraceLog() { synchronized (lock) { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java index 3d42b2c3d7a..77b0fa82bb9 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/PendingTransactionsForSender.java @@ -15,10 +15,10 @@ package org.hyperledger.besu.ethereum.eth.transactions.sorter; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.evm.account.Account; import java.util.List; -import java.util.Map; import java.util.NavigableMap; import java.util.NoSuchElementException; import java.util.Optional; @@ -70,12 +70,16 @@ public void updateSenderAccount(final Optional maybeSenderAccount) { this.maybeSenderAccount = maybeSenderAccount; } - public long getSenderAccountNonce() { - return maybeSenderAccount.map(Account::getNonce).orElse(0L); - } - - public Optional getSenderAccount() { - return maybeSenderAccount; + PendingTransactions.Status getStatus() { + synchronized (pendingTransactions) { + if (nextGap.isPresent()) { + final long gap = nextGap.getAsLong(); + final long accountNonce = maybeSenderAccount.map(Account::getNonce).orElse(0L); + final long nonGapped = Math.max(0, gap - accountNonce); + return new PendingTransactions.Status(nonGapped, transactionCount() - nonGapped); + } + return new PendingTransactions.Status(transactionCount(), 0); + } } private void findGap() { @@ -109,10 +113,6 @@ public OptionalLong maybeNextNonce() { } } - public Optional maybeLastPendingTransaction() { - return Optional.ofNullable(pendingTransactions.lastEntry()).map(Map.Entry::getValue); - } - public int transactionCount() { return pendingTransactions.size(); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java index 96f1508d1f8..e8d8218ca33 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayeredPendingTransactionsTest.java @@ -46,6 +46,7 @@ import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionAddedListener; import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactionDroppedListener; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransactions; import org.hyperledger.besu.ethereum.eth.transactions.RemovalReason; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolMetrics; @@ -821,6 +822,78 @@ public void returnsCorrectNextNonceWhenAddedTransactionsHaveGaps() { .hasValue(1); } + @Test + public void shouldReturnZeroStatusWhenPoolIsEmpty() { + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isZero(); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountAllTransactionsAsPendingWhenNoncesAreSequential() { + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(1, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountTransactionsBeyondNonceGapAsQueued() { + // nonce 0 lands in prioritized/ready (pending), nonce 2 lands in sparse with gap=1 (queued) + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(1); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldAggregatePendingAndQueuedAcrossMultipleSenders() { + // SENDER1: nonces 0, 1 — sequential, all pending + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(1, KEYS1)), Optional.empty()); + // SENDER2: nonces 0, 2 — gap at 1: 1 pending, 1 queued + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS2)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS2)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldMoveTxFromQueuedToPendingWhenGapIsFilled() { + // start with a gap: nonce 0 pending, nonce 2 queued + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(0, KEYS1)), Optional.empty()); + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(2, KEYS1)), Optional.empty()); + + assertThat(pendingTransactions.getStatus().pendingCount()).isEqualTo(1); + assertThat(pendingTransactions.getStatus().queuedCount()).isEqualTo(1); + + // fill the gap: nonce 1 arrives, nonce 2 is promoted from sparse to ready/prioritized + pendingTransactions.addTransaction( + createRemotePendingTransaction(createTransaction(1, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = pendingTransactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isZero(); + } + private TransactionAndAccount[] populateCache(final int numTxs, final long startingNonce) { return populateCache(numTxs, KEYS1, startingNonce, OptionalLong.empty()); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java index 80046f69074..e9f32cd1772 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/sorter/AbstractPendingTransactionsTestBase.java @@ -625,6 +625,78 @@ public void shouldNotIncreasePriorityOfTransactionsBecauseOfNonceOrder() { }); } + @Test + public void shouldReturnZeroStatusWhenPoolIsEmpty() { + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isZero(); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountAllTransactionsAsPendingWhenNonceIsSequential() { + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(1, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isZero(); + } + + @Test + public void shouldCountTransactionsBeyondNonceGapAsQueued() { + // nonce 0 is pending, nonce 2 is queued (gap at 1) + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS1)), Optional.empty()); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(1); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldAggregatePendingAndQueuedAcrossMultipleSenders() { + // SENDER1: nonces 0, 1 — sequential, all pending + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS1)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(1, KEYS1)), Optional.empty()); + // SENDER2: nonces 0, 2 — gap at 1: 1 pending, 1 queued + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(0, KEYS2)), Optional.empty()); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS2)), Optional.empty()); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(3); + assertThat(status.queuedCount()).isEqualTo(1); + } + + @Test + public void shouldUseAccountNonceToComputePendingCountWhenGapIsPresent() { + // account nonce is 2, txs at 2, 3, 5 — gap at 4: 2 pending (nonces 2,3), 1 queued (nonce 5) + final Account sender = mock(Account.class); + when(sender.getNonce()).thenReturn(2L); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(2, KEYS1)), + Optional.of(sender)); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(3, KEYS1)), + Optional.of(sender)); + transactions.addTransaction( + createRemotePendingTransaction(transactionWithNonceAndSender(5, KEYS1)), + Optional.of(sender)); + + final PendingTransactions.Status status = transactions.getStatus(); + assertThat(status.pendingCount()).isEqualTo(2); + assertThat(status.queuedCount()).isEqualTo(1); + } + protected void assertMaximumNonceForSender(final Address sender1, final int i) { assertThat(transactions.getNextNonceForSender(sender1)).isEqualTo(OptionalLong.of(i)); }