Skip to content

Commit 530f182

Browse files
fab-10claude
andauthored
Add txpool_contentFrom JSON-RPC method (#10111)
Implements the txpool_contentFrom method as defined in the execution-apis spec. Given a sender address, returns the pending (executable) and queued (non-executable) transactions for that address split by nonce continuity from the account's current on-chain nonce. - Add TX_POOL_CONTENT_FROM to RpcMethod enum - Add TxPoolContentFrom handler and TransactionPoolContentFromResult - Register the method in TxPoolJsonRpcMethods - Add getPendingTransactionsForSender and getCurrentNonceForSender to the PendingTransactions interface with implementations in all concrete classes - Add unit tests covering empty pool, all-pending, all-queued, mixed split, absent account nonce, non-zero base nonce, and missing param error cases Signed-off-by: Fabio Di Fabio <fabio.difabio@consensys.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4790c97 commit 530f182

File tree

19 files changed

+404
-30
lines changed

19 files changed

+404
-30
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
### Additions and Improvements
1313
- 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)
1414
- 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)
15+
- Add `txpool_contentFrom` JSON-RPC method [#10111](https://github.com/besu-eth/besu/pull/10111)
1516

1617
## 26.3.0
1718

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/RpcMethod.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ public enum RpcMethod {
169169
TX_POOL_BESU_TRANSACTIONS("txpool_besuTransactions"),
170170
TX_POOL_BESU_PENDING_TRANSACTIONS("txpool_besuPendingTransactions"),
171171
TX_POOL_STATUS("txpool_status"),
172+
TX_POOL_CONTENT_FROM("txpool_contentFrom"),
172173
WEB3_CLIENT_VERSION("web3_clientVersion"),
173174
WEB3_SHA3("web3_sha3"),
174175
PLUGINS_RELOAD_CONFIG("plugins_reloadPluginConfig"),
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
16+
17+
import org.hyperledger.besu.datatypes.Address;
18+
import org.hyperledger.besu.ethereum.api.jsonrpc.RpcMethod;
19+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
20+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
21+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.parameters.JsonRpcParameter;
22+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse;
23+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
24+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.RpcErrorType;
25+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPendingResult;
26+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPoolContentFromResult;
27+
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
28+
import org.hyperledger.besu.ethereum.eth.transactions.SenderPendingTransactionsData;
29+
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
30+
31+
import java.util.LinkedHashMap;
32+
import java.util.List;
33+
import java.util.SequencedMap;
34+
import java.util.stream.Collectors;
35+
36+
public class TxPoolContentFrom implements JsonRpcMethod {
37+
38+
private final TransactionPool transactionPool;
39+
40+
public TxPoolContentFrom(final TransactionPool transactionPool) {
41+
this.transactionPool = transactionPool;
42+
}
43+
44+
@Override
45+
public String getName() {
46+
return RpcMethod.TX_POOL_CONTENT_FROM.getMethodName();
47+
}
48+
49+
@Override
50+
public JsonRpcResponse response(final JsonRpcRequestContext requestContext) {
51+
try {
52+
final Address sender = requestContext.getRequiredParameter(0, Address.class);
53+
54+
return new JsonRpcSuccessResponse(requestContext.getRequest().getId(), contentFrom(sender));
55+
} catch (JsonRpcParameter.JsonRpcParameterException e) {
56+
throw new InvalidJsonRpcParameters(
57+
"Invalid address parameter (index 0)", RpcErrorType.INVALID_ADDRESS_PARAMS, e);
58+
}
59+
}
60+
61+
private TransactionPoolContentFromResult contentFrom(final Address sender) {
62+
final SenderPendingTransactionsData pendingTransactionsData =
63+
transactionPool.getPendingTransactionsFor(sender);
64+
final List<PendingTransaction> pendingTransactions =
65+
pendingTransactionsData.pendingTransactions();
66+
long expectedNonce = pendingTransactionsData.nonce();
67+
int idx = 0;
68+
while (idx < pendingTransactions.size()
69+
&& expectedNonce == pendingTransactions.get(idx).getNonce()) {
70+
++expectedNonce;
71+
++idx;
72+
}
73+
74+
final SequencedMap<String, TransactionPendingResult> pendingByNonce =
75+
pendingTransactions.subList(0, idx).stream()
76+
.map(PendingTransaction::getTransaction)
77+
.collect(
78+
Collectors.toMap(
79+
tx -> Long.toString(tx.getNonce()),
80+
TransactionPendingResult::new,
81+
(a, b) -> a,
82+
LinkedHashMap::new));
83+
84+
final SequencedMap<String, TransactionPendingResult> queuedByNonce =
85+
pendingTransactions.subList(idx, pendingTransactions.size()).stream()
86+
.map(PendingTransaction::getTransaction)
87+
.collect(
88+
Collectors.toMap(
89+
tx -> Long.toString(tx.getNonce()),
90+
TransactionPendingResult::new,
91+
(a, b) -> a,
92+
LinkedHashMap::new));
93+
94+
return new TransactionPoolContentFromResult(pendingByNonce, queuedByNonce);
95+
}
96+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.results;
16+
17+
import java.util.SequencedMap;
18+
19+
import com.fasterxml.jackson.annotation.JsonGetter;
20+
21+
public class TransactionPoolContentFromResult {
22+
23+
private final SequencedMap<String, TransactionPendingResult> pending;
24+
private final SequencedMap<String, TransactionPendingResult> queued;
25+
26+
public TransactionPoolContentFromResult(
27+
final SequencedMap<String, TransactionPendingResult> pending,
28+
final SequencedMap<String, TransactionPendingResult> queued) {
29+
this.pending = pending;
30+
this.queued = queued;
31+
}
32+
33+
@JsonGetter(value = "pending")
34+
public SequencedMap<String, TransactionPendingResult> getPending() {
35+
return pending;
36+
}
37+
38+
@JsonGetter(value = "queued")
39+
public SequencedMap<String, TransactionPendingResult> getQueued() {
40+
return queued;
41+
}
42+
}

ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/TxPoolJsonRpcMethods.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuPendingTransactions;
2020
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuStatistics;
2121
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolBesuTransactions;
22+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolContentFrom;
2223
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods.TxPoolStatus;
2324
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
2425

@@ -43,6 +44,7 @@ protected Map<String, JsonRpcMethod> create() {
4344
new TxPoolBesuTransactions(transactionPool),
4445
new TxPoolBesuPendingTransactions(transactionPool),
4546
new TxPoolBesuStatistics(transactionPool),
46-
new TxPoolStatus(transactionPool));
47+
new TxPoolStatus(transactionPool),
48+
new TxPoolContentFrom(transactionPool));
4749
}
4850
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/*
2+
* Copyright contributors to Besu.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
5+
* the License. You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
10+
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
11+
* specific language governing permissions and limitations under the License.
12+
*
13+
* SPDX-License-Identifier: Apache-2.0
14+
*/
15+
package org.hyperledger.besu.ethereum.api.jsonrpc.internal.methods;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
19+
import static org.mockito.Mockito.lenient;
20+
import static org.mockito.Mockito.mock;
21+
import static org.mockito.Mockito.when;
22+
23+
import org.hyperledger.besu.crypto.KeyPair;
24+
import org.hyperledger.besu.crypto.SignatureAlgorithmFactory;
25+
import org.hyperledger.besu.datatypes.Address;
26+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest;
27+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext;
28+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters;
29+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse;
30+
import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.TransactionPoolContentFromResult;
31+
import org.hyperledger.besu.ethereum.core.Transaction;
32+
import org.hyperledger.besu.ethereum.core.TransactionTestFixture;
33+
import org.hyperledger.besu.ethereum.eth.transactions.PendingTransaction;
34+
import org.hyperledger.besu.ethereum.eth.transactions.SenderPendingTransactionsData;
35+
import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool;
36+
37+
import java.util.List;
38+
39+
import org.junit.jupiter.api.BeforeEach;
40+
import org.junit.jupiter.api.Test;
41+
import org.junit.jupiter.api.extension.ExtendWith;
42+
import org.mockito.Mock;
43+
import org.mockito.junit.jupiter.MockitoExtension;
44+
45+
@ExtendWith(MockitoExtension.class)
46+
public class TxPoolContentFromTest {
47+
48+
@Mock private TransactionPool transactionPool;
49+
50+
private TxPoolContentFrom method;
51+
52+
private static final String JSON_RPC_VERSION = "2.0";
53+
private static final String METHOD_NAME = "txpool_contentFrom";
54+
private static final Address SENDER =
55+
Address.fromHexString("0x1234567890123456789012345678901234567890");
56+
private static final KeyPair KEY_PAIR = SignatureAlgorithmFactory.getInstance().generateKeyPair();
57+
58+
@BeforeEach
59+
public void setUp() {
60+
method = new TxPoolContentFrom(transactionPool);
61+
}
62+
63+
@Test
64+
public void returnsCorrectMethodName() {
65+
assertThat(method.getName()).isEqualTo(METHOD_NAME);
66+
}
67+
68+
@Test
69+
public void shouldReturnEmptyResultForSenderWithNoTransactions() {
70+
when(transactionPool.getPendingTransactionsFor(SENDER))
71+
.thenReturn(SenderPendingTransactionsData.empty(SENDER));
72+
73+
final TransactionPoolContentFromResult result = invokeMethod();
74+
75+
assertThat(result.getPending()).isEmpty();
76+
assertThat(result.getQueued()).isEmpty();
77+
}
78+
79+
@Test
80+
public void shouldReturnAllTransactionsAsPendingWhenAllAreConsecutive() {
81+
// Nonce = 0, txs at nonces 0, 1, 2 → all pending, none queued
82+
final PendingTransaction tx0 = pendingTx(0);
83+
final PendingTransaction tx1 = pendingTx(1);
84+
final PendingTransaction tx2 = pendingTx(2);
85+
86+
when(transactionPool.getPendingTransactionsFor(SENDER))
87+
.thenReturn(new SenderPendingTransactionsData(SENDER, 0L, List.of(tx0, tx1, tx2)));
88+
89+
final TransactionPoolContentFromResult result = invokeMethod();
90+
91+
assertThat(result.getPending()).containsOnlyKeys("0", "1", "2");
92+
assertThat(result.getQueued()).isEmpty();
93+
}
94+
95+
@Test
96+
public void shouldReturnAllTransactionsAsQueuedWhenGapExistsAtStart() {
97+
// Nonce = 0, but first tx has nonce 2 → all queued, none pending
98+
final PendingTransaction tx2 = pendingTx(2);
99+
final PendingTransaction tx3 = pendingTx(3);
100+
101+
when(transactionPool.getPendingTransactionsFor(SENDER))
102+
.thenReturn(new SenderPendingTransactionsData(SENDER, 0L, List.of(tx2, tx3)));
103+
104+
final TransactionPoolContentFromResult result = invokeMethod();
105+
106+
assertThat(result.getPending()).isEmpty();
107+
assertThat(result.getQueued()).containsOnlyKeys("2", "3");
108+
}
109+
110+
@Test
111+
public void shouldSplitTransactionsIntoPendingAndQueued() {
112+
// Nonce = 0, txs at nonces 0, 1, 3, 4 → pending: [0,1], queued: [3,4]
113+
final PendingTransaction tx0 = pendingTx(0);
114+
final PendingTransaction tx1 = pendingTx(1);
115+
final PendingTransaction tx3 = pendingTx(3);
116+
final PendingTransaction tx4 = pendingTx(4);
117+
118+
when(transactionPool.getPendingTransactionsFor(SENDER))
119+
.thenReturn(new SenderPendingTransactionsData(SENDER, 0L, List.of(tx0, tx1, tx3, tx4)));
120+
121+
final TransactionPoolContentFromResult result = invokeMethod();
122+
123+
assertThat(result.getPending()).containsOnlyKeys("0", "1");
124+
assertThat(result.getQueued()).containsOnlyKeys("3", "4");
125+
}
126+
127+
@Test
128+
public void shouldHandleMidNonceAccountState() {
129+
// Account has mined nonces 0-4; pool has nonces 5, 6, 8 → pending: [5,6], queued: [8]
130+
final PendingTransaction tx5 = pendingTx(5);
131+
final PendingTransaction tx6 = pendingTx(6);
132+
final PendingTransaction tx8 = pendingTx(8);
133+
134+
when(transactionPool.getPendingTransactionsFor(SENDER))
135+
.thenReturn(new SenderPendingTransactionsData(SENDER, 5L, List.of(tx5, tx6, tx8)));
136+
137+
final TransactionPoolContentFromResult result = invokeMethod();
138+
139+
assertThat(result.getPending()).containsOnlyKeys("5", "6");
140+
assertThat(result.getQueued()).containsOnlyKeys("8");
141+
}
142+
143+
@Test
144+
public void shouldThrowInvalidJsonRpcParametersWhenAddressParamIsMissing() {
145+
final JsonRpcRequestContext request =
146+
new JsonRpcRequestContext(
147+
new JsonRpcRequest(JSON_RPC_VERSION, METHOD_NAME, new Object[] {}));
148+
149+
assertThatThrownBy(() -> method.response(request)).isInstanceOf(InvalidJsonRpcParameters.class);
150+
}
151+
152+
private TransactionPoolContentFromResult invokeMethod() {
153+
final JsonRpcSuccessResponse response =
154+
(JsonRpcSuccessResponse) method.response(buildRequest(SENDER));
155+
return (TransactionPoolContentFromResult) response.getResult();
156+
}
157+
158+
private JsonRpcRequestContext buildRequest(final Address sender) {
159+
return new JsonRpcRequestContext(
160+
new JsonRpcRequest(JSON_RPC_VERSION, METHOD_NAME, new Object[] {sender.toString()}));
161+
}
162+
163+
private PendingTransaction pendingTx(final long nonce) {
164+
final Transaction tx =
165+
new TransactionTestFixture().sender(SENDER).nonce(nonce).createTransaction(KEY_PAIR);
166+
final PendingTransaction pendingTransaction = mock(PendingTransaction.class);
167+
lenient().when(pendingTransaction.getNonce()).thenReturn(nonce);
168+
when(pendingTransaction.getTransaction()).thenReturn(tx);
169+
return pendingTransaction;
170+
}
171+
}

ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/DisabledPendingTransactions.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ public Collection<PendingTransaction> getPendingTransactions() {
7979
return List.of();
8080
}
8181

82+
@Override
83+
public SenderPendingTransactionsData getPendingTransactionsFor(final Address sender) {
84+
return SenderPendingTransactionsData.empty(sender);
85+
}
86+
8287
@Override
8388
public long subscribePendingTransactions(final PendingTransactionAddedListener listener) {
8489
return 0;

ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/PendingTransactions.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ TransactionAddedResult addTransaction(
5353

5454
Collection<PendingTransaction> getPendingTransactions();
5555

56+
/**
57+
* Returns all pending transactions for the given sender, sorted by nonce in ascending order.
58+
*
59+
* @param sender the sender address
60+
* @return transactions for the sender sorted by nonce ascending, or an empty list if none exist
61+
*/
62+
SenderPendingTransactionsData getPendingTransactionsFor(Address sender);
63+
5664
long subscribePendingTransactions(PendingTransactionAddedListener listener);
5765

5866
void unsubscribePendingTransactions(long id);

0 commit comments

Comments
 (0)