diff --git a/.github/workflows/run-nethermind-tests-with-code-coverage.yml b/.github/workflows/run-nethermind-tests-with-code-coverage.yml index 8f46d7215b3..f87583f9917 100644 --- a/.github/workflows/run-nethermind-tests-with-code-coverage.yml +++ b/.github/workflows/run-nethermind-tests-with-code-coverage.yml @@ -43,6 +43,9 @@ jobs: - name: Nethermind.AuRa.Test run: | dotnet test -c Release $EXCLUDE_TEST_PROJECTS src/Nethermind/Nethermind.AuRa.Test + - name: Nethermind.AccountAbstraction.Test + run: | + dotnet test -c Release $EXCLUDE_TEST_PROJECTS src/Nethermind/Nethermind.AccountAbstraction.Test - name: Nethermind.Baseline.Test run: | dotnet test -c Release $EXCLUDE_TEST_PROJECTS src/Nethermind/Nethermind.Baseline.Test diff --git a/.github/workflows/run-nethermind-tests.yml b/.github/workflows/run-nethermind-tests.yml index f22afbf5a73..7070e4238a5 100644 --- a/.github/workflows/run-nethermind-tests.yml +++ b/.github/workflows/run-nethermind-tests.yml @@ -33,6 +33,9 @@ jobs: - name: Nethermind.AuRa.Test run: | dotnet test -c Release src/Nethermind/Nethermind.AuRa.Test + - name: Nethermind.AccountAbstraction.Test + run: | + dotnet test -c Release src/Nethermind/Nethermind.AccountAbstraction.Test - name: Nethermind.Baseline.Test run: | dotnet test -c Release src/Nethermind/Nethermind.Baseline.Test diff --git a/src/Nethermind/Nethermind.AccountAbstraction.Test/AccountAbstractionRpcModuleTests.TestAccountAbstractionRpcBlockchain.cs b/src/Nethermind/Nethermind.AccountAbstraction.Test/AccountAbstractionRpcModuleTests.TestAccountAbstractionRpcBlockchain.cs index f9e89c3d9ed..a18b0adcce6 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction.Test/AccountAbstractionRpcModuleTests.TestAccountAbstractionRpcBlockchain.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction.Test/AccountAbstractionRpcModuleTests.TestAccountAbstractionRpcBlockchain.cs @@ -195,7 +195,8 @@ protected override BlockProcessor CreateBlockProcessor() new UserOperationSortedPool( _accountAbstractionConfig.UserOperationPoolSize, new CompareUserOperationsByDecreasingGasPrice(), - LogManager)); + LogManager, + _accountAbstractionConfig.MaximumUserOperationPerSender)); return blockProcessor; } diff --git a/src/Nethermind/Nethermind.AccountAbstraction.Test/UserOperationPoolTests.cs b/src/Nethermind/Nethermind.AccountAbstraction.Test/UserOperationPoolTests.cs index 76c784d2dc1..b34b7e3b37e 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction.Test/UserOperationPoolTests.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction.Test/UserOperationPoolTests.cs @@ -112,13 +112,13 @@ public void Evicted_user_operation_has_its_simulated_removed_automatically() public void should_add_user_operations_concurrently() { int capacity = 2048; - _userOperationPool = GenerateUserOperationPool(capacity); + _userOperationPool = GenerateUserOperationPool(capacity, capacity); Parallel.ForEach(TestItem.PrivateKeys, k => { for (int i = 0; i < 100; i++) { - UserOperation op = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)i).SignedAndResolved(k).TestObject; + UserOperation op = Build.A.UserOperation.WithSender(k.Address).WithNonce((UInt256)i).SignedAndResolved(k).TestObject; _userOperationPool.AddUserOperation(op); } }); @@ -130,7 +130,7 @@ public void should_add_user_operations_concurrently() public async Task should_remove_user_operations_concurrently() { int capacity = 4096; - _userOperationPool = GenerateUserOperationPool(capacity); + _userOperationPool = GenerateUserOperationPool(capacity, capacity); int maxTryCount = 5; for (int i = 0; i < maxTryCount; ++i) @@ -139,7 +139,7 @@ public async Task should_remove_user_operations_concurrently() { for (int j = 0; j < 10; j++) { - UserOperation op = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)j).SignedAndResolved(k).TestObject; + UserOperation op = Build.A.UserOperation.WithSender(k.Address).WithNonce((UInt256)j).SignedAndResolved(k).TestObject; _userOperationPool.AddUserOperation(op); } }); @@ -166,6 +166,104 @@ void DeleteOpsFromPool(UserOperation[] ops) } } + [Test] + public void should_not_allow_more_than_max_capacity_per_sender_ops_from_same_sender() + { + _userOperationPool = GenerateUserOperationPool(); + for (int j = 0; j < 20; j++) + { + UserOperation op = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)j).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(op); + } + + _userOperationPool.GetUserOperations().Count().Should().Be(10); + } + + [Test] + public void should_replace_op_with_higher_fee() + { + _userOperationPool = GenerateUserOperationPool(); + UserOperation op = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)0).WithMaxFeePerGas(10).WithMaxPriorityFeePerGas(10).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(op); + + _userOperationPool.GetUserOperations().Should().BeEquivalentTo(new[] {op}); + + UserOperation higherGasPriceOp = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)0).WithMaxFeePerGas(20).WithMaxPriorityFeePerGas(20).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(higherGasPriceOp); + + _userOperationPool.GetUserOperations().Count().Should().Be(1); + _userOperationPool.GetUserOperations().Should().BeEquivalentTo(new[] {higherGasPriceOp}); + } + + [Test] + public void should_not_add_op_with_higher_fee_that_does_not_replace_op_if_sender_has_too_many_ops() + { + _userOperationPool = GenerateUserOperationPool(); + + for (int j = 0; j < 10; j++) + { + UserOperation op = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)j).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(op); + } + + UserOperation higherGasPriceOp = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)10).WithMaxFeePerGas(20).WithMaxPriorityFeePerGas(20).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(higherGasPriceOp); + + _userOperationPool.GetUserOperations().Count().Should().Be(10); + _userOperationPool.GetUserOperations().Should().NotContain(new[] {higherGasPriceOp}); + } + + [Test] + public void should_replace_op_with_higher_fee_even_at_full_capacity() + { + _userOperationPool = GenerateUserOperationPool(); + + IList opsIncluded = new List(); + + for (int j = 0; j < 10; j++) + { + UserOperation op = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)j).WithMaxFeePerGas(10).WithMaxPriorityFeePerGas(10).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + opsIncluded.Add(op); + _userOperationPool.AddUserOperation(op); + } + + UserOperation higherGasPriceOp = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce(9).WithMaxFeePerGas(20).WithMaxPriorityFeePerGas(20).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(higherGasPriceOp); + + _userOperationPool.GetUserOperations().Count().Should().Be(10); + _userOperationPool.GetUserOperations().Take(9).Should().BeEquivalentTo(opsIncluded.Take(9)); + _userOperationPool.GetUserOperations().Last().Should().BeEquivalentTo(higherGasPriceOp); + + UserOperation higherGasPriceOp2 = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce(3).WithMaxFeePerGas(20).WithMaxPriorityFeePerGas(20).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(higherGasPriceOp2); + + _userOperationPool.GetUserOperations().Count().Should().Be(10); + _userOperationPool.GetUserOperations().ToArray()[3].Should().Be(higherGasPriceOp2); + _userOperationPool.GetUserOperations().Should().NotContain(opsIncluded.ToArray()[3]); + } + + [Test] + public void should_not_replace_op_with_lower_fee_at_full_capacity() + { + _userOperationPool = GenerateUserOperationPool(); + + IList opsIncluded = new List(); + + for (int j = 0; j < 10; j++) + { + UserOperation op = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce((UInt256)j).WithMaxFeePerGas(10).WithMaxPriorityFeePerGas(10).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + opsIncluded.Add(op); + _userOperationPool.AddUserOperation(op); + } + + UserOperation lowerGasPriceOp = Build.A.UserOperation.WithSender(Address.SystemUser).WithNonce(9).WithMaxFeePerGas(5).WithMaxPriorityFeePerGas(5).SignedAndResolved(TestItem.PrivateKeyA).TestObject; + _userOperationPool.AddUserOperation(lowerGasPriceOp); + + _userOperationPool.GetUserOperations().Count().Should().Be(10); + _userOperationPool.GetUserOperations().Take(10).Should().BeEquivalentTo(opsIncluded.Take(10)); + _userOperationPool.GetUserOperations().Should().NotContain(lowerGasPriceOp); + } + [Test] public void should_remove_user_operations_from_pool_when_included_in_block() { @@ -314,10 +412,15 @@ public void Does_not_accept_obviously_bad_user_operations_into_pool(UserOperatio userOperations.Length.Should().Be(0); } - private UserOperationPool GenerateUserOperationPool(int capacity = 10) + private UserOperationPool GenerateUserOperationPool(int capacity = 10, int perSenderCapacity = 10) { + IAccountAbstractionConfig config = Substitute.For(); + config.EntryPointContractAddress.Returns(_entryPointContractAddress); + config.UserOperationPoolSize.Returns(capacity); + config.MaximumUserOperationPerSender.Returns(perSenderCapacity); + UserOperationSortedPool userOperationSortedPool = - new(capacity, CompareUserOperationsByDecreasingGasPrice.Default, LimboLogs.Instance); + new(capacity, CompareUserOperationsByDecreasingGasPrice.Default, LimboLogs.Instance, config.MaximumUserOperationPerSender); _stateProvider.GetBalance(Arg.Any
()).Returns(1.Ether()); _stateProvider.AccountExists(Arg.Any
()).Returns(true); @@ -332,10 +435,6 @@ private UserOperationPool GenerateUserOperationPool(int capacity = 10) _blockTree.Head.Returns(Core.Test.Builders.Build.A.Block.TestObject); - IAccountAbstractionConfig config = Substitute.For(); - config.EntryPointContractAddress.Returns(_entryPointContractAddress); - config.UserOperationPoolSize.Returns(capacity); - IPaymasterThrottler paymasterThrottler = Substitute.For(); return new UserOperationPool( diff --git a/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionConfig.cs b/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionConfig.cs index 89c40281c8a..055a5108b21 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionConfig.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionConfig.cs @@ -23,6 +23,7 @@ public class AccountAbstractionConfig : IAccountAbstractionConfig { public bool Enabled { get; set; } public int UserOperationPoolSize { get; set; } = 200; + public int MaximumUserOperationPerSender { get; set; } = 10; public string EntryPointContractAddress { get; set; } = ""; public string Create2FactoryAddress { get; set; } = ""; public UInt256 MinimumGasPrice { get; set; } = 1; diff --git a/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionPlugin.cs b/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionPlugin.cs index d8451b768d4..79317a4d6a4 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionPlugin.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction/AccountAbstractionPlugin.cs @@ -80,7 +80,8 @@ private UserOperationPool UserOperationPool UserOperationSortedPool userOperationSortedPool = new( _accountAbstractionConfig.UserOperationPoolSize, CompareUserOperationsByDecreasingGasPrice.Default, - getFromApi.LogManager); + getFromApi.LogManager, + _accountAbstractionConfig.MaximumUserOperationPerSender); _userOperationPool = new UserOperationPool( _accountAbstractionConfig, diff --git a/src/Nethermind/Nethermind.AccountAbstraction/IAccountAbstractionConfig.cs b/src/Nethermind/Nethermind.AccountAbstraction/IAccountAbstractionConfig.cs index a8516d44fe3..09571b9071c 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction/IAccountAbstractionConfig.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction/IAccountAbstractionConfig.cs @@ -31,6 +31,11 @@ public interface IAccountAbstractionConfig : IConfig Description = "Defines the maximum number of UserOperations that can be kept in memory by clients", DefaultValue = "200")] int UserOperationPoolSize { get; set; } + + [ConfigItem( + Description = "Defines the maximum number of UserOperations that can be kept for each sender", + DefaultValue = "10")] + int MaximumUserOperationPerSender { get; set; } [ConfigItem( Description = diff --git a/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareReplacedUserOperationByFee.cs b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareReplacedUserOperationByFee.cs new file mode 100644 index 00000000000..3db6c13bae0 --- /dev/null +++ b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareReplacedUserOperationByFee.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2021 Demerzel Solutions Limited +// This file is part of the Nethermind library. +// +// The Nethermind library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Nethermind library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Nethermind. If not, see . +// + +using System.Collections.Generic; +using Nethermind.AccountAbstraction.Data; +using Nethermind.Int256; + +namespace Nethermind.AccountAbstraction.Source +{ + public class CompareReplacedUserOperationByFee : IComparer + { + public static readonly CompareReplacedUserOperationByFee Instance = new(); + + // To replace old user operation, new user operation needs to have fee higher by at least 10% (1/10) of current fee. + // It is required to avoid acceptance and propagation of user operation with almost the same fee as replaced one. + private const int PartOfFeeRequiredToIncrease = 10; + + private CompareReplacedUserOperationByFee() { } + + public int Compare(UserOperation? x, UserOperation? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; + + y.MaxFeePerGas.Divide(PartOfFeeRequiredToIncrease, out UInt256 bumpMaxFeePerGas); + if (y.MaxFeePerGas + bumpMaxFeePerGas > x.MaxFeePerGas) return 1; + + y.MaxPriorityFeePerGas.Divide(PartOfFeeRequiredToIncrease, out UInt256 bumpMaxPriorityFeePerGas); + return (y.MaxPriorityFeePerGas + bumpMaxPriorityFeePerGas).CompareTo(x.MaxPriorityFeePerGas); + } + } +} diff --git a/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareUserOperationByNonce.cs b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareUserOperationByNonce.cs new file mode 100644 index 00000000000..69a978b05ef --- /dev/null +++ b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareUserOperationByNonce.cs @@ -0,0 +1,37 @@ +// Copyright (c) 2021 Demerzel Solutions Limited +// This file is part of the Nethermind library. +// +// The Nethermind library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Nethermind library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Nethermind. If not, see . +// + +using System.Collections.Generic; +using Nethermind.AccountAbstraction.Data; + +namespace Nethermind.AccountAbstraction.Source +{ + public class CompareUserOperationByNonce : IComparer + { + public static readonly CompareUserOperationByNonce Instance = new(); + + private CompareUserOperationByNonce() { } + + public int Compare(UserOperation? x, UserOperation? y) + { + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; + return x.Nonce.CompareTo(y.Nonce); + } + } +} diff --git a/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareUserOperationsByHash.cs b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareUserOperationsByHash.cs index ca9df9b6eea..18e104ca16c 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareUserOperationsByHash.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompareUserOperationsByHash.cs @@ -22,7 +22,9 @@ namespace Nethermind.AccountAbstraction.Source { public class CompareUserOperationsByHash : IComparer { - public static readonly CompareUserOperationsByHash Default = new(); + public static readonly CompareUserOperationsByHash Instance = new(); + + private CompareUserOperationsByHash() { } public int Compare(UserOperation? x, UserOperation? y) { diff --git a/src/Nethermind/Nethermind.AccountAbstraction/Source/CompetingUserOperationEqualityComparer.cs b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompetingUserOperationEqualityComparer.cs new file mode 100644 index 00000000000..29b33fa2fa1 --- /dev/null +++ b/src/Nethermind/Nethermind.AccountAbstraction/Source/CompetingUserOperationEqualityComparer.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2021 Demerzel Solutions Limited +// This file is part of the Nethermind library. +// +// The Nethermind library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The Nethermind library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the Nethermind. If not, see . +// + +using System; +using System.Collections.Generic; +using Nethermind.AccountAbstraction.Data; + +namespace Nethermind.AccountAbstraction.Source +{ + public class CompetingUserOperationEqualityComparer : IEqualityComparer + { + public static readonly CompetingUserOperationEqualityComparer Instance = new(); + + private CompetingUserOperationEqualityComparer() { } + + public bool Equals(UserOperation? x, UserOperation? y) + { + if (ReferenceEquals(x, y)) return true; + if (ReferenceEquals(x, null)) return false; + if (ReferenceEquals(y, null)) return false; + if (x.GetType() != y.GetType()) return false; + return x.Sender.Equals(y.Sender) && x.Nonce.Equals(y.Nonce); + } + + public int GetHashCode(UserOperation obj) + { + return HashCode.Combine(obj.Sender, obj.Nonce); + } + } +} diff --git a/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationPool.cs b/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationPool.cs index 61797b9debe..d694ec8a9e6 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationPool.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationPool.cs @@ -262,13 +262,18 @@ private ResultWrapper ValidateUserOperation(UserOperation userOperation) } } + if (_userOperationSortedPool.UserOperationWouldOverflowSenderBucket(userOperation)) + { + return ResultWrapper.Fail($"the pool already contains the maximum {_accountAbstractionConfig.MaximumUserOperationPerSender} user operations from the {userOperation.Sender} sender"); + } + if (userOperation.MaxFeePerGas < _accountAbstractionConfig.MinimumGasPrice) - return ResultWrapper.Fail("maxFeePerGas below minimum gas price"); + return ResultWrapper.Fail($"maxFeePerGas below minimum gas price {_accountAbstractionConfig.MinimumGasPrice} wei"); if (userOperation.CallGas < Transaction.BaseTxGasCost) return ResultWrapper.Fail($"callGas too low, must be at least {Transaction.BaseTxGasCost}"); - // make sure target account exists + // make sure target account exists or is going to be created if ( userOperation.Sender == Address.Zero || !(_stateProvider.AccountExists(userOperation.Sender) || userOperation.InitCode != Bytes.Empty)) diff --git a/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationSortedPool.cs b/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationSortedPool.cs index 1eb1ab92cad..d4fdca9710b 100644 --- a/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationSortedPool.cs +++ b/src/Nethermind/Nethermind.AccountAbstraction/Source/UserOperationSortedPool.cs @@ -16,6 +16,7 @@ // using System.Collections.Generic; +using System.Linq; using Nethermind.AccountAbstraction.Data; using Nethermind.Core; using Nethermind.Core.Crypto; @@ -26,22 +27,39 @@ namespace Nethermind.AccountAbstraction.Source { public class UserOperationSortedPool : DistinctValueSortedPool { - public UserOperationSortedPool(int capacity, IComparer comparer, ILogManager logManager) : - base(capacity, comparer, EqualityComparer.Default, logManager) + private readonly int _maximumUserOperationPerSender; + + public UserOperationSortedPool(int capacity, IComparer comparer, ILogManager logManager, int maximumUserOperationPerSender) : + base(capacity, comparer, CompetingUserOperationEqualityComparer.Instance, logManager) { + _maximumUserOperationPerSender = maximumUserOperationPerSender; } protected override IComparer GetUniqueComparer(IComparer comparer) => - comparer.ThenBy(CompareUserOperationsByHash.Default); + comparer.ThenBy(CompareUserOperationsByHash.Instance); protected override IComparer GetGroupComparer(IComparer comparer) => - comparer.ThenBy(CompareUserOperationsByHash.Default); + CompareUserOperationByNonce.Instance.ThenBy(CompareUserOperationsByHash.Instance.ThenBy(comparer)); protected override IComparer GetReplacementComparer(IComparer comparer) => - comparer.ThenBy(CompareUserOperationsByDecreasingGasPrice.Default); + CompareReplacedUserOperationByFee.Instance.ThenBy(comparer); protected override Address MapToGroup(UserOperation value) => value.Sender; protected override Keccak GetKey(UserOperation value) => value.Hash!; + + protected override bool AllowSameKeyReplacement => true; + + // each sender can only hold MaximumUserOperationPerSender (default 10) ops, however even if they + // hold the maximum we still want to allow fee replacement + public bool UserOperationWouldOverflowSenderBucket(UserOperation op) + { + if (GetBucketCount(op.Sender) < _maximumUserOperationPerSender) + { + return false; + } + + return !CanInsert(op.Hash, op); + } } } diff --git a/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs b/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs index 0fa83883726..c3f5940671a 100644 --- a/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs +++ b/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs @@ -25,7 +25,7 @@ public static class TxSortedPoolExtensions { public static IComparer GetPoolUniqueTxComparer(this IComparer comparer) => comparer - .ThenBy(ByHashTxComparer.Instance); // in order to sort properly and not loose transactions we need to differentiate on their identity which provided comparer might not be doing + .ThenBy(ByHashTxComparer.Instance); // in order to sort properly and not lose transactions we need to differentiate on their identity which provided comparer might not be doing public static IComparer GetPoolUniqueTxComparerByNonce(this IComparer comparer) => CompareTxByNonce.Instance // we need to ensure transactions are ordered by nonce, which might not be done in supplied comparer