diff --git a/src/Nethermind/Nethermind.TxPool.Test/OnlyOneTxPerDelegatedAccountFilterTest.cs b/src/Nethermind/Nethermind.TxPool.Test/DelegatedAccountFilterTest.cs similarity index 62% rename from src/Nethermind/Nethermind.TxPool.Test/OnlyOneTxPerDelegatedAccountFilterTest.cs rename to src/Nethermind/Nethermind.TxPool.Test/DelegatedAccountFilterTest.cs index b1751f03c23..edbe648a889 100644 --- a/src/Nethermind/Nethermind.TxPool.Test/OnlyOneTxPerDelegatedAccountFilterTest.cs +++ b/src/Nethermind/Nethermind.TxPool.Test/DelegatedAccountFilterTest.cs @@ -1,7 +1,9 @@ // SPDX-FileCopyrightText: 2024 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only +using Nethermind.Consensus; using Nethermind.Core; +using Nethermind.Core.Extensions; using Nethermind.Core.Specs; using Nethermind.Core.Test.Builders; using Nethermind.Crypto; @@ -24,7 +26,7 @@ using System.Threading.Tasks; namespace Nethermind.TxPool.Test; -internal class OnlyOneTxPerDelegatedAccountFilterTest +internal class DelegatedAccountFilterTest { [Test] public void Accept_SenderIsNotDelegated_ReturnsAccepted() @@ -33,7 +35,7 @@ public void Accept_SenderIsNotDelegated_ReturnsAccepted() headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance); TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For>(), NullLogManager.Instance); TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For>(), NullLogManager.Instance); - OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For(), new CodeInfoRepository(), new DelegationCache()); + DelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For(), new CodeInfoRepository(), new DelegationCache()); Transaction transaction = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject; TxFilteringState state = new(); @@ -57,9 +59,9 @@ public void Accept_SenderIsDelegatedWithNoTransactionsInPool_ReturnsAccepted() headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance); TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For>(), NullLogManager.Instance); TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For>(), NullLogManager.Instance); - OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, stateProvider, codeInfoRepository, new DelegationCache()); + DelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, stateProvider, codeInfoRepository, new DelegationCache()); Transaction transaction = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject; - TxFilteringState state = new(); + TxFilteringState state = new(transaction, stateProvider); AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None); @@ -83,9 +85,9 @@ public void Accept_SenderIsDelegatedWithOneTransactionInPoolWithSameNonce_Return 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()); + DelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, stateProvider, codeInfoRepository, new DelegationCache()); Transaction transaction = Build.A.Transaction.SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject; - TxFilteringState state = new(); + TxFilteringState state = new(transaction, stateProvider); AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None); @@ -93,7 +95,7 @@ public void Accept_SenderIsDelegatedWithOneTransactionInPoolWithSameNonce_Return } [Test] - public void Accept_SenderIsDelegatedWithOneTransactionInPoolWithDifferentNonce_ReturnsOnlyOneTxPerDelegatedAccount() + public void Accept_SenderIsDelegatedWithOneTransactionInPoolWithDifferentNonce_ReturnsFutureNonceForDelegatedAccount() { IChainHeadSpecProvider headInfoProvider = Substitute.For(); headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance); @@ -109,18 +111,18 @@ public void Accept_SenderIsDelegatedWithOneTransactionInPoolWithDifferentNonce_R 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()); + DelegatedAccountFilter 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(); + TxFilteringState state = new(transaction, stateProvider); AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None); - Assert.That(result, Is.EqualTo(AcceptTxResult.MoreThanOneTxPerDelegatedAccount)); + Assert.That(result, Is.EqualTo(AcceptTxResult.FutureNonceForDelegatedAccount)); } private static object[] EipActiveCases = { - new object[]{ true, AcceptTxResult.MoreThanOneTxPerDelegatedAccount }, + new object[]{ true, AcceptTxResult.FutureNonceForDelegatedAccount }, new object[]{ false, AcceptTxResult.Accepted}, }; [TestCaseSource(nameof(EipActiveCases))] @@ -140,9 +142,9 @@ public void Accept_Eip7702IsNotActivated_ReturnsExpected(bool isActive, AcceptTx 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()); + DelegatedAccountFilter 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(); + TxFilteringState state = new(transaction, stateProvider); AcceptTxResult result = filter.Accept(transaction, ref state, TxHandlingOptions.None); @@ -157,8 +159,8 @@ public void Accept_SenderHasPendingDelegation_ReturnsPendingDelegation() TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For>(), NullLogManager.Instance); TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For>(), NullLogManager.Instance); DelegationCache pendingDelegations = new(); - pendingDelegations.IncrementDelegationCount(TestItem.AddressA, 0); - OnlyOneTxPerDelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For(), new CodeInfoRepository(), pendingDelegations); + pendingDelegations.IncrementDelegationCount(TestItem.AddressA); + DelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For(), new CodeInfoRepository(), pendingDelegations); Transaction transaction = Build.A.Transaction.WithNonce(0).SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject; TxFilteringState state = new(); @@ -166,4 +168,74 @@ public void Accept_SenderHasPendingDelegation_ReturnsPendingDelegation() Assert.That(result, Is.EqualTo(AcceptTxResult.PendingDelegation)); } + + [TestCase(true)] + [TestCase(false)] + public void Accept_AuthorityHasPendingTransaction_ReturnsDelegatorHasPendingTx(bool useBlobPool) + { + IChainHeadSpecProvider headInfoProvider = Substitute.For(); + headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance); + TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For>(), NullLogManager.Instance); + TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For>(), NullLogManager.Instance); + DelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For(), new CodeInfoRepository(), new()); + Transaction transaction; + if (useBlobPool) + { + transaction + = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .SignedAndResolved(TestItem.PrivateKeyA).TestObject; + blobPool.TryInsert(transaction.Hash, transaction, out _); + } + else + { + transaction + = Build.A.Transaction + .WithNonce(0) + .SignedAndResolved(TestItem.PrivateKeyA).TestObject; + standardPool.TryInsert(transaction.Hash, transaction, out _); + } + TxFilteringState state = new(); + EthereumEcdsa ecdsa = new EthereumEcdsa(0); + AuthorizationTuple authTuple = new AuthorizationTuple(0, TestItem.AddressB, 0, new Core.Crypto.Signature(0, 0, 27), TestItem.AddressA); + Transaction setCodeTx = Build.A.Transaction + .WithType(TxType.SetCode) + .WithAuthorizationCode(authTuple) + .SignedAndResolved(TestItem.PrivateKeyB) + .TestObject; + + AcceptTxResult setCodeTxResult = filter.Accept(setCodeTx, ref state, TxHandlingOptions.None); + + Assert.That(setCodeTxResult, Is.EqualTo(AcceptTxResult.DelegatorHasPendingTx)); + } + + [Test] + public void Accept_SetCodeTxHasAuthorityWithPendingTx_ReturnsDelegatorHasPendingTx() + { + IChainHeadSpecProvider headInfoProvider = Substitute.For(); + headInfoProvider.GetCurrentHeadSpec().Returns(Prague.Instance); + TxDistinctSortedPool standardPool = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, Substitute.For>(), NullLogManager.Instance); + TxDistinctSortedPool blobPool = new BlobTxDistinctSortedPool(10, Substitute.For>(), NullLogManager.Instance); + DelegationCache pendingDelegations = new(); + pendingDelegations.IncrementDelegationCount(TestItem.AddressA); + DelegatedAccountFilter filter = new(headInfoProvider, standardPool, blobPool, Substitute.For(), new CodeInfoRepository(), pendingDelegations); + Transaction transaction = Build.A.Transaction + .WithNonce(1) + .SignedAndResolved(new EthereumEcdsa(0), TestItem.PrivateKeyA).TestObject; + standardPool.TryInsert(transaction.Hash, transaction); + Transaction setCodeTransaction = Build.A.Transaction + .WithNonce(0) + .WithType(TxType.SetCode) + .WithMaxFeePerGas(9.GWei()) + .WithMaxPriorityFeePerGas(9.GWei()) + .WithGasLimit(100_000) + .WithAuthorizationCode(new AuthorizationTuple(0, TestItem.AddressC, 0, new Core.Crypto.Signature(new byte[64], 0), TestItem.AddressA)) + .WithTo(TestItem.AddressB) + .SignedAndResolved(TestItem.PrivateKeyB).TestObject; + TxFilteringState state = new(); + + AcceptTxResult result = filter.Accept(setCodeTransaction, ref state, TxHandlingOptions.None); + + Assert.That(result, Is.EqualTo(AcceptTxResult.DelegatorHasPendingTx)); + } } diff --git a/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs index b91e450f83b..39731ede34a 100644 --- a/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs +++ b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs @@ -1748,7 +1748,7 @@ public void Should_correctly_add_tx_to_local_pool_when_underpaid([Values] TxType yield return ([.. Eip7702Constants.DelegationHeader, .. new byte[20]], AcceptTxResult.Accepted); } [TestCaseSource(nameof(CodeCases))] - public void SubmitTx_CodeIsNotDelegationAndDelegation_DelegationIsAccepted((byte[] code, AcceptTxResult expected) testCase) + public void Sender_account_has_delegation_and_normal_code((byte[] code, AcceptTxResult expected) testCase) { ISpecProvider specProvider = GetPragueSpecProvider(); TxPoolConfig txPoolConfig = new TxPoolConfig { Size = 30, PersistentBlobStorageSize = 0 }; @@ -1770,8 +1770,16 @@ public void SubmitTx_CodeIsNotDelegationAndDelegation_DelegationIsAccepted((byte result.Should().Be(testCase.expected); } - [Test] - public void Delegated_account_can_only_have_one_tx() + private static IEnumerable DifferentOrderNonces() + { + yield return new object[] { 0, 1, AcceptTxResult.Accepted, AcceptTxResult.FutureNonceForDelegatedAccount }; + yield return new object[] { 2, 5, AcceptTxResult.FutureNonceForDelegatedAccount, AcceptTxResult.FutureNonceForDelegatedAccount }; + yield return new object[] { 1, 0, AcceptTxResult.FutureNonceForDelegatedAccount, AcceptTxResult.Accepted }; + yield return new object[] { 5, 0, AcceptTxResult.FutureNonceForDelegatedAccount, AcceptTxResult.Accepted }; + } + + [TestCaseSource(nameof(DifferentOrderNonces))] + public void Delegated_account_can_only_have_one_tx_with_current_account_nonce(int firstNonce, int secondNonce, AcceptTxResult firstExpectation, AcceptTxResult secondExpectation) { ISpecProvider specProvider = GetPragueSpecProvider(); TxPoolConfig txPoolConfig = new TxPoolConfig { Size = 30, PersistentBlobStorageSize = 0 }; @@ -1783,7 +1791,7 @@ public void Delegated_account_can_only_have_one_tx() _stateProvider.InsertCode(signer.Address, delegation.AsMemory(), Prague.Instance); Transaction firstTx = Build.A.Transaction - .WithNonce(0) + .WithNonce((UInt256)firstNonce) .WithType(TxType.EIP1559) .WithMaxFeePerGas(9.GWei()) .WithMaxPriorityFeePerGas(9.GWei()) @@ -1792,10 +1800,10 @@ public void Delegated_account_can_only_have_one_tx() .SignedAndResolved(_ethereumEcdsa, signer).TestObject; AcceptTxResult result = _txPool.SubmitTx(firstTx, TxHandlingOptions.PersistentBroadcast); - result.Should().Be(AcceptTxResult.Accepted); + result.Should().Be(firstExpectation); Transaction secondTx = Build.A.Transaction - .WithNonce(1) + .WithNonce((UInt256)secondNonce) .WithType(TxType.EIP1559) .WithMaxFeePerGas(9.GWei()) .WithMaxPriorityFeePerGas(9.GWei()) @@ -1805,19 +1813,21 @@ public void Delegated_account_can_only_have_one_tx() result = _txPool.SubmitTx(secondTx, TxHandlingOptions.PersistentBroadcast); - result.Should().Be(AcceptTxResult.MoreThanOneTxPerDelegatedAccount); + result.Should().Be(secondExpectation); } [TestCase(true)] [TestCase(false)] - public void Tx_with_pending_delegation_is_rejected_then_is_accepted_after_delegation_removal(bool withRemoval) + public void Tx_with_conflicting_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; + PrivateKey sponsor = TestItem.PrivateKeyB; _stateProvider.CreateAccount(signer.Address, UInt256.MaxValue); + _stateProvider.CreateAccount(sponsor.Address, UInt256.MaxValue); EthereumEcdsa ecdsa = new EthereumEcdsa(_specProvider.ChainId); @@ -1829,12 +1839,50 @@ public void Tx_with_pending_delegation_is_rejected_then_is_accepted_after_delega .WithGasLimit(100_000) .WithAuthorizationCode(ecdsa.Sign(signer, specProvider.ChainId, TestItem.AddressC, 0)) .WithTo(TestItem.AddressB) - .SignedAndResolved(_ethereumEcdsa, signer).TestObject; + .SignedAndResolved(_ethereumEcdsa, sponsor).TestObject; AcceptTxResult result = _txPool.SubmitTx(firstTx, TxHandlingOptions.PersistentBroadcast); result.Should().Be(AcceptTxResult.Accepted); Transaction secondTx = Build.A.Transaction + .WithNonce(0) + .WithType(TxType.EIP1559) + .WithMaxFeePerGas(12.GWei()) + .WithMaxPriorityFeePerGas(12.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); + } + } + + [TestCase(true)] + [TestCase(false)] + public void SetCode_tx_has_authority_with_pending_transaction_is_rejected_then_is_accepted_after_tx_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.EIP1559) .WithMaxFeePerGas(9.GWei()) @@ -1843,6 +1891,19 @@ public void Tx_with_pending_delegation_is_rejected_then_is_accepted_after_delega .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.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; + if (withRemoval) { _txPool.RemoveTransaction(firstTx.Hash); @@ -1855,10 +1916,101 @@ public void Tx_with_pending_delegation_is_rejected_then_is_accepted_after_delega { result = _txPool.SubmitTx(secondTx, TxHandlingOptions.PersistentBroadcast); - result.Should().Be(AcceptTxResult.PendingDelegation); + result.Should().Be(AcceptTxResult.DelegatorHasPendingTx); } } + private static IEnumerable SetCodeReplacedTxCases() + { + yield return new object[] + { + //Not self sponsored + TestItem.PrivateKeyB, + (IWorldState state, Address account, IReleaseSpec spec) => + { + state.CreateAccount(account, UInt256.MaxValue); + state.CreateAccount(TestItem.AddressB, UInt256.MaxValue); + }, + AcceptTxResult.Accepted + }; + yield return new object[] + { + //Self sponsored + TestItem.PrivateKeyA, + (IWorldState state, Address account, IReleaseSpec spec) => + { + state.CreateAccount(account, UInt256.MaxValue); + }, + AcceptTxResult.Accepted + }; + yield return new object[] + { + //Self sponsored + TestItem.PrivateKeyA, + //Account is delegated so the last transaction should not be accepted + (IWorldState state, Address account, IReleaseSpec spec) => + { + state.CreateAccount(account, UInt256.MaxValue); + byte[] delegation = [..Eip7702Constants.DelegationHeader, ..TestItem.AddressB.Bytes]; + state.InsertCode(account, delegation, spec); + }, + AcceptTxResult.FutureNonceForDelegatedAccount + }; + } + + [TestCaseSource(nameof(SetCodeReplacedTxCases))] + public void SetCode_tx_can_be_replaced_itself_and_remove_pending_delegation_restriction( + PrivateKey sponsor, Action accountSetup, AcceptTxResult lastExpectation) + { + ISpecProvider specProvider = GetPragueSpecProvider(); + TxPoolConfig txPoolConfig = new TxPoolConfig { Size = 30, PersistentBlobStorageSize = 0 }; + _txPool = CreatePool(txPoolConfig, specProvider); + + PrivateKey signer = TestItem.PrivateKeyA; + accountSetup(_stateProvider, signer.Address, Prague.Instance); + + EthereumEcdsa ecdsa = new EthereumEcdsa(_specProvider.ChainId); + + Transaction firstSetcodeTx = 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, sponsor).TestObject; + + AcceptTxResult result = _txPool.SubmitTx(firstSetcodeTx, TxHandlingOptions.PersistentBroadcast); + result.Should().Be(AcceptTxResult.Accepted); + + Transaction replacementTx = Build.A.Transaction + .WithNonce(0) + .WithType(TxType.EIP1559) + .WithMaxFeePerGas(12.GWei()) + .WithMaxPriorityFeePerGas(12.GWei()) + .WithGasLimit(GasCostOf.Transaction) + .WithTo(TestItem.AddressB) + .SignedAndResolved(_ethereumEcdsa, sponsor).TestObject; + + result = _txPool.SubmitTx(replacementTx, TxHandlingOptions.PersistentBroadcast); + + result.Should().Be(AcceptTxResult.Accepted); + + Transaction thirdTx = 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(thirdTx, TxHandlingOptions.PersistentBroadcast); + + result.Should().Be(lastExpectation); + } + private IDictionary GetPeers(int limit = 100) { var peers = new Dictionary(); diff --git a/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs b/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs index f707264a6d3..0e50dfa3916 100644 --- a/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs +++ b/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs @@ -96,15 +96,20 @@ namespace Nethermind.TxPool public static readonly AcceptTxResult MaxTxSizeExceeded = new(16, nameof(MaxTxSizeExceeded)); /// - /// Only one tx is allowed per delegated account. + /// Only one tx with current state matching nonce is allowed per delegated account. /// - public static readonly AcceptTxResult MoreThanOneTxPerDelegatedAccount = new(17, nameof(MoreThanOneTxPerDelegatedAccount)); + public static readonly AcceptTxResult FutureNonceForDelegatedAccount = new(17, nameof(FutureNonceForDelegatedAccount)); /// /// There is a pending delegation in the tx pool already /// public static readonly AcceptTxResult PendingDelegation = new(18, nameof(PendingDelegation)); + /// + /// There is a pending transaction from a delegation in the tx pool already. + /// + public static readonly AcceptTxResult DelegatorHasPendingTx = new(19, nameof(DelegatorHasPendingTx)); + /// /// The node is syncing and cannot accept transactions at this time. /// diff --git a/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs b/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs index 49d9e53fad0..2ea40b6ea77 100644 --- a/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs +++ b/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs @@ -530,12 +530,11 @@ public bool TryGetBucketsWorstValue(TGroupKey groupKey, out TValue? item) return false; } - public bool BucketEmptyExcept(TGroupKey groupKey, Func predicate) + public bool BucketAny(TGroupKey groupKey, Func predicate) { using var lockRelease = Lock.Acquire(); - if (_buckets.TryGetValue(groupKey, out EnhancedSortedSet? bucket) && bucket.Count > 0) - return bucket.Any(predicate); - return true; + return _buckets.TryGetValue(groupKey, out EnhancedSortedSet? bucket) + && bucket.Any(predicate); } protected void EnsureCapacity(int? expectedCapacity = null) diff --git a/src/Nethermind/Nethermind.TxPool/DelegationCache.cs b/src/Nethermind/Nethermind.TxPool/DelegationCache.cs index f9a9be94475..1beba2074dc 100644 --- a/src/Nethermind/Nethermind.TxPool/DelegationCache.cs +++ b/src/Nethermind/Nethermind.TxPool/DelegationCache.cs @@ -14,28 +14,26 @@ namespace Nethermind.TxPool; internal sealed class DelegationCache { - private readonly ConcurrentDictionary _pendingDelegations = new(); + private readonly ConcurrentDictionary _pendingDelegations = new(); - public bool HasPending(AddressAsKey key, UInt256 nonce) + public bool HasPending(AddressAsKey key) { - return _pendingDelegations.ContainsKey(KeyMask(key, nonce)); + return _pendingDelegations.ContainsKey(key); } - public void DecrementDelegationCount(AddressAsKey key, UInt256 nonce) + public void DecrementDelegationCount(AddressAsKey key) { - InternalIncrement(key, nonce, false); + InternalIncrement(key, false); } - public void IncrementDelegationCount(AddressAsKey key, UInt256 nonce) + public void IncrementDelegationCount(AddressAsKey key) { - InternalIncrement(key, nonce, true); + InternalIncrement(key, true); } - private void InternalIncrement(AddressAsKey key, UInt256 nonce, bool increment) + private void InternalIncrement(AddressAsKey key, bool increment) { - UInt256 addressPlusNonce = KeyMask(key, nonce); - int value = increment ? 1 : -1; - var lastCount = _pendingDelegations.AddOrUpdate(addressPlusNonce, + var lastCount = _pendingDelegations.AddOrUpdate(key, (k) => { if (increment) @@ -47,18 +45,8 @@ private void InternalIncrement(AddressAsKey key, UInt256 nonce, bool increment) if (lastCount == 0) { //Remove() is threadsafe and only removes if the count is the same as the updated one - ((ICollection>)_pendingDelegations).Remove( - new KeyValuePair(addressPlusNonce, lastCount)); + ((ICollection>)_pendingDelegations).Remove( + new KeyValuePair(key, 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(ref baseRef), - Unsafe.ReadUnaligned(ref Unsafe.Add(ref baseRef, 8)), - Unsafe.ReadUnaligned(ref Unsafe.Add(ref baseRef, 16)), - nonce.u1); - } } diff --git a/src/Nethermind/Nethermind.TxPool/Filters/DelegatedAccountFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/DelegatedAccountFilter.cs new file mode 100644 index 00000000000..d5d78f2f9e1 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Filters/DelegatedAccountFilter.cs @@ -0,0 +1,70 @@ +using Nethermind.Core; +using Nethermind.Core.Specs; +using Nethermind.Evm; +using Nethermind.Int256; +using Nethermind.State; +using Nethermind.TxPool.Collections; +using System.Collections.Concurrent; +using System.Linq; + +namespace Nethermind.TxPool.Filters +{ + internal sealed class DelegatedAccountFilter( + IChainHeadSpecProvider specProvider, + TxDistinctSortedPool standardPool, + TxDistinctSortedPool blobPool, + IReadOnlyStateProvider worldState, + ICodeInfoRepository codeInfoRepository, + DelegationCache pendingDelegations) : IIncomingTxFilter + { + public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions) + { + IReleaseSpec spec = specProvider.GetCurrentHeadSpec(); + if (!spec.IsEip7702Enabled) + return AcceptTxResult.Accepted; + + if (tx.HasAuthorizationList && AuthorityHasPendingTx(tx.AuthorizationList)) + return AcceptTxResult.DelegatorHasPendingTx; + + if (pendingDelegations.HasPending(tx.SenderAddress!)) + { + //Check if the sender has a self-sponsored SetCode transaction with same nonce. + //If he does then this is a replacement tx and should be accepted + if (!standardPool.BucketAny(tx.SenderAddress!, + t => t.Nonce == tx.Nonce + && t.HasAuthorizationList + && t.AuthorizationList.Any(tuple => tuple.Authority == tx.SenderAddress))) + { + return AcceptTxResult.PendingDelegation; + } + } + + if (!codeInfoRepository.TryGetDelegation(worldState, tx.SenderAddress!, out _)) + return AcceptTxResult.Accepted; + //If the account is delegated we only accept the next transaction nonce + if (state.SenderAccount.Nonce != tx.Nonce) + { + return AcceptTxResult.FutureNonceForDelegatedAccount; + } + return AcceptTxResult.Accepted; + } + + private bool AuthorityHasPendingTx(AuthorizationTuple[] authorizations) + { + foreach (AuthorizationTuple authorization in authorizations) + { + //RecoverAuthorityFilter runs before this, so if a signature is null, we assume it is bad + if (authorization.Authority is null) + { + continue; + } + if (standardPool.ContainsBucket(authorization.Authority) + || blobPool.ContainsBucket(authorization.Authority)) + { + return true; + } + } + return false; + } + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Filters/OnlyOneTxPerDelegatedAccountFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/OnlyOneTxPerDelegatedAccountFilter.cs deleted file mode 100644 index e45f477e245..00000000000 --- a/src/Nethermind/Nethermind.TxPool/Filters/OnlyOneTxPerDelegatedAccountFilter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Nethermind.Core; -using Nethermind.Core.Specs; -using Nethermind.Evm; -using Nethermind.Int256; -using Nethermind.State; -using Nethermind.TxPool.Collections; -using System.Collections.Concurrent; - -namespace Nethermind.TxPool.Filters -{ - internal sealed class OnlyOneTxPerDelegatedAccountFilter( - IChainHeadSpecProvider specProvider, - TxDistinctSortedPool standardPool, - TxDistinctSortedPool blobPool, - IReadOnlyStateProvider worldState, - ICodeInfoRepository codeInfoRepository, - DelegationCache pendingDelegations) : IIncomingTxFilter - { - public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions) - { - IReleaseSpec spec = specProvider.GetCurrentHeadSpec(); - if (!spec.IsEip7702Enabled) - return AcceptTxResult.Accepted; - - if (pendingDelegations.HasPending(tx.SenderAddress!, tx.Nonce)) - return AcceptTxResult.PendingDelegation; - - if (!codeInfoRepository.TryGetDelegation(worldState, tx.SenderAddress!, out _)) - return AcceptTxResult.Accepted; - //Transactios from the same source can only be either blob transactions or other type - if (tx.SupportsBlobs ? !blobPool.BucketEmptyExcept(tx.SenderAddress!, (t) => t.Nonce == tx.Nonce) - : !standardPool.BucketEmptyExcept(tx.SenderAddress!, (t) => t.Nonce == tx.Nonce)) - { - return AcceptTxResult.MoreThanOneTxPerDelegatedAccount; - } - return AcceptTxResult.Accepted; - } - } -} diff --git a/src/Nethermind/Nethermind.TxPool/TxPool.cs b/src/Nethermind/Nethermind.TxPool/TxPool.cs index 5bce5d6ac21..471d5a7a15c 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPool.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPool.cs @@ -120,6 +120,8 @@ public TxPool(IEthereumEcdsa ecdsa, TxPoolHeadChanged += _broadcaster.OnNewHead; _transactions = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, comparer, logManager); + _transactions.Removed += OnRemovedTx; + _blobTransactions = txPoolConfig.BlobsSupport.IsPersistentStorage() ? new PersistentBlobTxDistinctSortedPool(blobTxStorage, _txPoolConfig, comparer, logManager) : new BlobTxDistinctSortedPool(txPoolConfig.BlobsSupport == BlobsSupportMode.InMemory ? _txPoolConfig.InMemoryBlobPoolSize : 0, comparer, logManager); @@ -151,7 +153,7 @@ public TxPool(IEthereumEcdsa ecdsa, new FutureNonceFilter(txPoolConfig), new GapNonceFilter(_transactions, _blobTransactions, _logger), new RecoverAuthorityFilter(ecdsa), - new OnlyOneTxPerDelegatedAccountFilter(_specProvider, _transactions, _blobTransactions, chainHeadInfoProvider.ReadOnlyStateProvider, chainHeadInfoProvider.CodeInfoRepository, _pendingDelegations), + new DelegatedAccountFilter(_specProvider, _transactions, _blobTransactions, chainHeadInfoProvider.ReadOnlyStateProvider, chainHeadInfoProvider.CodeInfoRepository, _pendingDelegations), ]; if (incomingTxFilter is not null) @@ -198,6 +200,10 @@ public bool TryGetBlobAndProof(byte[] blobVersionedHash, [NotNullWhen(true)] out byte[]? proof) => _blobTransactions.TryGetBlobAndProof(blobVersionedHash, out blob, out proof); + private void OnRemovedTx(object? sender, SortedPool.SortedPoolRemovedEventArgs args) + { + RemovePendingDelegations(args.Value); + } private void OnHeadChange(object? sender, BlockReplacementEventArgs e) { if (_headInfo.IsSyncing) return; @@ -460,10 +466,10 @@ private void AddPendingDelegations(Transaction tx) { if (tx.HasAuthorizationList) { - foreach (var auth in tx.AuthorizationList) + foreach (AuthorizationTuple auth in tx.AuthorizationList) { if (auth.Authority is not null) - _pendingDelegations.IncrementDelegationCount(auth.Authority!, auth.Nonce); + _pendingDelegations.IncrementDelegationCount(auth.Authority!); } } } @@ -562,7 +568,7 @@ private void RemovePendingDelegations(Transaction transaction) foreach (var auth in transaction.AuthorizationList) { if (auth.Authority is not null) - _pendingDelegations.DecrementDelegationCount(auth.Authority!, auth.Nonce); + _pendingDelegations.DecrementDelegationCount(auth.Authority!); } } } @@ -801,6 +807,7 @@ public void Dispose() _broadcaster.Dispose(); _headInfo.HeadChanged -= OnHeadChange; _headBlocksChannel.Writer.Complete(); + _transactions.Removed -= OnRemovedTx; } ///