Skip to content

Commit

Permalink
New filter restricting delegations in txpool (#8022)
Browse files Browse the repository at this point in the history
Co-authored-by: Lukasz Rozmej <[email protected]>
  • Loading branch information
ak88 and LukaszRozmej authored Feb 4, 2025
1 parent ae13ee2 commit 3e6bfba
Show file tree
Hide file tree
Showing 8 changed files with 418 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Core;
using Nethermind.Core.Specs;
using Nethermind.Core.Test.Builders;
using Nethermind.Crypto;
using Nethermind.Db;
using Nethermind.Evm;
using Nethermind.Logging;
using Nethermind.Specs.Forks;
using Nethermind.State;
using Nethermind.Trie.Pruning;
using Nethermind.TxPool.Collections;
using Nethermind.TxPool.Filters;
using NSubstitute;
using NUnit.Framework;
using Org.BouncyCastle.Pqc.Crypto.Lms;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Nethermind.TxPool.Test;
internal class OnlyOneTxPerDelegatedAccountFilterTest
{
[Test]
public void Accept_SenderIsNotDelegated_ReturnsAccepted()
{
IChainHeadSpecProvider headInfoProvider = Substitute.For<IChainHeadSpecProvider>();
headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance);
TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For<IReadOnlyStateProvider>(), new CodeInfoRepository(), new DelegationCache());
Transaction transaction = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
TxFilteringState state = new();

AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None);

Assert.That(result, Is.EqualTo(AcceptTxResult.Accepted));
}

[Test]
public void Accept_SenderIsDelegatedWithNoTransactionsInPool_ReturnsAccepted()
{
IDb stateDb = new MemDb();
IDb codeDb = new MemDb();
TrieStore trieStore = new(stateDb, LimboLogs.Instance);
IWorldState stateProvider = new WorldState(trieStore, codeDb, LimboLogs.Instance);
stateProvider.CreateAccount(TestItem.AddressA, 0);
CodeInfoRepository codeInfoRepository = new();
byte[] code = [.. Eip7702Constants.DelegationHeader, .. TestItem.PrivateKeyA.Address.Bytes];
codeInfoRepository.InsertCode(stateProvider, code, TestItem.AddressA, Prague.Instance);
IChainHeadSpecProvider headInfoProvider = Substitute.For<IChainHeadSpecProvider>();
headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance);
TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, stateProvider, codeInfoRepository, new DelegationCache());
Transaction transaction = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
TxFilteringState state = new();

AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None);

Assert.That(result, Is.EqualTo(AcceptTxResult.Accepted));
}

[Test]
public void Accept_SenderIsDelegatedWithOneTransactionInPoolWithSameNonce_ReturnsAccepted()
{
IChainHeadSpecProvider headInfoProvider = Substitute.For<IChainHeadSpecProvider>();
headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance);
TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
Transaction inPool = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
standardPool.TryInsert(inPool.Hash, inPool);
IDb stateDb = new MemDb();
IDb codeDb = new MemDb();
TrieStore trieStore = new(stateDb, LimboLogs.Instance);
IWorldState stateProvider = new WorldState(trieStore, codeDb, LimboLogs.Instance);
stateProvider.CreateAccount(TestItem.AddressA, 0);
CodeInfoRepository codeInfoRepository = new();
byte[] code = [.. Eip7702Constants.DelegationHeader, .. TestItem.PrivateKeyA.Address.Bytes];
codeInfoRepository.InsertCode(stateProvider, code, TestItem.AddressA, Prague.Instance);
OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, stateProvider, codeInfoRepository, new DelegationCache());
Transaction transaction = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
TxFilteringState state = new();

AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None);

Assert.That(result, Is.EqualTo(AcceptTxResult.Accepted));
}

[Test]
public void Accept_SenderIsDelegatedWithOneTransactionInPoolWithDifferentNonce_ReturnsOnlyOneTxPerDelegatedAccount()
{
IChainHeadSpecProvider headInfoProvider = Substitute.For<IChainHeadSpecProvider>();
headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance);
TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
Transaction inPool = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
standardPool.TryInsert(inPool.Hash, inPool);
IDb stateDb = new MemDb();
IDb codeDb = new MemDb();
TrieStore trieStore = new(stateDb, LimboLogs.Instance);
IWorldState stateProvider = new WorldState(trieStore, codeDb, LimboLogs.Instance);
stateProvider.CreateAccount(TestItem.AddressA, 0);
CodeInfoRepository codeInfoRepository = new();
byte[] code = [.. Eip7702Constants.DelegationHeader, .. TestItem.PrivateKeyA.Address.Bytes];
codeInfoRepository.InsertCode(stateProvider, code, TestItem.AddressA, Prague.Instance);
OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, stateProvider, codeInfoRepository, new DelegationCache());
Transaction transaction = Build.A.Transaction.WithNonce(1).SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
TxFilteringState state = new();

AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None);

Assert.That(result, Is.EqualTo(AcceptTxResult.MoreThanOneTxPerDelegatedAccount));
}

private static object[] EipActiveCases =
{
new object[]{ true, AcceptTxResult.MoreThanOneTxPerDelegatedAccount },
new object[]{ false, AcceptTxResult.Accepted},
};
[TestCaseSource(nameof(EipActiveCases))]
public void Accept_Eip7702IsNotActivated_ReturnsExpected(bool isActive, AcceptTxResult expected)
{
IChainHeadSpecProvider headInfoProvider = Substitute.For<IChainHeadSpecProvider>();
headInfoProvider.GetCurrentHeadSpec().Returns(isActive ? Prague.Instance : Cancun.Instance);
TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
Transaction inPool = Build.A.Transaction.WithNonce(0).SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
standardPool.TryInsert(inPool.Hash, inPool);
IDb stateDb = new MemDb();
IDb codeDb = new MemDb();
TrieStore trieStore = new(stateDb, LimboLogs.Instance);
IWorldState stateProvider = new WorldState(trieStore, codeDb, LimboLogs.Instance);
stateProvider.CreateAccount(TestItem.AddressA, 0);
CodeInfoRepository codeInfoRepository = new();
byte[] code = [.. Eip7702Constants.DelegationHeader, .. TestItem.PrivateKeyA.Address.Bytes];
codeInfoRepository.InsertCode(stateProvider, code, TestItem.AddressA, Prague.Instance);
OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, stateProvider, codeInfoRepository, new DelegationCache());
Transaction transaction = Build.A.Transaction.WithNonce(1).SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
TxFilteringState state = new();

AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None);

Assert.That(result, Is.EqualTo(expected));
}

[Test]
public void Accept_SenderHasPendingDelegation_ReturnsPendingDelegation()
{
IChainHeadSpecProvider headInfoProvider = Substitute.For<IChainHeadSpecProvider>();
headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance);
TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For<IComparer<Transaction>>(), NullLogManager.Instance);
DelegationCache pendingDelegations = new();
pendingDelegations.IncrementDelegationCount(TestItem.AddressA, 0);
OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For<IReadOnlyStateProvider>(), new CodeInfoRepository(), pendingDelegations);
Transaction transaction = Build.A.Transaction.WithNonce(0).SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject;
TxFilteringState state = new();

AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None);

Assert.That(result, Is.EqualTo(AcceptTxResult.PendingDelegation));
}
}
89 changes: 89 additions & 0 deletions src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,95 @@ public void SubmitTx_CodeIsNotDelegationAndDelegation_DelegationIsAccepted((byte
result.Should().Be(testCase.expected);
}

[Test]
public void Delegated_account_can_only_have_one_tx()
{
ISpecProvider specProvider = GetPragueSpecProvider();
TxPoolConfig txPoolConfig = new TxPoolConfig { Size = 30, PersistentBlobStorageSize = 0 };
_txPool = CreatePool(txPoolConfig, specProvider);

PrivateKey signer = TestItem.PrivateKeyA;
_stateProvider.CreateAccount(signer.Address, UInt256.MaxValue);
byte[] delegation = [.. Eip7702Constants.DelegationHeader, .. TestItem.AddressC.Bytes];
_stateProvider.InsertCode(signer.Address, delegation.AsMemory(), Prague.Instance);

Transaction firstTx = Build.A.Transaction
.WithNonce(0)
.WithType(TxType.EIP1559)
.WithMaxFeePerGas(9.GWei())
.WithMaxPriorityFeePerGas(9.GWei())
.WithGasLimit(GasCostOf.Transaction)
.WithTo(TestItem.AddressB)
.SignedAndResolved(_ethereumEcdsa, signer).TestObject;

AcceptTxResult result = _txPool.SubmitTx(firstTx, TxHandlingOptions.PersistentBroadcast);
result.Should().Be(AcceptTxResult.Accepted);

Transaction secondTx = Build.A.Transaction
.WithNonce(1)
.WithType(TxType.EIP1559)
.WithMaxFeePerGas(9.GWei())
.WithMaxPriorityFeePerGas(9.GWei())
.WithGasLimit(GasCostOf.Transaction)
.WithTo(TestItem.AddressB)
.SignedAndResolved(_ethereumEcdsa, signer).TestObject;

result = _txPool.SubmitTx(secondTx, TxHandlingOptions.PersistentBroadcast);

result.Should().Be(AcceptTxResult.MoreThanOneTxPerDelegatedAccount);
}

[TestCase(true)]
[TestCase(false)]
public void Tx_with_pending_delegation_is_rejected_then_is_accepted_after_delegation_removal(bool withRemoval)
{
ISpecProvider specProvider = GetPragueSpecProvider();
TxPoolConfig txPoolConfig = new TxPoolConfig { Size = 30, PersistentBlobStorageSize = 0 };
_txPool = CreatePool(txPoolConfig, specProvider);

PrivateKey signer = TestItem.PrivateKeyA;
_stateProvider.CreateAccount(signer.Address, UInt256.MaxValue);

EthereumEcdsa ecdsa = new EthereumEcdsa(_specProvider.ChainId);

Transaction firstTx = Build.A.Transaction
.WithNonce(0)
.WithType(TxType.SetCode)
.WithMaxFeePerGas(9.GWei())
.WithMaxPriorityFeePerGas(9.GWei())
.WithGasLimit(100_000)
.WithAuthorizationCode(ecdsa.Sign(signer, specProvider.ChainId, TestItem.AddressC, 0))
.WithTo(TestItem.AddressB)
.SignedAndResolved(_ethereumEcdsa, signer).TestObject;

AcceptTxResult result = _txPool.SubmitTx(firstTx, TxHandlingOptions.PersistentBroadcast);
result.Should().Be(AcceptTxResult.Accepted);

Transaction secondTx = Build.A.Transaction
.WithNonce(0)
.WithType(TxType.EIP1559)
.WithMaxFeePerGas(9.GWei())
.WithMaxPriorityFeePerGas(9.GWei())
.WithGasLimit(GasCostOf.Transaction)
.WithTo(TestItem.AddressB)
.SignedAndResolved(_ethereumEcdsa, signer).TestObject;

if (withRemoval)
{
_txPool.RemoveTransaction(firstTx.Hash);

result = _txPool.SubmitTx(secondTx, TxHandlingOptions.PersistentBroadcast);

result.Should().Be(AcceptTxResult.Accepted);
}
else
{
result = _txPool.SubmitTx(secondTx, TxHandlingOptions.PersistentBroadcast);

result.Should().Be(AcceptTxResult.PendingDelegation);
}
}

private IDictionary<ITxPoolPeer, PrivateKey> GetPeers(int limit = 100)
{
var peers = new Dictionary<ITxPoolPeer, PrivateKey>();
Expand Down
10 changes: 10 additions & 0 deletions src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ namespace Nethermind.TxPool
/// </summary>
public static readonly AcceptTxResult MaxTxSizeExceeded = new(16, nameof(MaxTxSizeExceeded));

/// <summary>
/// Only one tx is allowed per delegated account.
/// </summary>
public static readonly AcceptTxResult MoreThanOneTxPerDelegatedAccount = new(17, nameof(MoreThanOneTxPerDelegatedAccount));

/// <summary>
/// There is a pending delegation in the tx pool already
/// </summary>
public static readonly AcceptTxResult PendingDelegation = new(18, nameof(PendingDelegation));

/// <summary>
/// The node is syncing and cannot accept transactions at this time.
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,14 @@ public bool TryGetBucketsWorstValue(TGroupKey groupKey, out TValue? item)
return false;
}

public bool BucketEmptyExcept(TGroupKey groupKey, Func<TValue, bool> predicate)
{
using var lockRelease = Lock.Acquire();
if (_buckets.TryGetValue(groupKey, out EnhancedSortedSet<TValue>? bucket) && bucket.Count > 0)
return bucket.Any(predicate);
return true;
}

protected void EnsureCapacity(int? expectedCapacity = null)
{
expectedCapacity ??= _capacity; // expectedCapacity is added for testing purpose. null should be used in production code
Expand Down
64 changes: 64 additions & 0 deletions src/Nethermind/Nethermind.TxPool/DelegationCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Core;
using Nethermind.Int256;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace Nethermind.TxPool;
internal sealed class DelegationCache
{
private readonly ConcurrentDictionary<UInt256, int> _pendingDelegations = new();

public bool HasPending(AddressAsKey key, UInt256 nonce)
{
return _pendingDelegations.ContainsKey(KeyMask(key, nonce));
}

public void DecrementDelegationCount(AddressAsKey key, UInt256 nonce)
{
InternalIncrement(key, nonce, false);
}
public void IncrementDelegationCount(AddressAsKey key, UInt256 nonce)
{
InternalIncrement(key, nonce, true);
}

private void InternalIncrement(AddressAsKey key, UInt256 nonce, bool increment)
{
UInt256 addressPlusNonce = KeyMask(key, nonce);

int value = increment ? 1 : -1;
var lastCount = _pendingDelegations.AddOrUpdate(addressPlusNonce,
(k) =>
{
if (increment)
return 1;
return 0;
},
(k, c) => c + value);

if (lastCount == 0)
{
//Remove() is threadsafe and only removes if the count is the same as the updated one
((ICollection<KeyValuePair<UInt256, int>>)_pendingDelegations).Remove(
new KeyValuePair<UInt256, int>(addressPlusNonce, lastCount));
}
}

private static UInt256 KeyMask(AddressAsKey key, UInt256 nonce)
{
//A nonce cannot exceed 2^64-1 and an address is 20 bytes, so we can pack them together in one u256
ref byte baseRef = ref key.Value.Bytes[0];
return new UInt256(Unsafe.ReadUnaligned<ulong>(ref baseRef),
Unsafe.ReadUnaligned<ulong>(ref Unsafe.Add(ref baseRef, 8)),
Unsafe.ReadUnaligned<uint>(ref Unsafe.Add(ref baseRef, 16)),
nonce.u1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ internal sealed class MalformedTxFilter(
public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions)
{
IReleaseSpec spec = specProvider.GetCurrentHeadSpec();
if (!txValidator.IsWellFormed(tx, spec))
ValidationResult result = txValidator.IsWellFormed(tx, spec);
if (!result)
{
Metrics.PendingTransactionsMalformed++;
// It may happen that other nodes send us transactions that were signed for another chain or don't have enough gas.
Expand Down
Loading

0 comments on commit 3e6bfba

Please sign in to comment.