Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
270 changes: 263 additions & 7 deletions src/Nethermind/Nethermind.JsonRpc.Test/Modules/TxPoolRpcModuleTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

// using System.Collections.Generic;

using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using Nethermind.Core;
using Nethermind.Core.Eip2930;
Expand All @@ -20,7 +19,7 @@ namespace Nethermind.JsonRpc.Test.Modules;
public class TxPoolRpcModuleTests
{
[Test]
public void Pool_content_produces_transactions_with_ChainId()
public void TxPoolContent_WhenLegacyTxHasNoChainId_SerializesChainIdFromSpec()
{
const ulong SomeChainId = 123ul;
Transaction txA = Build.A.Transaction
Expand Down Expand Up @@ -62,10 +61,267 @@ public void Pool_content_produces_transactions_with_ChainId()

TxPoolContent txpoolContent = txPoolRpcModule.txpool_content().Data;

LegacyTransactionForRpc? rpcTxA = txpoolContent.Pending[new AddressAsKey(TestItem.AddressA)][1] as LegacyTransactionForRpc;
AccessListTransactionForRpc? rpcTxB = txpoolContent.Queued[new AddressAsKey(TestItem.AddressB)][2] as AccessListTransactionForRpc;
LegacyTransactionForRpc? rpcTxA = txpoolContent.Pending[TestItem.AddressA.ToString(withZeroX: true, withEip55Checksum: true)][1] as LegacyTransactionForRpc;
AccessListTransactionForRpc? rpcTxB = txpoolContent.Queued[TestItem.AddressB.ToString(withZeroX: true, withEip55Checksum: true)][2] as AccessListTransactionForRpc;

rpcTxA!.ChainId.Should().BeNull("legacy txs without chainId must not have one injected");
rpcTxB!.ChainId.Should().Be(SomeChainId, "EIP-2930 txs without chainId should inherit it from the spec provider");
}

[Test]
public void TxPoolStatus_WhenPoolHasTransactions_ReturnsPendingAndQueuedCounts()
{
Transaction txA = Build.A.Transaction.WithType(TxType.Legacy).TestObject;
Transaction txB = Build.A.Transaction.WithType(TxType.Legacy).TestObject;

ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(
pending: new()
{
{
new AddressAsKey(TestItem.AddressA), new Dictionary<ulong, Transaction>
{
{ 1, txA }, { 2, txB }
}
}
},
queued: new()
{
{
new AddressAsKey(TestItem.AddressB), new Dictionary<ulong, Transaction>
{
{ 5, txA }
}
}
}
));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

TxPoolStatus status = txPoolRpcModule.txpool_status().Data;

status.Pending.Should().Be(2ul, "AddressA has 2 pending transactions");
status.Queued.Should().Be(1ul, "AddressB has 1 queued transaction");
}

[Test]
public void TxPoolContentFrom_WhenAddressIsInPool_ReturnsOnlyMatchingTransactions()
{
// AddressA has nonces 1 and 2 in pending; AddressB has nonce 3.
// Querying for AddressA must return only its 2 transactions and an empty queued map.
const ulong SomeChainId = 1ul;
Transaction txA = Build.A.Transaction.WithType(TxType.Legacy).WithChainId(null).TestObject;
Transaction txB = Build.A.Transaction.WithType(TxType.Legacy).WithChainId(null).TestObject;
Transaction txC = Build.A.Transaction.WithType(TxType.Legacy).WithChainId(null).TestObject;

ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(
pending: new()
{
{
new AddressAsKey(TestItem.AddressA), new Dictionary<ulong, Transaction>
{
{ 1, txA }, { 2, txB }
}
},
{
new AddressAsKey(TestItem.AddressB), new Dictionary<ulong, Transaction>
{
{ 3, txC }
}
}
},
queued: new()
));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
specProvider.ChainId.Returns(SomeChainId);

TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

TxPoolContentFrom result = txPoolRpcModule.txpool_contentFrom(TestItem.AddressA).Data;

result.Pending.Should().HaveCount(2, "AddressA has exactly 2 pending transactions");
result.Pending.Should().ContainKey(1ul, "nonce 1 belongs to AddressA");
result.Pending.Should().ContainKey(2ul, "nonce 2 belongs to AddressA");
result.Queued.Should().BeEmpty("no queued transactions were set up for AddressA");
}

[Test]
public void TxPoolContentFrom_WhenAddressNotInPool_ReturnsEmptyPendingAndQueued()
{
ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(
pending: new(),
queued: new()
));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
specProvider.ChainId.Returns(1ul);

TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

TxPoolContentFrom result = txPoolRpcModule.txpool_contentFrom(TestItem.AddressA).Data;

result.Pending.Should().BeEmpty("the pool has no transactions for any address");
result.Queued.Should().BeEmpty("the pool has no transactions for any address");
}

[Test]
public async Task TxPoolStatus_WhenPoolHasTransactions_SerializesCountsAsHexStrings()
{
Transaction txA = Build.A.Transaction.WithType(TxType.Legacy).TestObject;
Transaction txB = Build.A.Transaction.WithType(TxType.Legacy).TestObject;

ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(
pending: new()
{
{
new AddressAsKey(TestItem.AddressA), new Dictionary<ulong, Transaction>
{
{ 1, txA }, { 2, txB }
}
}
},
queued: new()
{
{
new AddressAsKey(TestItem.AddressB), new Dictionary<ulong, Transaction>
{
{ 5, txA }
}
}
}
));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

string json = await RpcTest.TestSerializedRequest<ITxPoolRpcModule>(txPoolRpcModule, "txpool_status");

json.Should().Contain("\"pending\":\"0x2\"", "the spec requires pending count as a hex-encoded uint");
json.Should().Contain("\"queued\":\"0x1\"", "the spec requires queued count as a hex-encoded uint");
}

[Test]
public async Task TxPoolContent_WhenNonceIsLarge_SerializesNonceKeyAsDecimalString()
{
Transaction tx = Build.A.Transaction
.WithType(TxType.Legacy)
.WithNonce(806)
.WithSenderAddress(TestItem.AddressA)
.TestObject;

ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(
pending: new()
{
{
new AddressAsKey(TestItem.AddressA), new Dictionary<ulong, Transaction>
{
{ 806, tx }
}
}
},
queued: new()
));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
specProvider.ChainId.Returns(1ul);
TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

string json = await RpcTest.TestSerializedRequest<ITxPoolRpcModule>(txPoolRpcModule, "txpool_content");

json.Should().Contain("\"806\":{", "the spec requires nonce map keys as decimal strings, not hex");
json.Should().NotContain("\"0x326\":{", "hex nonce keys would violate the spec");
}

[Test]
public async Task TxPoolContent_WhenTransactionIsPending_IncludesNullBlockFieldsAndFrom()
{
Transaction tx = Build.A.Transaction
.WithType(TxType.Legacy)
.WithSenderAddress(TestItem.AddressA)
.TestObject;

ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(
pending: new()
{
{
new AddressAsKey(TestItem.AddressA), new Dictionary<ulong, Transaction>
{
{ 0, tx }
}
}
},
queued: new()
));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
specProvider.ChainId.Returns(1ul);
TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

string json = await RpcTest.TestSerializedRequest<ITxPoolRpcModule>(txPoolRpcModule, "txpool_content");

json.Should().Contain("\"blockHash\":null", "pending transactions have no block context");
json.Should().Contain("\"blockNumber\":null", "pending transactions have no block context");
json.Should().Contain("\"blockTimestamp\":null", "pending transactions have no block context");
json.Should().Contain("\"transactionIndex\":null", "pending transactions have no block context");
json.Should().Contain("\"from\":\"" + TestItem.AddressA.ToString().ToLowerInvariant() + "\"",
"the spec requires 'from' to be present on every pending transaction");
json.Should().Contain("\"" + TestItem.AddressA.ToString(withZeroX: true, withEip55Checksum: true) + "\":{",
"address map keys must use EIP-55 checksum format to match the spec");
}
Comment thread
svlachakis marked this conversation as resolved.

[Test]
public async Task TxPoolContentFrom_WhenNonceIsLarge_SerializesNonceKeyAsDecimalString()
{
Transaction tx = Build.A.Transaction
.WithType(TxType.Legacy)
.WithNonce(806)
.WithSenderAddress(TestItem.AddressA)
.TestObject;

ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(
pending: new()
{
{
new AddressAsKey(TestItem.AddressA), new Dictionary<ulong, Transaction>
{
{ 806, tx }
}
}
},
queued: new()
));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
specProvider.ChainId.Returns(1ul);
TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

string json = await RpcTest.TestSerializedRequest<ITxPoolRpcModule>(txPoolRpcModule, "txpool_contentFrom", TestItem.AddressA);

json.Should().Contain("\"806\":{", "the spec requires nonce map keys as decimal strings, not hex");
json.Should().NotContain("\"0x326\":{", "hex nonce keys would violate the spec");
}

[Test]
public async Task TxPoolContentFrom_WhenAddressNotInPool_SerializesEmptyPendingAndQueued()
{
ITxPoolInfoProvider txPoolInfoProvider = Substitute.For<ITxPoolInfoProvider>();
txPoolInfoProvider.GetInfo().Returns(new TxPoolInfo(pending: new(), queued: new()));

ISpecProvider specProvider = Substitute.For<ISpecProvider>();
specProvider.ChainId.Returns(1ul);
TxPoolRpcModule txPoolRpcModule = new(txPoolInfoProvider, specProvider);

string json = await RpcTest.TestSerializedRequest<ITxPoolRpcModule>(txPoolRpcModule, "txpool_contentFrom", TestItem.AddressA);

rpcTxA!.ChainId.Should().BeNull();
rpcTxB!.ChainId.Should().Be(SomeChainId);
json.Should().Contain("\"pending\":{}", "spec requires the pending field to always be present");
json.Should().Contain("\"queued\":{}", "spec requires the queued field to always be present");
}
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

namespace Nethermind.JsonRpc.Modules.TxPool
using Nethermind.Core;

namespace Nethermind.JsonRpc.Modules.TxPool;

[RpcModule(ModuleType.TxPool)]
public interface ITxPoolRpcModule : IRpcModule
{
[RpcModule(ModuleType.TxPool)]
public interface ITxPoolRpcModule : IRpcModule
{
[JsonRpcMethod(Description = "Returns a tx pool status.", IsImplemented = true, ExampleResponse = "{\"pending\":1010,\"queued\":14}")]
ResultWrapper<TxPoolStatus> txpool_status();
[JsonRpcMethod(Description = "Returns a tx pool status.", IsImplemented = true, ExampleResponse = "{\"pending\":\"0x3f2\",\"queued\":\"0xe\"}")]
ResultWrapper<TxPoolStatus> txpool_status();

[JsonRpcMethod(Description = "Returns tx pool content.",
IsImplemented = true,
ExampleResponse = "{\"0x0f990ef7ec160f01af7148b74cc8a86fe46c551e\":{\"153\":{\"hash\":\"0x84f6f2e5d24b9a0c25bd7018adbbf4388b2c07842782f73d5ddc389906d5f2c8\",\"nonce\":\"0x99\",\"blockHash\":null,\"blockNumber\":null,\"blockTimestamp\":null,\"transactionIndex\":null,\"from\":\"0x0f990ef7ec160f01af7148b74cc8a86fe46c551e\",\"to\":\"0x1b4e4664de1d57b665b4bf3523cbccf007766de3\",\"value\":\"0xc8\",\"gasPrice\":\"0x3b9aca08\",\"gas\":\"0x1c9c37f\",\"data\":\"0xaeeb89600000000000000000000000000000000000000000000000000000000000000001\",\"input\":\"0xaeeb89600000000000000000000000000000000000000000000000000000000000000001\",\"type\":\"0x0\",\"v\":\"0x2c\",\"s\":\"0x20158ce3f4f9c65f8c657c0d91bbfb43632b2951f6192bca8fb3a25c26dd81d5\",\"r\":\"0x2814d998f2a78dd4f37461485d88158a32ef5dcfa8c57e224b3ea77536df01b1\"}}}")]
Comment thread
svlachakis marked this conversation as resolved.
ResultWrapper<TxPoolContent> txpool_content();

[JsonRpcMethod(Description = "Returns tx pool content.",
IsImplemented = true,
ExampleResponse = "{\"0x0f990ef7ec160f01af7148b74cc8a86fe46c551e\":{\"153\":{\"hash\":\"0x84f6f2e5d24b9a0c25bd7018adbbf4388b2c07842782f73d5ddc389906d5f2c8\",\"nonce\":\"0x99\",\"blockHash\":null,\"blockNumber\":null,\"transactionIndex\":null,\"from\":\"0x0f990ef7ec160f01af7148b74cc8a86fe46c551e\",\"to\":\"0x1b4e4664de1d57b665b4bf3523cbccf007766de3\",\"value\":\"0xc8\",\"gasPrice\":\"0x3b9aca08\",\"gas\":\"0x1c9c37f\",\"data\":\"0xaeeb89600000000000000000000000000000000000000000000000000000000000000001\",\"input\":\"0xaeeb89600000000000000000000000000000000000000000000000000000000000000001\",\"type\":\"0x0\",\"v\":\"0x2c\",\"s\":\"0x20158ce3f4f9c65f8c657c0d91bbfb43632b2951f6192bca8fb3a25c26dd81d5\",\"r\":\"0x2814d998f2a78dd4f37461485d88158a32ef5dcfa8c57e224b3ea77536df01b1\"}}}")]
ResultWrapper<TxPoolContent> txpool_content();
[JsonRpcMethod(Description = "Returns tx pool content for a specific sender address.",
IsImplemented = true,
ExampleResponse = "{\"pending\":{\"806\":{\"blockHash\":null,\"blockNumber\":null,\"blockTimestamp\":null,\"from\":\"0x0216d5032f356960cd3749c31ab34eeff21b3395\",\"gas\":\"0x5208\",\"gasPrice\":\"0xba43b7400\",\"hash\":\"0xaf953a2d01f55cfe080c0c94150a60105e8ac3d51153058a1f03dd239dd08586\",\"nonce\":\"0x326\",\"to\":\"0x7f69a91a3cf4be60020fb58b893b7cbb65376db8\",\"transactionIndex\":null,\"value\":\"0x19a99f0cf456000\"}},\"queued\":{}}")]
ResultWrapper<TxPoolContentFrom> txpool_contentFrom(Address address);
Comment thread
svlachakis marked this conversation as resolved.

[JsonRpcMethod(Description = "Returns a detailed info on tx pool transactions.",
IsImplemented = true,
ExampleResponse = "{\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea\":{\"20\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6721975 × 140000000000 gas\",\"21\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6721975 × 140000000000 gas\",\"22\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6721975 × 140000000000 gas\",\"23\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6700000 × 140000000000 gas\",\"24\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6700000 × 140000000000 gas\",\"27\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6700000 × 140000000000 gas\"},\"0xc51db3339a7603f70b347a0b9680554f777d1f3c\":{\"82\":\"0xc51db3339a7603f70b347a0b9680554f777d1f3c: 0 wei + 4500000 × 10000000000 gas\"},\"0x084dd4aefc6853253573fee9f5fcc23e849d164c\":{\"17\":\"0x084dd4aefc6853253573fee9f5fcc23e849d164c: 0 wei + 28472169 × 1000000008 gas\"}}")]
ResultWrapper<TxPoolInspection> txpool_inspect();
}
[JsonRpcMethod(Description = "Returns a detailed info on tx pool transactions.",
IsImplemented = true,
ExampleResponse = "{\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea\":{\"20\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6721975 × 140000000000 gas\",\"21\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6721975 × 140000000000 gas\",\"22\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6721975 × 140000000000 gas\",\"23\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6700000 × 140000000000 gas\",\"24\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6700000 × 140000000000 gas\",\"27\":\"0xb49928fcb10123e451cfe63aa47edcaea0f8aeea: 0 wei + 6700000 × 140000000000 gas\"},\"0xc51db3339a7603f70b347a0b9680554f777d1f3c\":{\"82\":\"0xc51db3339a7603f70b347a0b9680554f777d1f3c: 0 wei + 4500000 × 10000000000 gas\"},\"0x084dd4aefc6853253573fee9f5fcc23e849d164c\":{\"17\":\"0x084dd4aefc6853253573fee9f5fcc23e849d164c: 0 wei + 28472169 × 1000000008 gas\"}}")]
ResultWrapper<TxPoolInspection> txpool_inspect();
}
Loading
Loading