Skip to content

Commit c2b16b7

Browse files
Avoid bucket snapshot allocation in BalanceTooLowFilter (#11686)
* Avoid bucket snapshot allocation in BalanceTooLowFilter * fix BucketBalanceState nonce type, hoist null checks, add VisitBucket tests - BucketBalanceState fields are UInt256 to match Transaction.Nonce / AccountStruct.Nonce; the previous long fields did not compile. - Move ArgumentNullException.ThrowIfNull calls in VisitBucket before lock acquisition. - Add SortedPoolTests covering VisitBucket: missing group, full iteration, early stop, ref-state mutation, null-visitor guard. * drop cumulativeCost local; parameterize VisitBucket tests - Write the final tx-cost accumulation directly into BucketBalanceState.CumulativeCost instead of a separate local. - Collapse VisitBucket iteration tests into a single [TestCaseSource] (missing group / full iteration / early stop) and drop the redundant ref-state-mutation test (already covered by accumulating into List<int>). --------- Co-authored-by: lukasz.rozmej <lukasz.rozmej@gmail.com>
1 parent b98d427 commit c2b16b7

3 files changed

Lines changed: 101 additions & 20 deletions

File tree

src/Nethermind/Nethermind.TxPool.Test/Collections/SortedPoolTests.cs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-License-Identifier: LGPL-3.0-only
33

44
using System;
5+
using System.Collections.Generic;
56
using FluentAssertions;
67
using Nethermind.Blockchain;
78
using Nethermind.Consensus.Comparers;
@@ -104,5 +105,52 @@ public void should_remove_empty_buckets()
104105
_sortedPool.TryRemove(tx.Hash);
105106
_sortedPool.TryGetBucket(tx.SenderAddress, out _).Should().BeFalse();
106107
}
108+
109+
private static IEnumerable<TestCaseData> VisitBucketCases()
110+
{
111+
yield return new TestCaseData(Array.Empty<int>(), int.MaxValue, Array.Empty<int>())
112+
.SetName("VisitBucket_missing_group_visits_nothing");
113+
yield return new TestCaseData(new[] { 0, 1, 2, 3 }, int.MaxValue, new[] { 0, 1, 2, 3 })
114+
.SetName("VisitBucket_iterates_all_items_in_ascending_nonce_order");
115+
yield return new TestCaseData(new[] { 0, 1, 2, 3 }, 2, new[] { 0, 1, 2 })
116+
.SetName("VisitBucket_stops_after_visitor_returns_false");
117+
}
118+
119+
[TestCaseSource(nameof(VisitBucketCases))]
120+
public void VisitBucket_visits_expected_nonces(int[] insertNonces, int stopAfterNonce, int[] expectedVisited)
121+
{
122+
InsertNonces(TestItem.AddressA, insertNonces);
123+
124+
(List<int> Visited, int StopAfter) state = (new List<int>(), stopAfterNonce);
125+
_sortedPool.VisitBucket(TestItem.AddressA, ref state, static (Transaction tx, ref (List<int> Visited, int StopAfter) s) =>
126+
{
127+
s.Visited.Add((int)tx.Nonce);
128+
return (int)tx.Nonce < s.StopAfter;
129+
});
130+
131+
state.Visited.Should().Equal(expectedVisited);
132+
}
133+
134+
[Test]
135+
public void VisitBucket_throws_on_null_visitor()
136+
{
137+
int unused = 0;
138+
Action act = () => _sortedPool.VisitBucket(TestItem.AddressA, ref unused, null!);
139+
140+
act.Should().Throw<ArgumentNullException>();
141+
}
142+
143+
private void InsertNonces(Address sender, ReadOnlySpan<int> nonces)
144+
{
145+
foreach (int nonce in nonces)
146+
{
147+
Transaction tx = Build.A.Transaction
148+
.WithNonce((UInt256)nonce)
149+
.WithSenderAddress(sender)
150+
.TestObject;
151+
tx.Hash = tx.CalculateHash();
152+
_sortedPool.TryInsert(tx.Hash, tx);
153+
}
154+
}
107155
}
108156
}

src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public abstract partial class SortedPool<TKey, TValue, TGroupKey>
2323
where TKey : notnull
2424
where TGroupKey : notnull
2525
{
26+
internal delegate bool BucketVisitor<TState>(TValue value, ref TState state);
27+
2628
protected McsLock Lock { get; } = new();
2729

2830
private readonly int _capacity;
@@ -548,6 +550,34 @@ public bool TryGetBucketsWorstValue(TGroupKey groupKey, out TValue? item)
548550
return false;
549551
}
550552

553+
/// <summary>
554+
/// Iterates over bucket items under lock until visitor returns false.
555+
/// </summary>
556+
/// <remarks>
557+
/// The visitor runs while the pool lock is held, so it must be short and must not call back into the pool.
558+
/// Items are visited in the pool's group comparer order (ascending nonce for the tx pool).
559+
/// </remarks>
560+
internal void VisitBucket<TState>(TGroupKey groupKey, ref TState state, BucketVisitor<TState> visitor)
561+
{
562+
ArgumentNullException.ThrowIfNull(groupKey);
563+
ArgumentNullException.ThrowIfNull(visitor);
564+
565+
using McsLock.Disposable lockRelease = Lock.Acquire();
566+
567+
if (!_buckets.TryGetValue(groupKey, out EnhancedSortedSet<TValue>? bucket))
568+
{
569+
return;
570+
}
571+
572+
foreach (TValue value in bucket)
573+
{
574+
if (!visitor(value, ref state))
575+
{
576+
break;
577+
}
578+
}
579+
}
580+
551581
public bool BucketAny(TGroupKey groupKey, Func<TValue, bool> predicate)
552582
{
553583
using McsLock.Disposable lockRelease = Lock.Acquire();

src/Nethermind/Nethermind.TxPool/Filters/BalanceTooLowFilter.cs

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@ namespace Nethermind.TxPool.Filters
1313
/// </summary>
1414
internal sealed class BalanceTooLowFilter(TxDistinctSortedPool txs, TxDistinctSortedPool blobTxs, ILogger logger) : IIncomingTxFilter
1515
{
16+
private struct BucketBalanceState(UInt256 accountNonce, UInt256 txNonce)
17+
{
18+
public readonly UInt256 AccountNonce = accountNonce;
19+
public readonly UInt256 TxNonce = txNonce;
20+
public UInt256 CumulativeCost = UInt256.Zero;
21+
public bool Overflow = false;
22+
}
23+
1624
private readonly TxDistinctSortedPool _txs = txs;
1725
private readonly TxDistinctSortedPool _blobTxs = blobTxs;
1826
private readonly ILogger _logger = logger;
@@ -27,32 +35,27 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl
2735
AccountStruct account = state.SenderAccount;
2836
UInt256 balance = account.Balance;
2937

30-
UInt256 cumulativeCost = UInt256.Zero;
31-
bool overflow = false;
32-
Transaction[] sameTypeTxs = tx.SupportsBlobs
33-
? _blobTxs.GetBucketSnapshot(tx.SenderAddress!) // it will create a snapshot of light txs (without actual blobs)
34-
: _txs.GetBucketSnapshot(tx.SenderAddress!);
38+
BucketBalanceState bucketBalanceState = new(account.Nonce, tx.Nonce);
39+
TxDistinctSortedPool pool = tx.SupportsBlobs ? _blobTxs : _txs;
3540
// tx.SenderAddress! as unknownSenderFilter will run before this one
36-
37-
for (int i = 0; i < sameTypeTxs.Length; i++)
41+
pool.VisitBucket(tx.SenderAddress!, ref bucketBalanceState, static (Transaction otherTx, ref BucketBalanceState bucketState) =>
3842
{
39-
Transaction otherTx = sameTypeTxs[i];
40-
if (otherTx.Nonce < account.Nonce)
43+
if (otherTx.Nonce < bucketState.AccountNonce)
4144
{
42-
continue;
45+
return true;
4346
}
4447

45-
if (otherTx.Nonce < tx.Nonce)
48+
if (otherTx.Nonce >= bucketState.TxNonce)
4649
{
47-
overflow |= otherTx.IsOverflowWhenAddingTxCostToCumulative(cumulativeCost, out cumulativeCost);
50+
return false;
4851
}
49-
else
50-
{
51-
break;
52-
}
53-
}
5452

55-
overflow |= tx.IsOverflowWhenAddingTxCostToCumulative(cumulativeCost, out cumulativeCost);
53+
bucketState.Overflow |= otherTx.IsOverflowWhenAddingTxCostToCumulative(bucketState.CumulativeCost, out bucketState.CumulativeCost);
54+
return true;
55+
});
56+
57+
bool overflow = bucketBalanceState.Overflow;
58+
overflow |= tx.IsOverflowWhenAddingTxCostToCumulative(bucketBalanceState.CumulativeCost, out bucketBalanceState.CumulativeCost);
5659

5760
if (overflow)
5861
{
@@ -61,7 +64,7 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl
6164
return AcceptTxResult.Int256Overflow;
6265
}
6366

64-
if (balance < cumulativeCost)
67+
if (balance < bucketBalanceState.CumulativeCost)
6568
{
6669
Metrics.PendingTransactionsTooLowBalance++;
6770

@@ -73,7 +76,7 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl
7376
bool isNotLocal = (handlingOptions & TxHandlingOptions.PersistentBroadcast) == 0;
7477
return isNotLocal ?
7578
AcceptTxResult.InsufficientFunds :
76-
AcceptTxResult.InsufficientFunds.WithMessage($"Account balance: {balance}, cumulative cost: {cumulativeCost}");
79+
AcceptTxResult.InsufficientFunds.WithMessage($"Account balance: {balance}, cumulative cost: {bucketBalanceState.CumulativeCost}");
7780
}
7881

7982
return AcceptTxResult.Accepted;

0 commit comments

Comments
 (0)