diff --git a/CHANGELOG.md b/CHANGELOG.md index b1455ef0794..29d78083ee6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ ### Additions and Improvements - Dispatch snap server request processing (GET_ACCOUNT_RANGE, GET_STORAGE_RANGE, GET_BYTECODES, GET_TRIE_NODES, GET_BLOCK_ACCESS_LISTS) off the Netty event loop to prevent heavy trie/DB work from blocking ETH protocol message handling [#10083](https://github.com/besu-eth/besu/pull/10083) - Add DiscV5 discovery metrics (`discv5_live_nodes_current`, `discv5_total_nodes_current`) to track node counts in the routing table [#9692](https://github.com/besu-eth/besu/issues/9692) +- Add `txpool_contentFrom` JSON-RPC method [#10111](https://github.com/besu-eth/besu/pull/10111) ## 26.3.0 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 e8f6bc7ce9e..ebbf90bd6c7 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 @@ -169,6 +169,7 @@ public enum RpcMethod { TX_POOL_BESU_TRANSACTIONS("txpool_besuTransactions"), TX_POOL_BESU_PENDING_TRANSACTIONS("txpool_besuPendingTransactions"), TX_POOL_STATUS("txpool_status"), + TX_POOL_CONTENT_FROM("txpool_contentFrom"), 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/TxPoolContentFrom.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolContentFrom.java new file mode 100644 index 00000000000..2c3e71d699a --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolContentFrom.java @@ -0,0 +1,96 @@ +/* + * 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.datatypes.Address; +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.exception.InvalidJsonRpcParameters; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter; +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.response.RpcErrorType; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPendingResult; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPoolContentFromResult; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.SenderPendingTransactionsData; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.SequencedMap; +import java.util.stream.Collectors; + +public class TxPoolContentFrom implements JsonRpcMethod { + + private final TransactionPool transactionPool; + + public TxPoolContentFrom(final TransactionPool transactionPool) { + this.transactionPool = transactionPool; + } + + @Override + public String getName() { + return RpcMethod.TX_POOL_CONTENT_FROM.getMethodName(); + } + + @Override + public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { + try { + final Address sender = requestContext.getRequiredParameter(0, Address.class); + + return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), contentFrom(sender)); + } catch (JsonRpcParameter.JsonRpcParameterException e) { + throw new InvalidJsonRpcParameters( + "Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e); + } + } + + private TransactionPoolContentFromResult contentFrom(final Address sender) { + final SenderPendingTransactionsData pendingTransactionsData = + transactionPool.getPendingTransactionsFor(sender); + final List pendingTransactions = + pendingTransactionsData.pendingTransactions(); + long expectedNonce = pendingTransactionsData.nonce(); + int idx = 0; + while (idx < pendingTransactions.size() + && expectedNonce == pendingTransactions.get(idx).getNonce()) { + ++expectedNonce; + ++idx; + } + + final SequencedMap pendingByNonce = + pendingTransactions.subList(0, idx).stream() + .map(PendingTransaction::getTransaction) + .collect( + Collectors.toMap( + tx -> Long.toString(tx.getNonce()), + TransactionPendingResult::new, + (a, b) -> a, + LinkedHashMap::new)); + + final SequencedMap queuedByNonce = + pendingTransactions.subList(idx, pendingTransactions.size()).stream() + .map(PendingTransaction::getTransaction) + .collect( + Collectors.toMap( + tx -> Long.toString(tx.getNonce()), + TransactionPendingResult::new, + (a, b) -> a, + LinkedHashMap::new)); + + return new TransactionPoolContentFromResult(pendingByNonce, queuedByNonce); + } +} diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolContentFromResult.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolContentFromResult.java new file mode 100644 index 00000000000..feebc9a2469 --- /dev/null +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/results/TransactionPoolContentFromResult.java @@ -0,0 +1,42 @@ +/* + * 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 java.util.SequencedMap; + +import com.fasterxml.jackson.annotation.JsonGetter; + +public class TransactionPoolContentFromResult { + + private final SequencedMap pending; + private final SequencedMap queued; + + public TransactionPoolContentFromResult( + final SequencedMap pending, + final SequencedMap queued) { + this.pending = pending; + this.queued = queued; + } + + @JsonGetter(value = "pending") + public SequencedMap getPending() { + return pending; + } + + @JsonGetter(value = "queued") + public SequencedMap getQueued() { + return 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 5a62028fce7..efae70a3463 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.TxPoolContentFrom; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolStatus; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; @@ -43,6 +44,7 @@ protected Map create() { new TxPoolBesuTransactions(transactionPool), new TxPoolBesuPendingTransactions(transactionPool), new TxPoolBesuStatistics(transactionPool), - new TxPoolStatus(transactionPool)); + new TxPoolStatus(transactionPool), + new TxPoolContentFrom(transactionPool)); } } diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolContentFromTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolContentFromTest.java new file mode 100644 index 00000000000..3faf5df667d --- /dev/null +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TxPoolContentFromTest.java @@ -0,0 +1,171 @@ +/* + * 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.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.crypto.KeyPair; +import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; +import org.hyperledger.besu.datatypes.Address; +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.exception.InvalidJsonRpcParameters; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPoolContentFromResult; +import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.core.TransactionTestFixture; +import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; +import org.hyperledger.besu.ethereum.eth.transactions.SenderPendingTransactionsData; +import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; + +import java.util.List; + +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 TxPoolContentFromTest { + + @Mock private TransactionPool transactionPool; + + private TxPoolContentFrom method; + + private static final String JSON_RPC_VERSION = "2.0"; + private static final String METHOD_NAME = "txpool_contentFrom"; + private static final Address SENDER = + Address.fromHexString("0x1234567890123456789012345678901234567890"); + private static final KeyPair KEY_PAIR = SignatureAlgorithmFactory.getInstance().generateKeyPair(); + + @BeforeEach + public void setUp() { + method = new TxPoolContentFrom(transactionPool); + } + + @Test + public void returnsCorrectMethodName() { + assertThat(method.getName()).isEqualTo(METHOD_NAME); + } + + @Test + public void shouldReturnEmptyResultForSenderWithNoTransactions() { + when(transactionPool.getPendingTransactionsFor(SENDER)) + .thenReturn(SenderPendingTransactionsData.empty(SENDER)); + + final TransactionPoolContentFromResult result = invokeMethod(); + + assertThat(result.getPending()).isEmpty(); + assertThat(result.getQueued()).isEmpty(); + } + + @Test + public void shouldReturnAllTransactionsAsPendingWhenAllAreConsecutive() { + // Nonce = 0, txs at nonces 0, 1, 2 → all pending, none queued + final PendingTransaction tx0 = pendingTx(0); + final PendingTransaction tx1 = pendingTx(1); + final PendingTransaction tx2 = pendingTx(2); + + when(transactionPool.getPendingTransactionsFor(SENDER)) + .thenReturn(new SenderPendingTransactionsData(SENDER, 0L, List.of(tx0, tx1, tx2))); + + final TransactionPoolContentFromResult result = invokeMethod(); + + assertThat(result.getPending()).containsOnlyKeys("0", "1", "2"); + assertThat(result.getQueued()).isEmpty(); + } + + @Test + public void shouldReturnAllTransactionsAsQueuedWhenGapExistsAtStart() { + // Nonce = 0, but first tx has nonce 2 → all queued, none pending + final PendingTransaction tx2 = pendingTx(2); + final PendingTransaction tx3 = pendingTx(3); + + when(transactionPool.getPendingTransactionsFor(SENDER)) + .thenReturn(new SenderPendingTransactionsData(SENDER, 0L, List.of(tx2, tx3))); + + final TransactionPoolContentFromResult result = invokeMethod(); + + assertThat(result.getPending()).isEmpty(); + assertThat(result.getQueued()).containsOnlyKeys("2", "3"); + } + + @Test + public void shouldSplitTransactionsIntoPendingAndQueued() { + // Nonce = 0, txs at nonces 0, 1, 3, 4 → pending: [0,1], queued: [3,4] + final PendingTransaction tx0 = pendingTx(0); + final PendingTransaction tx1 = pendingTx(1); + final PendingTransaction tx3 = pendingTx(3); + final PendingTransaction tx4 = pendingTx(4); + + when(transactionPool.getPendingTransactionsFor(SENDER)) + .thenReturn(new SenderPendingTransactionsData(SENDER, 0L, List.of(tx0, tx1, tx3, tx4))); + + final TransactionPoolContentFromResult result = invokeMethod(); + + assertThat(result.getPending()).containsOnlyKeys("0", "1"); + assertThat(result.getQueued()).containsOnlyKeys("3", "4"); + } + + @Test + public void shouldHandleMidNonceAccountState() { + // Account has mined nonces 0-4; pool has nonces 5, 6, 8 → pending: [5,6], queued: [8] + final PendingTransaction tx5 = pendingTx(5); + final PendingTransaction tx6 = pendingTx(6); + final PendingTransaction tx8 = pendingTx(8); + + when(transactionPool.getPendingTransactionsFor(SENDER)) + .thenReturn(new SenderPendingTransactionsData(SENDER, 5L, List.of(tx5, tx6, tx8))); + + final TransactionPoolContentFromResult result = invokeMethod(); + + assertThat(result.getPending()).containsOnlyKeys("5", "6"); + assertThat(result.getQueued()).containsOnlyKeys("8"); + } + + @Test + public void shouldThrowInvalidJsonRpcParametersWhenAddressParamIsMissing() { + final JsonRpcRequestContext request = + new JsonRpcRequestContext( + new JsonRpcRequest(JSON_RPC_VERSION, METHOD_NAME, new Object[] {})); + + assertThatThrownBy(() -> method.response(request)).isInstanceOf(InvalidJsonRpcParameters.class); + } + + private TransactionPoolContentFromResult invokeMethod() { + final JsonRpcSuccessResponse response = + (JsonRpcSuccessResponse) method.response(buildRequest(SENDER)); + return (TransactionPoolContentFromResult) response.getResult(); + } + + private JsonRpcRequestContext buildRequest(final Address sender) { + return new JsonRpcRequestContext( + new JsonRpcRequest(JSON_RPC_VERSION, METHOD_NAME, new Object[] {sender.toString()})); + } + + private PendingTransaction pendingTx(final long nonce) { + final Transaction tx = + new TransactionTestFixture().sender(SENDER).nonce(nonce).createTransaction(KEY_PAIR); + final PendingTransaction pendingTransaction = mock(PendingTransaction.class); + lenient().when(pendingTransaction.getNonce()).thenReturn(nonce); + when(pendingTransaction.getTransaction()).thenReturn(tx); + return pendingTransaction; + } +} 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 bc7c6db516c..b882ff9ed8b 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 @@ -79,6 +79,11 @@ public Collection getPendingTransactions() { return List.of(); } + @Override + public SenderPendingTransactionsData getPendingTransactionsFor(final Address sender) { + return SenderPendingTransactionsData.empty(sender); + } + @Override public long subscribePendingTransactions(final PendingTransactionAddedListener listener) { return 0; 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 13642cb70dd..ee3462422db 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 @@ -53,6 +53,14 @@ TransactionAddedResult addTransaction( Collection getPendingTransactions(); + /** + * Returns all pending transactions for the given sender, sorted by nonce in ascending order. + * + * @param sender the sender address + * @return transactions for the sender sorted by nonce ascending, or an empty list if none exist + */ + SenderPendingTransactionsData getPendingTransactionsFor(Address sender); + long subscribePendingTransactions(PendingTransactionAddedListener listener); void unsubscribePendingTransactions(long id); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SenderPendingTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/SenderPendingTransactionsData.java similarity index 73% rename from ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SenderPendingTransactions.java rename to ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/SenderPendingTransactionsData.java index 7fb7c5cfeb7..7729bd75504 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/SenderPendingTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/SenderPendingTransactionsData.java @@ -12,10 +12,9 @@ * * SPDX-License-Identifier: Apache-2.0 */ -package org.hyperledger.besu.ethereum.eth.transactions.layered; +package org.hyperledger.besu.ethereum.eth.transactions; import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction; import java.util.List; import java.util.stream.Collectors; @@ -24,16 +23,23 @@ * A list of pending transactions of a specific sender, ordered by nonce asc * * @param sender the sender + * @param nonce the expected nonce for the sender as seen by the txpool * @param pendingTransactions the list of pending transactions order by nonce asc */ -public record SenderPendingTransactions( - Address sender, List pendingTransactions) { +public record SenderPendingTransactionsData( + Address sender, long nonce, List pendingTransactions) { + + public static SenderPendingTransactionsData empty(final Address sender) { + return new SenderPendingTransactionsData(sender, 0L, List.of()); + } @Override public String toString() { return "Sender " + sender - + " has " + + " has nonce " + + nonce + + " and " + pendingTransactions.size() + " pending transactions " + pendingTransactions.stream() 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 bafce8ab92a..2121386c077 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 @@ -555,6 +555,16 @@ public Collection getPendingTransactions() { return pendingTransactions.getPendingTransactions(); } + /** + * Returns all pending transactions for the given sender, sorted by nonce in ascending order. + * + * @param sender the sender address + * @return transactions for the sender sorted by nonce ascending, or an empty list if none exist + */ + public SenderPendingTransactionsData getPendingTransactionsFor(final Address sender) { + return pendingTransactions.getPendingTransactionsFor(sender); + } + public OptionalLong getNextNonceForSender(final Address address) { return pendingTransactions.getNextNonceForSender(address); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactions.java index 612f27534ba..22952e96f2e 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractPrioritizedTransactions.java @@ -193,15 +193,12 @@ protected int[] getRemainingPromotionsPerType() { * @return a list of sender pending txs */ @Override - public List getBySender() { + public List> getBySender() { final var sendersToAdd = new HashSet<>(txsBySender.keySet()); return orderByFee.descendingSet().stream() .map(PendingTransaction::getSender) .filter(sendersToAdd::remove) - .map( - sender -> - new SenderPendingTransactions( - sender, List.copyOf(txsBySender.get(sender).values()))) + .map(sender -> List.copyOf(txsBySender.get(sender).values())) .toList(); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractTransactionsLayer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractTransactionsLayer.java index 27ab6ddaa28..a46175b0893 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractTransactionsLayer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/AbstractTransactionsLayer.java @@ -63,6 +63,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -151,7 +152,8 @@ public boolean contains(final Transaction transaction) { * * @return a list of sender pending txs */ - public abstract List getBySender(); + @VisibleForTesting + abstract List> getBySender(); @Override public List getAll() { 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 e514c35a8bb..6000ff0bdf7 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 @@ -35,6 +35,7 @@ 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.SenderPendingTransactionsData; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; @@ -346,6 +347,15 @@ public synchronized List getPendingTransactions() { return prioritizedTransactions.getAll(); } + @Override + public synchronized SenderPendingTransactionsData getPendingTransactionsFor( + final Address sender) { + return new SenderPendingTransactionsData( + sender, + prioritizedTransactions.getCurrentNonceFor(sender).orElse(0), + prioritizedTransactions.getAllFor(sender)); + } + @Override public long subscribePendingTransactions(final PendingTransactionAddedListener listener) { return prioritizedTransactions.subscribeToAdded(listener); @@ -367,7 +377,7 @@ public void unsubscribeDroppedTransactions(final long id) { } @Override - public OptionalLong getNextNonceForSender(final Address sender) { + public synchronized OptionalLong getNextNonceForSender(final Address sender) { return prioritizedTransactions.getNextNonceFor(sender); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReadyTransactions.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReadyTransactions.java index d8fbf1e2a1d..1bfd56af6a1 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReadyTransactions.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/layered/ReadyTransactions.java @@ -160,13 +160,10 @@ protected boolean promotionFilter(final PendingTransaction pendingTransaction) { * @return a list of sender pending txs */ @Override - public List getBySender() { + public List> getBySender() { return orderByMaxFee.descendingSet().stream() .map(PendingTransaction::getSender) - .map( - sender -> - new SenderPendingTransactions( - sender, List.copyOf(txsBySender.get(sender).values()))) + .map(sender -> List.copyOf(txsBySender.get(sender).values())) .toList(); } 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 78492749566..d4058a14f15 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 @@ -357,15 +357,12 @@ protected boolean promotionFilter(final PendingTransaction pendingTransaction) { * @return a list of sender pending txs */ @Override - public List getBySender() { + public List> getBySender() { final var sendersToAdd = new HashSet<>(txsBySender.keySet()); return sparseEvictionOrder.descendingSet().stream() .map(PendingTransaction::getSender) .filter(sendersToAdd::remove) - .map( - sender -> - new SenderPendingTransactions( - sender, List.copyOf(txsBySender.get(sender).values()))) + .map(sender -> List.copyOf(txsBySender.get(sender).values())) .toList(); } 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 e37c5920579..a54c13f04cc 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 @@ -87,6 +87,13 @@ void blockAdded( List getAll(); + /** + * Returns all pending transactions for the given sender across all layers, sorted by nonce in + * ascending order. + * + * @param sender the sender address + * @return transactions for the sender sorted by nonce ascending, or an empty list if none exist + */ List getAllFor(Address sender); List getAllLocal(); 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 9c4b4a71a40..fcafb3d9545 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 @@ -32,6 +32,7 @@ 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.SenderPendingTransactionsData; import org.hyperledger.besu.ethereum.eth.transactions.TransactionAddedResult; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPoolReplacementHandler; @@ -391,6 +392,20 @@ public List getPendingTransactions() { return new ArrayList<>(pendingTransactions.values()); } + @Override + public SenderPendingTransactionsData getPendingTransactionsFor(final Address sender) { + final PendingTransactionsForSender pendingTransactionsForSender = + transactionsBySender.get(sender); + if (pendingTransactionsForSender == null) { + return SenderPendingTransactionsData.empty(sender); + } + + return new SenderPendingTransactionsData( + sender, + pendingTransactionsForSender.maybeCurrentNonce().orElse(0), + pendingTransactionsForSender.getPendingTransactions()); + } + @Override public long subscribePendingTransactions(final PendingTransactionAddedListener listener) { return pendingTransactionSubscribers.subscribe(listener); 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 77b0fa82bb9..caf957b17da 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 @@ -31,7 +31,7 @@ public class PendingTransactionsForSender { private final NavigableMap pendingTransactions; private OptionalLong nextGap = OptionalLong.empty(); - private Optional maybeSenderAccount; + private volatile Optional maybeSenderAccount; public PendingTransactionsForSender(final Optional maybeSenderAccount) { this.pendingTransactions = new TreeMap<>(); @@ -113,6 +113,12 @@ public OptionalLong maybeNextNonce() { } } + public OptionalLong maybeCurrentNonce() { + return maybeSenderAccount + .map(account -> OptionalLong.of(account.getNonce())) + .orElse(OptionalLong.empty()); + } + public int transactionCount() { return pendingTransactions.size(); } @@ -121,6 +127,10 @@ public List getPendingTransactions(final long startingNonce) return List.copyOf(pendingTransactions.tailMap(startingNonce).values()); } + public List getPendingTransactions() { + return List.copyOf(pendingTransactions.values()); + } + public Stream streamPendingTransactions() { return pendingTransactions.values().stream(); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java index 2a4dc67d3f6..a40f4aeed87 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/transactions/layered/LayersTest.java @@ -1947,17 +1947,15 @@ private void assertExpectedPrioritized( private void assertExpectedReady( final ReadyTransactions readyLayer, final List expected) { - assertThat(readyLayer.getBySender()) + assertThat(readyLayer.getBySender().stream().flatMap(List::stream).toList()) .describedAs("Ready") - .flatExtracting(SenderPendingTransactions::pendingTransactions) .containsExactlyElementsOf(expected); } private void assertExpectedSparse( final SparseTransactions sparseLayer, final List expected) { - assertThat(sparseLayer.getBySender()) + assertThat(sparseLayer.getBySender().stream().flatMap(List::stream).toList()) .describedAs("Sparse") - .flatExtracting(SenderPendingTransactions::pendingTransactions) .containsExactlyElementsOf(expected); } @@ -2053,8 +2051,7 @@ public Scenario expectedSelectedTransactions(final Object... args) { expectedSelected.add(get(sender, nonce)); } - assertThat(prio.getBySender()) - .flatExtracting(SenderPendingTransactions::pendingTransactions) + assertThat(prio.getBySender().stream().flatMap(List::stream).toList()) .containsExactlyElementsOf(expectedSelected); }); return this;