diff --git a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs index 02aa7d706f4..b342ad31900 100644 --- a/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs +++ b/src/Nethermind/Ethereum.Test.Base/BlockchainTestBase.cs @@ -135,8 +135,6 @@ protected async Task RunTest(BlockchainTest test, Stopwatch? .TestObject; ITransactionComparerProvider transactionComparerProvider = new TransactionComparerProvider(specProvider, blockTree); IStateReader stateReader = new StateReader(trieStore, codeDb, _logManager); - IChainHeadInfoProvider chainHeadInfoProvider = new ChainHeadInfoProvider(specProvider, blockTree, stateReader); - ITxPool transactionPool = new TxPool(ecdsa, chainHeadInfoProvider, new TxPoolConfig(), new TxValidator(specProvider.ChainId), _logManager, transactionComparerProvider.GetDefaultComparer()); IReceiptStorage receiptStorage = NullReceiptStorage.Instance; IBlockhashProvider blockhashProvider = new BlockhashProvider(blockTree, _logManager); diff --git a/src/Nethermind/Nethermind.Api/IApiWithStores.cs b/src/Nethermind/Nethermind.Api/IApiWithStores.cs index 3c0f71b8c1d..12fe585bc59 100644 --- a/src/Nethermind/Nethermind.Api/IApiWithStores.cs +++ b/src/Nethermind/Nethermind.Api/IApiWithStores.cs @@ -8,12 +8,14 @@ using Nethermind.Crypto; using Nethermind.Db.Blooms; using Nethermind.State.Repositories; +using Nethermind.TxPool; using Nethermind.Wallet; namespace Nethermind.Api { public interface IApiWithStores : IBasicApi { + ITxStorage? BlobTxStorage { get; set; } IBlockTree? BlockTree { get; set; } IBloomStorage? BloomStorage { get; set; } IChainLevelInfoRepository? ChainLevelInfoRepository { get; set; } diff --git a/src/Nethermind/Nethermind.Api/NethermindApi.cs b/src/Nethermind/Nethermind.Api/NethermindApi.cs index c94205abf91..86fe643e0c5 100644 --- a/src/Nethermind/Nethermind.Api/NethermindApi.cs +++ b/src/Nethermind/Nethermind.Api/NethermindApi.cs @@ -104,6 +104,7 @@ public IBlockchainBridge CreateBlockchainBridge() } public IAbiEncoder AbiEncoder { get; } = Nethermind.Abi.AbiEncoder.Instance; + public ITxStorage? BlobTxStorage { get; set; } public IBlockchainProcessor? BlockchainProcessor { get; set; } public CompositeBlockPreprocessorStep BlockPreprocessor { get; } = new(); public IBlockProcessingQueue? BlockProcessingQueue { get; set; } diff --git a/src/Nethermind/Nethermind.Blockchain.Test/FullPruning/FullPruningDiskTest.cs b/src/Nethermind/Nethermind.Blockchain.Test/FullPruning/FullPruningDiskTest.cs index 999a0c9030f..6592da67dda 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/FullPruning/FullPruningDiskTest.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/FullPruning/FullPruningDiskTest.cs @@ -61,7 +61,7 @@ protected override async Task CreateDbProvider() { IDbProvider dbProvider = new DbProvider(DbModeHint.Persisted); RocksDbFactory rocksDbFactory = new(new DbConfig(), LogManager, TempDirectory.Path); - StandardDbInitializer standardDbInitializer = new(dbProvider, rocksDbFactory, new MemDbFactory(), new FileSystem(), true); + StandardDbInitializer standardDbInitializer = new(dbProvider, rocksDbFactory, new MemDbFactory(), new FileSystem()); await standardDbInitializer.InitStandardDbsAsync(true); return dbProvider; } diff --git a/src/Nethermind/Nethermind.Blockchain.Test/ReorgTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/ReorgTests.cs index 9bddbe2a42e..6a5f838c54f 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/ReorgTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/ReorgTests.cs @@ -53,6 +53,7 @@ public void Setup() TxPool.TxPool txPool = new( ecdsa, + new BlobTxStorage(), new ChainHeadInfoProvider(specProvider, _blockTree, stateProvider), new TxPoolConfig(), new TxValidator(specProvider.ChainId), diff --git a/src/Nethermind/Nethermind.Blockchain.Test/TransactionSelectorTests.cs b/src/Nethermind/Nethermind.Blockchain.Test/TransactionSelectorTests.cs index 5281b0a37ce..3b3b7ef1198 100644 --- a/src/Nethermind/Nethermind.Blockchain.Test/TransactionSelectorTests.cs +++ b/src/Nethermind/Nethermind.Blockchain.Test/TransactionSelectorTests.cs @@ -23,6 +23,7 @@ using NSubstitute; using NUnit.Framework; using Nethermind.Config; +using Nethermind.Core.Crypto; namespace Nethermind.Blockchain.Test { @@ -229,13 +230,30 @@ void SetAccountStates(IEnumerable
missingAddresses) new(specProvider, blockTree); IComparer defaultComparer = transactionComparerProvider.GetDefaultComparer(); IComparer comparer = CompareTxByNonce.Instance.ThenBy(defaultComparer); - Dictionary transactions = testCase.Transactions - .Where(t => t.SenderAddress is not null) - .GroupBy(t => t.SenderAddress) - .ToDictionary( - g => g.Key!, - g => g.OrderBy(t => t, comparer).ToArray()); + + Dictionary GroupTransactions(bool supportBlobs) => + testCase.Transactions + .Where(t => t.SenderAddress is not null) + .Where(t => t.SupportsBlobs == supportBlobs) + .GroupBy(t => t.SenderAddress) + .ToDictionary( + g => g.Key!, + g => g.OrderBy(t => t, comparer).ToArray()); + + Dictionary transactions = GroupTransactions(false); + Dictionary blobTransactions = GroupTransactions(true); transactionPool.GetPendingTransactionsBySender().Returns(transactions); + transactionPool.GetPendingLightBlobTransactionsBySender().Returns(blobTransactions); + foreach (Transaction blobTx in blobTransactions.SelectMany(kvp => kvp.Value)) + { + transactionPool.TryGetPendingBlobTransaction(Arg.Is(h => h == blobTx.Hash), + out Arg.Any()).Returns(x => + { + x[1] = blobTx; + return true; + }); + } + BlocksConfig blocksConfig = new() { MinGasPrice = testCase.MinGasPriceForMining }; ITxFilterPipeline txFilterPipeline = new TxFilterPipelineBuilder(LimboLogs.Instance) .WithMinGasPriceFilter(blocksConfig, specProvider) @@ -252,6 +270,7 @@ void SetAccountStates(IEnumerable
missingAddresses) { parentHeader = parentHeader.WithExcessBlobGas(0); } + IEnumerable selectedTransactions = poolTxSource.GetTransactions(parentHeader.TestObject, testCase.GasLimit); diff --git a/src/Nethermind/Nethermind.Blockchain/ChainHeadInfoProvider.cs b/src/Nethermind/Nethermind.Blockchain/ChainHeadInfoProvider.cs index a2701230e2d..c23aeed1f73 100644 --- a/src/Nethermind/Nethermind.Blockchain/ChainHeadInfoProvider.cs +++ b/src/Nethermind/Nethermind.Blockchain/ChainHeadInfoProvider.cs @@ -6,6 +6,7 @@ using Nethermind.Blockchain.Spec; using Nethermind.Core; using Nethermind.Core.Specs; +using Nethermind.Evm; using Nethermind.Int256; using Nethermind.State; using Nethermind.TxPool; @@ -42,12 +43,18 @@ public ChainHeadInfoProvider(IChainHeadSpecProvider specProvider, IBlockTree blo public UInt256 CurrentBaseFee { get; private set; } + public UInt256 CurrentPricePerBlobGas { get; internal set; } + public event EventHandler? HeadChanged; private void OnHeadChanged(object? sender, BlockReplacementEventArgs e) { BlockGasLimit = e.Block!.GasLimit; CurrentBaseFee = e.Block.Header.BaseFeePerGas; + CurrentPricePerBlobGas = + BlobGasCalculator.TryCalculateBlobGasPricePerUnit(e.Block.Header, out UInt256 currentPricePerBlobGas) + ? currentPricePerBlobGas + : UInt256.Zero; HeadChanged?.Invoke(sender, e); } } diff --git a/src/Nethermind/Nethermind.Clique.Test/CliqueBlockProducerTests.cs b/src/Nethermind/Nethermind.Clique.Test/CliqueBlockProducerTests.cs index 5226071fc11..ab0a0be1ebf 100644 --- a/src/Nethermind/Nethermind.Clique.Test/CliqueBlockProducerTests.cs +++ b/src/Nethermind/Nethermind.Clique.Test/CliqueBlockProducerTests.cs @@ -108,7 +108,13 @@ public On CreateNode(PrivateKey privateKey, bool withGenesisAlreadyProcessed = f ITransactionComparerProvider transactionComparerProvider = new TransactionComparerProvider(specProvider, blockTree); - TxPool.TxPool txPool = new(_ethereumEcdsa, new ChainHeadInfoProvider(new FixedForkActivationChainHeadSpecProvider(GoerliSpecProvider.Instance), blockTree, stateProvider), new TxPoolConfig(), new TxValidator(goerliSpecProvider.ChainId), _logManager, transactionComparerProvider.GetDefaultComparer()); + TxPool.TxPool txPool = new(_ethereumEcdsa, + new BlobTxStorage(), + new ChainHeadInfoProvider(new FixedForkActivationChainHeadSpecProvider(GoerliSpecProvider.Instance), blockTree, stateProvider), + new TxPoolConfig(), + new TxValidator(goerliSpecProvider.ChainId), + _logManager, + transactionComparerProvider.GetDefaultComparer()); _pools[privateKey] = txPool; BlockhashProvider blockhashProvider = new(blockTree, LimboLogs.Instance); diff --git a/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs b/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs index 650a82b5dd6..f23c2acfce7 100644 --- a/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs +++ b/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs @@ -274,6 +274,7 @@ protected override TxPool.TxPool CreateTxPool() return new TxPool.TxPool( _api.EthereumEcdsa, + _api.BlobTxStorage ?? NullBlobTxStorage.Instance, new ChainHeadInfoProvider(_api.SpecProvider, _api.BlockTree, _api.StateReader), NethermindApi.Config(), _api.TxValidator, diff --git a/src/Nethermind/Nethermind.Consensus/Producers/TxPoolTxSource.cs b/src/Nethermind/Nethermind.Consensus/Producers/TxPoolTxSource.cs index 558ce50f7d7..7b291568bd5 100644 --- a/src/Nethermind/Nethermind.Consensus/Producers/TxPoolTxSource.cs +++ b/src/Nethermind/Nethermind.Consensus/Producers/TxPoolTxSource.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.CompilerServices; using Nethermind.Consensus.Comparers; @@ -46,26 +47,26 @@ public TxPoolTxSource( public IEnumerable GetTransactions(BlockHeader parent, long gasLimit) { long blockNumber = parent.Number + 1; - IEip1559Spec specFor1559 = _specProvider.GetSpecFor1559(blockNumber); - UInt256 baseFee = BaseFeeCalculator.Calculate(parent, specFor1559); + IReleaseSpec spec = _specProvider.GetSpec(parent); + UInt256 baseFee = BaseFeeCalculator.Calculate(parent, spec); IDictionary pendingTransactions = _transactionPool.GetPendingTransactionsBySender(); + IDictionary pendingBlobTransactionsEquivalences = _transactionPool.GetPendingLightBlobTransactionsBySender(); IComparer comparer = GetComparer(parent, new BlockPreparationContext(baseFee, blockNumber)) .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 IEnumerable transactions = GetOrderedTransactions(pendingTransactions, comparer); + IEnumerable blobTransactions = GetOrderedTransactions(pendingBlobTransactionsEquivalences, comparer); if (_logger.IsDebug) _logger.Debug($"Collecting pending transactions at block gas limit {gasLimit}."); + int checkedTransactions = 0; int selectedTransactions = 0; - int i = 0; + using ArrayPoolList selectedBlobTxs = new(Eip4844Constants.MaxBlobsPerBlock); - // TODO: removing transactions from TX pool here seems to be a bad practice since they will - // not come back if the block is ignored? - int blobsCounter = 0; - UInt256 blobGasPrice = UInt256.Zero; + SelectBlobTransactions(blobTransactions, parent, spec, selectedBlobTxs); foreach (Transaction tx in transactions) { - i++; + checkedTransactions++; if (tx.SenderAddress is null) { @@ -77,52 +78,130 @@ public IEnumerable GetTransactions(BlockHeader parent, long gasLimi bool success = _txFilterPipeline.Execute(tx, parent); if (!success) continue; - if (tx.SupportsBlobs) + foreach (Transaction blobTx in PickBlobTxsBetterThanCurrentTx(selectedBlobTxs, tx, comparer)) { - if (blobGasPrice.IsZero) - { - ulong? excessBlobGas = BlobGasCalculator.CalculateExcessBlobGas(parent, _specProvider.GetSpec(parent)); - if (excessBlobGas is null) - { - if (_logger.IsTrace) _logger.Trace($"Declining {tx.ToShortString()}, the specification is not configured to handle shard blob transactions."); - continue; - } - if (!BlobGasCalculator.TryCalculateBlobGasPricePerUnit(excessBlobGas.Value, out blobGasPrice)) - { - if (_logger.IsTrace) _logger.Trace($"Declining {tx.ToShortString()}, failed to calculate blob gas price."); - continue; - } - } + yield return blobTx; + } - int txAmountOfBlobs = tx.BlobVersionedHashes?.Length ?? 0; + if (_logger.IsTrace) _logger.Trace($"Selected {tx.ToShortString()} to be potentially included in block."); - if (blobGasPrice > tx.MaxFeePerBlobGas) - { - if (_logger.IsTrace) _logger.Trace($"Declining {tx.ToShortString()}, blob gas fee is too low."); - continue; - } + selectedTransactions++; + yield return tx; + } - if (BlobGasCalculator.CalculateBlobGas(blobsCounter + txAmountOfBlobs) > - Eip4844Constants.MaxBlobGasPerBlock) - { - if (_logger.IsTrace) _logger.Trace($"Declining {tx.ToShortString()}, no more blob space."); - continue; - } + if (selectedBlobTxs.Count > 0) + { + foreach (Transaction blobTx in selectedBlobTxs) + { + yield return blobTx; + } + } + + if (_logger.IsDebug) _logger.Debug($"Potentially selected {selectedTransactions} out of {checkedTransactions} pending transactions checked."); + } - blobsCounter += txAmountOfBlobs; - if (_logger.IsTrace) _logger.Trace($"Selected shard blob tx {tx.ToShortString()} to be potentially included in block, total blobs included: {blobsCounter}."); + private IEnumerable PickBlobTxsBetterThanCurrentTx(ArrayPoolList selectedBlobTxs, Transaction tx, IComparer comparer) + { + while (selectedBlobTxs.Count > 0) + { + Transaction blobTx = selectedBlobTxs[0]; + if (comparer.Compare(blobTx, tx) > 0) + { + yield return blobTx; + selectedBlobTxs.Remove(blobTx); } else { - if (_logger.IsTrace) - _logger.Trace($"Selected {tx.ToShortString()} to be potentially included in block."); + break; } + } + } - selectedTransactions++; - yield return tx; + private void SelectBlobTransactions(IEnumerable blobTransactions, BlockHeader parent, IReleaseSpec spec, ArrayPoolList selectedBlobTxs) + { + int checkedBlobTransactions = 0; + int selectedBlobTransactions = 0; + int blobsCounter = 0; + UInt256 blobGasPrice = UInt256.Zero; + + foreach (Transaction blobTx in blobTransactions) + { + if (blobsCounter == Eip4844Constants.MaxBlobsPerBlock) + { + if (_logger.IsTrace) _logger.Trace($"Declining {blobTx.ToShortString()}, no more blob space. Block already have {blobsCounter} which is max value allowed."); + break; + } + + checkedBlobTransactions++; + + int txAmountOfBlobs = blobTx.BlobVersionedHashes?.Length ?? 0; + if (blobsCounter + txAmountOfBlobs > Eip4844Constants.MaxBlobsPerBlock) + { + if (_logger.IsTrace) _logger.Trace($"Declining {blobTx.ToShortString()}, not enough blob space."); + continue; + } + + if (blobGasPrice.IsZero && !TryUpdateBlobGasPrice(blobTx, parent, spec, out blobGasPrice)) + { + if (_logger.IsTrace) _logger.Trace($"Declining {blobTx.ToShortString()}, failed to get full version of this blob tx from TxPool."); + continue; + } + + if (blobGasPrice > blobTx.MaxFeePerBlobGas) + { + if (_logger.IsTrace) _logger.Trace($"Declining {blobTx.ToShortString()}, data gas fee is too low."); + continue; + } + + bool success = _txFilterPipeline.Execute(blobTx, parent); + if (!success) continue; + + if (!TryGetFullBlobTx(blobTx, out Transaction fullBlobTx)) + { + if (_logger.IsTrace) _logger.Trace($"Declining {blobTx.ToShortString()}, failed to get full version of this blob tx from TxPool."); + continue; + } + + blobsCounter += txAmountOfBlobs; + if (_logger.IsTrace) _logger.Trace($"Selected shard blob tx {fullBlobTx.ToShortString()} to be potentially included in block, total blobs included: {blobsCounter}."); + + selectedBlobTransactions++; + selectedBlobTxs.Add(fullBlobTx); + } + + if (_logger.IsDebug) _logger.Debug($"Potentially selected {selectedBlobTransactions} out of {checkedBlobTransactions} pending blob transactions checked."); + } + + private bool TryGetFullBlobTx(Transaction blobTx, [NotNullWhen(true)] out Transaction? fullBlobTx) + { + if (blobTx.NetworkWrapper is not null) + { + fullBlobTx = blobTx; + return true; + } + + fullBlobTx = null; + return blobTx.Hash is not null && _transactionPool.TryGetPendingBlobTransaction(blobTx.Hash, out fullBlobTx); + } + + private bool TryUpdateBlobGasPrice(Transaction lightBlobTx, BlockHeader parent, IReleaseSpec spec, out UInt256 blobGasPrice) + { + ulong? excessDataGas = BlobGasCalculator.CalculateExcessBlobGas(parent, spec); + if (excessDataGas is null) + { + if (_logger.IsTrace) _logger.Trace($"Declining {lightBlobTx.ToShortString()}, the specification is not configured to handle shard blob transactions."); + blobGasPrice = UInt256.Zero; + return false; + } + + if (!BlobGasCalculator.TryCalculateBlobGasPricePerUnit(excessDataGas.Value, out blobGasPrice)) + { + if (_logger.IsTrace) _logger.Trace($"Declining {lightBlobTx.ToShortString()}, failed to calculate data gas price."); + blobGasPrice = UInt256.Zero; + return false; } - if (_logger.IsDebug) _logger.Debug($"Potentially selected {selectedTransactions} out of {i} pending transactions checked."); + return true; } protected virtual IEnumerable GetOrderedTransactions(IDictionary pendingTransactions, IComparer comparer) => diff --git a/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs b/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs index ed26c69fc8c..5d243d3ffa0 100644 --- a/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs +++ b/src/Nethermind/Nethermind.Core.Test/Blockchain/TestBlockchain.cs @@ -279,8 +279,9 @@ protected virtual IBlockProducer CreateTestBlockProducer(TxPoolTxSource txPoolTx protected virtual TxPool.TxPool CreateTxPool() => new( EthereumEcdsa, + new BlobTxStorage(), new ChainHeadInfoProvider(new FixedForkActivationChainHeadSpecProvider(SpecProvider), BlockTree, ReadOnlyState), - new TxPoolConfig(), + new TxPoolConfig() { BlobSupportEnabled = true }, new TxValidator(SpecProvider.ChainId), LogManager, TransactionComparerProvider.GetDefaultComparer()); diff --git a/src/Nethermind/Nethermind.Core/Eip4844Constants.cs b/src/Nethermind/Nethermind.Core/Eip4844Constants.cs index f64eb88ffc3..ab502e4ebc4 100644 --- a/src/Nethermind/Nethermind.Core/Eip4844Constants.cs +++ b/src/Nethermind/Nethermind.Core/Eip4844Constants.cs @@ -8,10 +8,11 @@ namespace Nethermind.Core; public class Eip4844Constants { public const int MinBlobsPerTransaction = 1; + public const int MaxBlobsPerBlock = 6; public const ulong BlobGasPerBlob = 1 << 17; - public const ulong TargetBlobGasPerBlock = BlobGasPerBlob * 3; - public const ulong MaxBlobGasPerBlock = BlobGasPerBlob * 6; + public const ulong MaxBlobGasPerBlock = BlobGasPerBlob * MaxBlobsPerBlock; + public const ulong TargetBlobGasPerBlock = MaxBlobGasPerBlock / 2; public const ulong MaxBlobGasPerTransaction = MaxBlobGasPerBlock; public static readonly UInt256 BlobGasUpdateFraction = 3338477; diff --git a/src/Nethermind/Nethermind.Core/Transaction.cs b/src/Nethermind/Nethermind.Core/Transaction.cs index 1847087924e..18a134e1b17 100644 --- a/src/Nethermind/Nethermind.Core/Transaction.cs +++ b/src/Nethermind/Nethermind.Core/Transaction.cs @@ -145,7 +145,8 @@ private void ClearPreHashInternal() /// Used for sorting in edge cases. public ulong PoolIndex { get; set; } - private int? _size = null; + protected int? _size = null; + /// /// Encoded transaction length /// diff --git a/src/Nethermind/Nethermind.Db.Test/StandardDbInitializerTests.cs b/src/Nethermind/Nethermind.Db.Test/StandardDbInitializerTests.cs index 91a4845eb38..74d9d66ab20 100644 --- a/src/Nethermind/Nethermind.Db.Test/StandardDbInitializerTests.cs +++ b/src/Nethermind/Nethermind.Db.Test/StandardDbInitializerTests.cs @@ -18,7 +18,7 @@ namespace Nethermind.Db.Test [Parallelizable(ParallelScope.All)] public class StandardDbInitializerTests { - private string _folderWithDbs; + private string _folderWithDbs = null!; [OneTimeSetUp] public void Initialize() @@ -30,7 +30,7 @@ public void Initialize() [TestCase(true)] public async Task InitializerTests_MemDbProvider(bool useReceipts) { - IDbProvider dbProvider = await InitializeStandardDb(useReceipts, DbModeHint.Mem, "mem"); + using IDbProvider dbProvider = await InitializeStandardDb(useReceipts, DbModeHint.Mem, "mem"); Type receiptsType = GetReceiptsType(useReceipts, typeof(MemColumnsDb)); AssertStandardDbs(dbProvider, typeof(MemDb), receiptsType); dbProvider.StateDb.Should().BeOfType(typeof(FullPruningDb)); @@ -40,7 +40,7 @@ public async Task InitializerTests_MemDbProvider(bool useReceipts) [TestCase(true)] public async Task InitializerTests_RocksDbProvider(bool useReceipts) { - IDbProvider dbProvider = await InitializeStandardDb(useReceipts, DbModeHint.Persisted, $"rocks_{useReceipts}"); + using IDbProvider dbProvider = await InitializeStandardDb(useReceipts, DbModeHint.Persisted, $"rocks_{useReceipts}"); Type receiptsType = GetReceiptsType(useReceipts); AssertStandardDbs(dbProvider, typeof(DbOnTheRocks), receiptsType); dbProvider.StateDb.Should().BeOfType(typeof(FullPruningDb)); @@ -50,7 +50,7 @@ public async Task InitializerTests_RocksDbProvider(bool useReceipts) [TestCase(true)] public async Task InitializerTests_ReadonlyDbProvider(bool useReceipts) { - IDbProvider dbProvider = await InitializeStandardDb(useReceipts, DbModeHint.Persisted, $"readonly_{useReceipts}"); + using IDbProvider dbProvider = await InitializeStandardDb(useReceipts, DbModeHint.Persisted, $"readonly_{useReceipts}"); using ReadOnlyDbProvider readonlyDbProvider = new(dbProvider, true); Type receiptsType = GetReceiptsType(useReceipts); AssertStandardDbs(dbProvider, typeof(DbOnTheRocks), receiptsType); @@ -62,15 +62,15 @@ public async Task InitializerTests_ReadonlyDbProvider(bool useReceipts) [Test] public async Task InitializerTests_WithPruning() { - IDbProvider dbProvider = await InitializeStandardDb(false, DbModeHint.Mem, "pruning", true); + using IDbProvider dbProvider = await InitializeStandardDb(false, DbModeHint.Mem, "pruning"); dbProvider.StateDb.Should().BeOfType(); } - private async Task InitializeStandardDb(bool useReceipts, DbModeHint dbModeHint, string path, bool pruning = false) + private async Task InitializeStandardDb(bool useReceipts, DbModeHint dbModeHint, string path) { - using IDbProvider dbProvider = new DbProvider(dbModeHint); + IDbProvider dbProvider = new DbProvider(dbModeHint); RocksDbFactory rocksDbFactory = new(new DbConfig(), LimboLogs.Instance, Path.Combine(_folderWithDbs, path)); - StandardDbInitializer initializer = new(dbProvider, rocksDbFactory, new MemDbFactory(), Substitute.For(), pruning); + StandardDbInitializer initializer = new(dbProvider, rocksDbFactory, new MemDbFactory(), Substitute.For()); await initializer.InitStandardDbsAsync(useReceipts); return dbProvider; } diff --git a/src/Nethermind/Nethermind.Db/BlobTxsColumns.cs b/src/Nethermind/Nethermind.Db/BlobTxsColumns.cs new file mode 100644 index 00000000000..56b4f1d4848 --- /dev/null +++ b/src/Nethermind/Nethermind.Db/BlobTxsColumns.cs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +namespace Nethermind.Db; + +public enum BlobTxsColumns +{ + FullBlobTxs, + LightBlobTxs +} diff --git a/src/Nethermind/Nethermind.Db/DbNames.cs b/src/Nethermind/Nethermind.Db/DbNames.cs index e6ccb60be91..4d9f0c21350 100644 --- a/src/Nethermind/Nethermind.Db/DbNames.cs +++ b/src/Nethermind/Nethermind.Db/DbNames.cs @@ -17,5 +17,6 @@ public static class DbNames public const string Witness = "witness"; public const string CHT = "canonicalHashTrie"; public const string Metadata = "metadata"; + public const string BlobTransactions = "blobTransactions"; } } diff --git a/src/Nethermind/Nethermind.Db/IDbProvider.cs b/src/Nethermind/Nethermind.Db/IDbProvider.cs index 977901353f9..a3494e64274 100644 --- a/src/Nethermind/Nethermind.Db/IDbProvider.cs +++ b/src/Nethermind/Nethermind.Db/IDbProvider.cs @@ -33,6 +33,8 @@ public interface IDbProvider : IDisposable public IDb MetadataDb => GetDb(DbNames.Metadata); + public IColumnsDb BlobTransactionsDb => GetDb>(DbNames.BlobTransactions); + T GetDb(string dbName) where T : class, IDb; void RegisterDb(string dbName, T db) where T : class, IDb; diff --git a/src/Nethermind/Nethermind.Db/Metrics.cs b/src/Nethermind/Nethermind.Db/Metrics.cs index 471e13fe9a7..349b7eb430e 100644 --- a/src/Nethermind/Nethermind.Db/Metrics.cs +++ b/src/Nethermind/Nethermind.Db/Metrics.cs @@ -126,6 +126,14 @@ public static class Metrics [Description("Number of Metadata DB writes.")] public static long MetadataDbWrites { get; set; } + [CounterMetric] + [Description("Number of BlobTransactions DB reads.")] + public static long BlobTransactionsDbReads { get; set; } + + [CounterMetric] + [Description("Number of BlobTransactions DB writes.")] + public static long BlobTransactionsDbWrites { get; set; } + [GaugeMetric] [Description("Indicator if StadeDb is being pruned.")] public static int StateDbPruning { get; set; } diff --git a/src/Nethermind/Nethermind.Db/StandardDbInitializer.cs b/src/Nethermind/Nethermind.Db/StandardDbInitializer.cs index e38a0d7fbd3..2c2a2f2c6fc 100644 --- a/src/Nethermind/Nethermind.Db/StandardDbInitializer.cs +++ b/src/Nethermind/Nethermind.Db/StandardDbInitializer.cs @@ -12,33 +12,30 @@ namespace Nethermind.Db public class StandardDbInitializer : RocksDbInitializer { private readonly IFileSystem _fileSystem; - private readonly bool _fullPruning; public StandardDbInitializer( IDbProvider? dbProvider, IRocksDbFactory? rocksDbFactory, IMemDbFactory? memDbFactory, - IFileSystem? fileSystem = null, - bool fullPruning = false) + IFileSystem? fileSystem = null) : base(dbProvider, rocksDbFactory, memDbFactory) { _fileSystem = fileSystem ?? new FileSystem(); - _fullPruning = fullPruning; } - public void InitStandardDbs(bool useReceiptsDb) + public void InitStandardDbs(bool useReceiptsDb, bool useBlobsDb = true) { - RegisterAll(useReceiptsDb); + RegisterAll(useReceiptsDb, useBlobsDb); InitAll(); } - public async Task InitStandardDbsAsync(bool useReceiptsDb) + public async Task InitStandardDbsAsync(bool useReceiptsDb, bool useBlobsDb = true) { - RegisterAll(useReceiptsDb); + RegisterAll(useReceiptsDb, useBlobsDb); await InitAllAsync(); } - private void RegisterAll(bool useReceiptsDb) + private void RegisterAll(bool useReceiptsDb, bool useBlobsDb) { RegisterDb(BuildRocksDbSettings(DbNames.Blocks, () => Metrics.BlocksDbReads++, () => Metrics.BlocksDbWrites++)); RegisterDb(BuildRocksDbSettings(DbNames.Headers, () => Metrics.HeaderDbReads++, () => Metrics.HeaderDbWrites++)); @@ -66,6 +63,10 @@ private void RegisterAll(bool useReceiptsDb) RegisterCustomDb(DbNames.Receipts, () => new ReadOnlyColumnsDb(new MemColumnsDb(), false)); } RegisterDb(BuildRocksDbSettings(DbNames.Metadata, () => Metrics.MetadataDbReads++, () => Metrics.MetadataDbWrites++)); + if (useBlobsDb) + { + RegisterColumnsDb(BuildRocksDbSettings(DbNames.BlobTransactions, () => Metrics.BlobTransactionsDbReads++, () => Metrics.BlobTransactionsDbWrites++)); + } } private RocksDbSettings BuildRocksDbSettings(string dbName, Action updateReadsMetrics, Action updateWriteMetrics, bool deleteOnStart = false) diff --git a/src/Nethermind/Nethermind.EthStats/Integrations/EthStatsIntegration.cs b/src/Nethermind/Nethermind.EthStats/Integrations/EthStatsIntegration.cs index 238d5772f0a..5e4a171024b 100644 --- a/src/Nethermind/Nethermind.EthStats/Integrations/EthStatsIntegration.cs +++ b/src/Nethermind/Nethermind.EthStats/Integrations/EthStatsIntegration.cs @@ -142,7 +142,7 @@ private void TimerOnElapsed(object? sender, ElapsedEventArgs e) { if (_logger.IsDebug) _logger.Debug("ETH Stats sending 'stats' message..."); SendStatsAsync(); - SendPendingAsync(_txPool.GetPendingTransactionsCount()); + SendPendingAsync(_txPool.GetPendingTransactionsCount() + _txPool.GetPendingBlobTransactionsCount()); } } diff --git a/src/Nethermind/Nethermind.Init/Steps/InitDatabase.cs b/src/Nethermind/Nethermind.Init/Steps/InitDatabase.cs index 3b1bda2069b..cd1de616fc2 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitDatabase.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitDatabase.cs @@ -14,6 +14,7 @@ using Nethermind.Db.Rpc; using Nethermind.JsonRpc.Client; using Nethermind.Logging; +using Nethermind.TxPool; namespace Nethermind.Init.Steps { @@ -35,7 +36,7 @@ public async Task Execute(CancellationToken _) IDbConfig dbConfig = _api.Config(); ISyncConfig syncConfig = _api.Config(); IInitConfig initConfig = _api.Config(); - IPruningConfig pruningConfig = _api.Config(); + ITxPoolConfig txPoolConfig = _api.Config(); foreach (PropertyInfo propertyInfo in typeof(IDbConfig).GetProperties()) { @@ -45,9 +46,13 @@ public async Task Execute(CancellationToken _) try { bool useReceiptsDb = initConfig.StoreReceipts || syncConfig.DownloadReceiptsInFastSync; + bool useBlobsDb = txPoolConfig is { BlobSupportEnabled: true, PersistentBlobStorageEnabled: true }; InitDbApi(initConfig, dbConfig, initConfig.StoreReceipts || syncConfig.DownloadReceiptsInFastSync); - StandardDbInitializer dbInitializer = new(_api.DbProvider, _api.RocksDbFactory, _api.MemDbFactory, _api.FileSystem, pruningConfig.Mode.IsFull()); - await dbInitializer.InitStandardDbsAsync(useReceiptsDb); + StandardDbInitializer dbInitializer = new(_api.DbProvider, _api.RocksDbFactory, _api.MemDbFactory, _api.FileSystem); + await dbInitializer.InitStandardDbsAsync(useReceiptsDb, useBlobsDb); + _api.BlobTxStorage = useBlobsDb + ? new BlobTxStorage(_api.DbProvider!.BlobTransactionsDb) + : NullBlobTxStorage.Instance; } catch (TypeInitializationException ex) { diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs index 952bdaa678a..4524b38d1f7 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeBlockchain.cs @@ -344,6 +344,7 @@ protected virtual IHealthHintService CreateHealthHintService() => protected virtual TxPool.TxPool CreateTxPool() => new(_api.EthereumEcdsa!, + _api.BlobTxStorage ?? NullBlobTxStorage.Instance, new ChainHeadInfoProvider(_api.SpecProvider!, _api.BlockTree!, _api.StateReader!), _api.Config(), _api.TxValidator!, diff --git a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs index ceade1167cf..06d419ef487 100644 --- a/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs +++ b/src/Nethermind/Nethermind.Init/Steps/InitializeNetwork.cs @@ -40,6 +40,7 @@ using Nethermind.Synchronization.Reporting; using Nethermind.Synchronization.SnapSync; using Nethermind.Synchronization.Trie; +using Nethermind.TxPool; namespace Nethermind.Init.Steps; @@ -528,7 +529,7 @@ private async Task InitPeer() ForkInfo forkInfo = new(_api.SpecProvider!, syncServer.Genesis.Hash!); ProtocolValidator protocolValidator = new(_api.NodeStatsManager!, _api.BlockTree, forkInfo, _api.LogManager); - PooledTxsRequestor pooledTxsRequestor = new(_api.TxPool!); + PooledTxsRequestor pooledTxsRequestor = new(_api.TxPool!, _api.Config()); _api.ProtocolsManager = new ProtocolsManager( _api.SyncPeerPool!, syncServer, diff --git a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/ParityRpcModuleTests.cs b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/ParityRpcModuleTests.cs index e3eac292c0d..04b60294ea7 100644 --- a/src/Nethermind/Nethermind.JsonRpc.Test/Modules/ParityRpcModuleTests.cs +++ b/src/Nethermind/Nethermind.JsonRpc.Test/Modules/ParityRpcModuleTests.cs @@ -74,6 +74,7 @@ public void Initialize() .TestObject; _txPool = new TxPool.TxPool(_ethereumEcdsa, + new BlobTxStorage(), new ChainHeadInfoProvider(new FixedForkActivationChainHeadSpecProvider(specProvider), _blockTree, stateProvider), new TxPoolConfig(), new TxValidator(specProvider.ChainId), diff --git a/src/Nethermind/Nethermind.Network.Benchmark/Eth62ProtocolHandlerBenchmarks.cs b/src/Nethermind/Nethermind.Network.Benchmark/Eth62ProtocolHandlerBenchmarks.cs index 4f2ccbd1125..7c65c42c584 100644 --- a/src/Nethermind/Nethermind.Network.Benchmark/Eth62ProtocolHandlerBenchmarks.cs +++ b/src/Nethermind/Nethermind.Network.Benchmark/Eth62ProtocolHandlerBenchmarks.cs @@ -56,6 +56,7 @@ public void SetUp() var specProvider = MainnetSpecProvider.Instance; TxPool.TxPool txPool = new TxPool.TxPool( ecdsa, + new BlobTxStorage(), new ChainHeadInfoProvider(new FixedForkActivationChainHeadSpecProvider(MainnetSpecProvider.Instance), tree, stateProvider), new TxPoolConfig(), new TxValidator(TestBlockchainIds.ChainId), diff --git a/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V65/PooledTxsRequestorTests.cs b/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V65/PooledTxsRequestorTests.cs index 0bac0287522..5d42d59960d 100644 --- a/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V65/PooledTxsRequestorTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V65/PooledTxsRequestorTests.cs @@ -29,7 +29,7 @@ public class PooledTxsRequestorTests public void filter_properly_newPooledTxHashes() { _response = new List(); - _requestor = new PooledTxsRequestor(_txPool); + _requestor = new PooledTxsRequestor(_txPool, new TxPoolConfig()); _requestor.RequestTransactions(_doNothing, new List { TestItem.KeccakA, TestItem.KeccakD }); _request = new List { TestItem.KeccakA, TestItem.KeccakB, TestItem.KeccakC }; @@ -42,7 +42,7 @@ public void filter_properly_newPooledTxHashes() public void filter_properly_already_pending_hashes() { _response = new List(); - _requestor = new PooledTxsRequestor(_txPool); + _requestor = new PooledTxsRequestor(_txPool, new TxPoolConfig()); _requestor.RequestTransactions(_doNothing, new List { TestItem.KeccakA, TestItem.KeccakB, TestItem.KeccakC }); _request = new List { TestItem.KeccakA, TestItem.KeccakB, TestItem.KeccakC }; @@ -54,7 +54,7 @@ public void filter_properly_already_pending_hashes() public void filter_properly_discovered_hashes() { _response = new List(); - _requestor = new PooledTxsRequestor(_txPool); + _requestor = new PooledTxsRequestor(_txPool, new TxPoolConfig()); _request = new List { TestItem.KeccakA, TestItem.KeccakB, TestItem.KeccakC }; _expected = new List { TestItem.KeccakA, TestItem.KeccakB, TestItem.KeccakC }; @@ -66,7 +66,7 @@ public void filter_properly_discovered_hashes() public void can_handle_empty_argument() { _response = new List(); - _requestor = new PooledTxsRequestor(_txPool); + _requestor = new PooledTxsRequestor(_txPool, new TxPoolConfig()); _requestor.RequestTransactions(Send, new List()); _response.Should().BeEmpty(); } @@ -77,7 +77,7 @@ public void filter_properly_hashes_present_in_hashCache() _response = new List(); ITxPool txPool = Substitute.For(); txPool.IsKnown(Arg.Any()).Returns(true); - _requestor = new PooledTxsRequestor(txPool); + _requestor = new PooledTxsRequestor(txPool, new TxPoolConfig()); _request = new List { TestItem.KeccakA, TestItem.KeccakB }; _expected = new List { }; diff --git a/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandlerTests.cs b/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandlerTests.cs index 4598b159ba6..e56f5af3e2b 100644 --- a/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandlerTests.cs @@ -304,7 +304,7 @@ public void should_request_in_GetPooledTransactionsMessage_up_to_256_txs(int num new NodeStatsManager(_timerFactory, LimboLogs.Instance), _syncManager, _transactionPool, - new PooledTxsRequestor(_transactionPool), + new PooledTxsRequestor(_transactionPool, new TxPoolConfig()), _gossipPolicy, new ForkInfo(_specProvider, _genesisBlock.Header.Hash!), LimboLogs.Instance); diff --git a/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandlerTests.cs b/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandlerTests.cs index a61f602f6d2..91b21fca318 100644 --- a/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandlerTests.cs +++ b/src/Nethermind/Nethermind.Network.Test/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandlerTests.cs @@ -115,7 +115,7 @@ public void Can_handle_NewPooledTransactions_message([Values(0, 1, 2, 100)] int HandleZeroMessage(msg, Eth68MessageCode.NewPooledTransactionHashes); _pooledTxsRequestor.Received(canGossipTransactions ? 1 : 0).RequestTransactionsEth68(Arg.Any>(), - Arg.Any>(), Arg.Any>()); + Arg.Any>(), Arg.Any>(), Arg.Any>()); } [TestCase(true)] @@ -153,7 +153,7 @@ public void Should_process_huge_transaction() HandleZeroMessage(msg, Eth68MessageCode.NewPooledTransactionHashes); _pooledTxsRequestor.Received(1).RequestTransactionsEth68(Arg.Any>(), - Arg.Any>(), Arg.Any>()); + Arg.Any>(), Arg.Any>(), Arg.Any>()); } [TestCase(1)] @@ -202,7 +202,7 @@ public void should_divide_GetPooledTransactionsMessage_if_max_message_size_is_ex new NodeStatsManager(_timerFactory, LimboLogs.Instance), _syncManager, _transactionPool, - new PooledTxsRequestor(_transactionPool), + new PooledTxsRequestor(_transactionPool, new TxPoolConfig()), _gossipPolicy, new ForkInfo(_specProvider, _genesisBlock.Header.Hash!), LimboLogs.Instance, diff --git a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/IPooledTxsRequestor.cs b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/IPooledTxsRequestor.cs index 786186ebbf4..3feaaac63e0 100644 --- a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/IPooledTxsRequestor.cs +++ b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/IPooledTxsRequestor.cs @@ -12,6 +12,6 @@ public interface IPooledTxsRequestor { void RequestTransactions(Action send, IReadOnlyList hashes); void RequestTransactionsEth66(Action send, IReadOnlyList hashes); - void RequestTransactionsEth68(Action send, IReadOnlyList hashes, IReadOnlyList sizes); + void RequestTransactionsEth68(Action send, IReadOnlyList hashes, IReadOnlyList sizes, IReadOnlyList types); } } diff --git a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/PooledTxsRequestor.cs b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/PooledTxsRequestor.cs index 3dd79c22bbd..a882bbf3b32 100644 --- a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/PooledTxsRequestor.cs +++ b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/PooledTxsRequestor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Nethermind.Core; using Nethermind.Core.Caching; using Nethermind.Core.Collections; using Nethermind.Core.Crypto; @@ -16,12 +17,15 @@ public class PooledTxsRequestor : IPooledTxsRequestor { private const int MaxNumberOfTxsInOneMsg = 256; private readonly ITxPool _txPool; + private readonly ITxPoolConfig _txPoolConfig; + private readonly LruKeyCache _pendingHashes = new(MemoryAllowance.TxHashCacheSize, Math.Min(1024 * 16, MemoryAllowance.TxHashCacheSize), "pending tx hashes"); - public PooledTxsRequestor(ITxPool txPool) + public PooledTxsRequestor(ITxPool txPool, ITxPoolConfig txPoolConfig) { _txPool = txPool; + _txPoolConfig = txPoolConfig; } public void RequestTransactions(Action send, IReadOnlyList hashes) @@ -68,10 +72,10 @@ public void RequestTransactionsEth66(Action send, IReadOnlyList hashes, IReadOnlyList sizes) + public void RequestTransactionsEth68(Action send, IReadOnlyList hashes, IReadOnlyList sizes, IReadOnlyList types) { - using ArrayPoolList<(Keccak Hash, int Size)> discoveredTxHashesAndSizes = new(hashes.Count); - AddMarkUnknownHashesEth68(hashes, sizes, discoveredTxHashesAndSizes); + using ArrayPoolList<(Keccak Hash, byte Type, int Size)> discoveredTxHashesAndSizes = new(hashes.Count); + AddMarkUnknownHashesEth68(hashes, sizes, types, discoveredTxHashesAndSizes); if (discoveredTxHashesAndSizes.Count != 0) { @@ -81,6 +85,7 @@ public void RequestTransactionsEth68(Action packetSizeLeft && hashesToRequest.Count > 0) { @@ -89,8 +94,11 @@ public void RequestTransactionsEth68(Action 0) @@ -112,15 +120,15 @@ private void AddMarkUnknownHashes(IReadOnlyList hashes, ArrayPoolList hashes, IReadOnlyList sizes, ArrayPoolList<(Keccak, int)> discoveredTxHashesAndSizes) + private void AddMarkUnknownHashesEth68(IReadOnlyList hashes, IReadOnlyList sizes, IReadOnlyList types, ArrayPoolList<(Keccak, byte, int)> discoveredTxHashesAndSizes) { int count = hashes.Count; for (int i = 0; i < count; i++) { Keccak hash = hashes[i]; - if (!_txPool.IsKnown(hash) && _pendingHashes.Set(hash)) + if (!_txPool.IsKnown(hash) && !_txPool.ContainsTx(hash, (TxType)types[i]) && _pendingHashes.Set(hash)) { - discoveredTxHashesAndSizes.Add((hash, sizes[i])); + discoveredTxHashesAndSizes.Add((hash, types[i], sizes[i])); } } } diff --git a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V65/Eth65ProtocolHandler.cs b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V65/Eth65ProtocolHandler.cs index 4ede85c04e1..49867cc5bea 100644 --- a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V65/Eth65ProtocolHandler.cs +++ b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V65/Eth65ProtocolHandler.cs @@ -100,6 +100,7 @@ protected virtual void Handle(NewPooledTransactionHashesMessage msg) Stopwatch stopwatch = Stopwatch.StartNew(); + TxPool.Metrics.PendingTransactionsHashesReceived += msg.Hashes.Count; _pooledTxsRequestor.RequestTransactions(Send, msg.Hashes); stopwatch.Stop(); @@ -146,6 +147,7 @@ internal PooledTransactionsMessage FulfillPooledTransactionsRequest( txsToSend.Add(tx); packetSizeLeft -= txSize; + TxPool.Metrics.PendingTransactionsSent++; } } diff --git a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandler.cs b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandler.cs index cc55d85827c..93f5f310b22 100644 --- a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandler.cs +++ b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V66/Eth66ProtocolHandler.cs @@ -205,6 +205,7 @@ protected override void Handle(NewPooledTransactionHashesMessage msg) bool isTrace = Logger.IsTrace; Stopwatch? stopwatch = isTrace ? Stopwatch.StartNew() : null; + TxPool.Metrics.PendingTransactionsHashesReceived += msg.Hashes.Count; _pooledTxsRequestor.RequestTransactionsEth66(_sendAction, msg.Hashes); stopwatch?.Stop(); diff --git a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandler.cs b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandler.cs index 36d3547a2c9..38ba9ce84c5 100644 --- a/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandler.cs +++ b/src/Nethermind/Nethermind.Network/P2P/Subprotocols/Eth/V68/Eth68ProtocolHandler.cs @@ -88,12 +88,13 @@ private void Handle(NewPooledTransactionHashesMessage68 message) } Metrics.Eth68NewPooledTransactionHashesReceived++; + TxPool.Metrics.PendingTransactionsHashesReceived += message.Hashes.Count; AddNotifiedTransactions(message.Hashes); Stopwatch? stopwatch = isTrace ? Stopwatch.StartNew() : null; - _pooledTxsRequestor.RequestTransactionsEth68(_sendAction, message.Hashes, message.Sizes); + _pooledTxsRequestor.RequestTransactionsEth68(_sendAction, message.Hashes, message.Sizes, message.Types); stopwatch?.Stop(); diff --git a/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs b/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs index 640a17aaf84..bcef1d4ba97 100644 --- a/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs +++ b/src/Nethermind/Nethermind.Serialization.Rlp/RlpStream.cs @@ -1409,7 +1409,7 @@ public override string ToString() return $"[{nameof(RlpStream)}|{Position}/{Length}]"; } - internal byte[][] DecodeByteArrays() + public byte[][] DecodeByteArrays() { int length = ReadSequenceLength(); if (length is 0) diff --git a/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs b/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs index fd913fa8f3c..10f2000f23f 100644 --- a/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs +++ b/src/Nethermind/Nethermind.Synchronization.Test/SyncThreadTests.cs @@ -264,8 +264,13 @@ private SyncTestContext CreateSyncManager(int index) ITransactionComparerProvider transactionComparerProvider = new TransactionComparerProvider(specProvider, tree); - TxPool.TxPool txPool = new(ecdsa, new ChainHeadInfoProvider(specProvider, tree, stateReader), - new TxPoolConfig(), new TxValidator(specProvider.ChainId), logManager, transactionComparerProvider.GetDefaultComparer()); + TxPool.TxPool txPool = new(ecdsa, + new BlobTxStorage(), + new ChainHeadInfoProvider(specProvider, tree, stateReader), + new TxPoolConfig(), + new TxValidator(specProvider.ChainId), + logManager, + transactionComparerProvider.GetDefaultComparer()); BlockhashProvider blockhashProvider = new(tree, LimboLogs.Instance); VirtualMachine virtualMachine = new(blockhashProvider, specProvider, logManager); diff --git a/src/Nethermind/Nethermind.TxPool.Test/BlobTxStorageTests.cs b/src/Nethermind/Nethermind.TxPool.Test/BlobTxStorageTests.cs new file mode 100644 index 00000000000..385ae8e9d9d --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool.Test/BlobTxStorageTests.cs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using FluentAssertions; +using Nethermind.Core; +using Nethermind.Core.Test.Builders; +using Nethermind.Db; +using NUnit.Framework; + +namespace Nethermind.TxPool.Test; + +[TestFixture] +public class BlobTxStorageTests +{ + [Test] + public void should_throw_when_trying_to_add_null_tx() + { + BlobTxStorage blobTxStorage = new(); + + Action act = () => blobTxStorage.Add(null); + act.Should().Throw(); + } + + [Test] + public void should_throw_when_trying_to_add_tx_with_null_hash() + { + BlobTxStorage blobTxStorage = new(); + + Transaction tx = Build.A.Transaction.TestObject; + tx.Hash = null; + + Action act = () => blobTxStorage.Add(tx); + act.Should().Throw(); + } +} diff --git a/src/Nethermind/Nethermind.TxPool.Test/TxBroadcasterTests.cs b/src/Nethermind/Nethermind.TxPool.Test/TxBroadcasterTests.cs index 7d911711971..4c1daef9c3d 100644 --- a/src/Nethermind/Nethermind.TxPool.Test/TxBroadcasterTests.cs +++ b/src/Nethermind/Nethermind.TxPool.Test/TxBroadcasterTests.cs @@ -112,13 +112,8 @@ public async Task should_not_broadcast_persisted_tx_to_peer_too_quickly() peer.Received(2).SendNewTransactions(Arg.Any>(), true); } - [TestCase(1)] - [TestCase(2)] - [TestCase(99)] - [TestCase(100)] - [TestCase(101)] - [TestCase(1000)] - public void should_pick_best_persistent_txs_to_broadcast(int threshold) + [Test] + public void should_pick_best_persistent_txs_to_broadcast([Values(1, 2, 99, 100, 101, 1000)] int threshold) { _txPoolConfig = new TxPoolConfig() { PeerNotificationThreshold = threshold }; _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); @@ -154,20 +149,106 @@ public void should_pick_best_persistent_txs_to_broadcast(int threshold) expectedTxs.Should().BeEquivalentTo(pickedTxs); } - [TestCase(1)] - [TestCase(2)] - [TestCase(25)] - [TestCase(50)] - [TestCase(99)] - [TestCase(100)] - [TestCase(101)] - [TestCase(1000)] - public void should_skip_blob_txs_when_picking_best_persistent_txs_to_broadcast(int threshold) + [Test] + public void should_add_light_form_of_blob_txs_to_persistent_txs_but_not_return_if_requested([Values(true, false)] bool isBlob) + { + _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); + + Transaction tx = Build.A.Transaction + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .SignedAndResolved().TestObject; + + _broadcaster.Broadcast(tx, true); + _broadcaster.GetSnapshot().Length.Should().Be(1); + _broadcaster.GetSnapshot().FirstOrDefault().Should().BeEquivalentTo(isBlob ? new LightTransaction(tx) : tx); + + _broadcaster.TryGetPersistentTx(tx.Hash, out Transaction returnedTx).Should().Be(!isBlob); + returnedTx.Should().BeEquivalentTo(isBlob ? null : tx); + } + + [Test] + public void should_announce_details_of_full_blob_tx() + { + _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); + + Transaction tx = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .SignedAndResolved().TestObject; + + Transaction lightTx = new LightTransaction(tx); + + int size = tx.GetLength(); + size.Should().Be(131320); + lightTx.GetLength().Should().Be(size); + + _broadcaster.Broadcast(tx, true); + _broadcaster.GetSnapshot().Length.Should().Be(1); + _broadcaster.GetSnapshot().FirstOrDefault().Should().BeEquivalentTo(lightTx); + + ITxPoolPeer peer = Substitute.For(); + peer.Id.Returns(TestItem.PublicKeyA); + + _broadcaster.AddPeer(peer); + _broadcaster.BroadcastPersistentTxs(); + + peer.Received(1).SendNewTransactions(Arg.Is>(t => t.FirstOrDefault().GetLength() == size), false); + } + + [Test] + public void should_skip_large_txs_when_picking_best_persistent_txs_to_broadcast([Values(1, 2, 25, 50, 99, 100, 101, 1000)] int threshold) + { + _txPoolConfig = new TxPoolConfig() { PeerNotificationThreshold = threshold }; + _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); + _headInfo.CurrentBaseFee.Returns(0.GWei()); + + // add 256 transactions, 10% of them is large + int addedTxsCount = TestItem.PrivateKeys.Length; + Transaction[] transactions = new Transaction[addedTxsCount]; + + for (int i = 0; i < addedTxsCount; i++) + { + bool isLarge = i % 10 == 0; + transactions[i] = Build.A.Transaction + .WithType(TxType.EIP1559) + .WithGasPrice((addedTxsCount - i - 1).GWei()) + .WithData(isLarge ? new byte[4 * 1024] : Array.Empty()) //some part of txs (10%) is large (>4KB) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeys[i]) + .TestObject; + + _broadcaster.Broadcast(transactions[i], true); + } + + _broadcaster.GetSnapshot().Length.Should().Be(addedTxsCount); + + // count numbers of expected hashes and full transactions + int expectedCountTotal = Math.Min(addedTxsCount * threshold / 100 + 1, addedTxsCount); + int expectedCountOfHashes = expectedCountTotal / 10 + 1; + int expectedCountOfFullTxs = expectedCountTotal - expectedCountOfHashes; + + // prepare list of expected full transactions and hashes + (IList expectedFullTxs, IList expectedHashes) = GetTxsAndHashesExpectedToBroadcast(transactions, expectedCountTotal); + + // get hashes and full transactions to broadcast + (IList pickedFullTxs, IList pickedHashes) = _broadcaster.GetPersistentTxsToSend(); + + // check if numbers of full transactions and hashes are correct + CheckCorrectness(pickedFullTxs, expectedCountOfFullTxs); + CheckCorrectness(pickedHashes, expectedCountOfHashes); + + // check if full transactions and hashes returned by broadcaster are as expected + expectedFullTxs.Should().BeEquivalentTo(pickedFullTxs); + expectedHashes.Should().BeEquivalentTo(pickedHashes.Select(t => t.Hash).ToArray()); + } + + [Test] + public void should_skip_blob_txs_when_picking_best_persistent_txs_to_broadcast([Values(1, 2, 25, 50, 99, 100, 101, 1000)] int threshold) { _txPoolConfig = new TxPoolConfig() { PeerNotificationThreshold = threshold }; _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); _headInfo.CurrentBaseFee.Returns(0.GWei()); + // add 256 transactions, 10% of them is blob type int addedTxsCount = TestItem.PrivateKeys.Length; Transaction[] transactions = new Transaction[addedTxsCount]; @@ -186,57 +267,28 @@ public void should_skip_blob_txs_when_picking_best_persistent_txs_to_broadcast(i _broadcaster.GetSnapshot().Length.Should().Be(addedTxsCount); - (IList pickedTxs, IList pickedHashes) = _broadcaster.GetPersistentTxsToSend(); - + // count numbers of expected hashes and full transactions int expectedCountTotal = Math.Min(addedTxsCount * threshold / 100 + 1, addedTxsCount); int expectedCountOfBlobHashes = expectedCountTotal / 10 + 1; int expectedCountOfNonBlobTxs = expectedCountTotal - expectedCountOfBlobHashes; - if (expectedCountOfNonBlobTxs > 0) - { - pickedTxs.Count.Should().Be(expectedCountOfNonBlobTxs); - } - else - { - pickedTxs.Should().BeNull(); - } - - if (expectedCountOfBlobHashes > 0) - { - pickedHashes.Count.Should().Be(expectedCountOfBlobHashes); - } - else - { - pickedHashes.Should().BeNull(); - } - List expectedTxs = new(); - List expectedHashes = new(); + // prepare list of expected full transactions and hashes + (IList expectedFullTxs, IList expectedHashes) = GetTxsAndHashesExpectedToBroadcast(transactions, expectedCountTotal); - for (int i = 0; i < expectedCountTotal; i++) - { - Transaction tx = transactions[i]; + // get hashes and full transactions to broadcast + (IList pickedFullTxs, IList pickedHashes) = _broadcaster.GetPersistentTxsToSend(); - if (!tx.SupportsBlobs) - { - expectedTxs.Add(tx); - } - else - { - expectedHashes.Add(tx); - } - } + // check if numbers of full transactions and hashes are correct + CheckCorrectness(pickedFullTxs, expectedCountOfNonBlobTxs); + CheckCorrectness(pickedHashes, expectedCountOfBlobHashes); - expectedTxs.Should().BeEquivalentTo(pickedTxs); - expectedHashes.Should().BeEquivalentTo(pickedHashes); + // check if full transactions and hashes returned by broadcaster are as expected + expectedFullTxs.Should().BeEquivalentTo(pickedFullTxs); + expectedHashes.Should().BeEquivalentTo(pickedHashes.Select(t => t.Hash).ToArray()); } - [TestCase(1)] - [TestCase(2)] - [TestCase(99)] - [TestCase(100)] - [TestCase(101)] - [TestCase(1000)] - public void should_not_pick_txs_with_GasPrice_lower_than_CurrentBaseFee(int threshold) + [Test] + public void should_not_pick_txs_with_GasPrice_lower_than_CurrentBaseFee([Values(1, 2, 99, 100, 101, 1000)] int threshold) { _txPoolConfig = new TxPoolConfig() { PeerNotificationThreshold = threshold }; _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); @@ -279,13 +331,8 @@ public void should_not_pick_txs_with_GasPrice_lower_than_CurrentBaseFee(int thre expectedTxs.Should().BeEquivalentTo(pickedTxs); } - [TestCase(1)] - [TestCase(2)] - [TestCase(99)] - [TestCase(100)] - [TestCase(101)] - [TestCase(1000)] - public void should_not_pick_1559_txs_with_MaxFeePerGas_lower_than_CurrentBaseFee(int threshold) + [Test] + public void should_not_pick_1559_txs_with_MaxFeePerGas_lower_than_CurrentBaseFee([Values(1, 2, 99, 100, 101, 1000)] int threshold) { _txPoolConfig = new TxPoolConfig() { PeerNotificationThreshold = threshold }; _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); @@ -329,6 +376,52 @@ public void should_not_pick_1559_txs_with_MaxFeePerGas_lower_than_CurrentBaseFee expectedTxs.Should().BeEquivalentTo(pickedTxs, o => o.Excluding(transaction => transaction.MaxFeePerGas)); } + [Test] + public void should_not_pick_blob_txs_with_MaxFeePerBlobGas_lower_than_CurrentPricePerBlobGas([Values(1, 2, 99, 100, 101, 1000)] int threshold) + { + _txPoolConfig = new TxPoolConfig() { PeerNotificationThreshold = threshold }; + _broadcaster = new TxBroadcaster(_comparer, TimerFactory.Default, _txPoolConfig, _headInfo, _logManager); + + const int currentPricePerBlobGasInGwei = 250; + _headInfo.CurrentPricePerBlobGas.Returns(currentPricePerBlobGasInGwei.GWei()); + + // add 256 transactions with MaxFeePerBlobGas 0-255 + int addedTxsCount = TestItem.PrivateKeys.Length; + Transaction[] transactions = new Transaction[addedTxsCount]; + + for (int i = 0; i < addedTxsCount; i++) + { + transactions[i] = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithMaxFeePerBlobGas(i.GWei()) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeys[i]) + .TestObject; + + _broadcaster.Broadcast(transactions[i], true); + } + + _broadcaster.GetSnapshot().Length.Should().Be(addedTxsCount); + + // count number of expected hashes to broadcast + int expectedCount = Math.Min(addedTxsCount * threshold / 100 + 1, addedTxsCount - currentPricePerBlobGasInGwei); + + // prepare list of expected hashes to broadcast + List expectedTxs = new(); + for (int i = 1; i <= expectedCount; i++) + { + expectedTxs.Add(transactions[addedTxsCount - i]); + } + + // get actual hashes to broadcast + IList pickedHashes = _broadcaster.GetPersistentTxsToSend().HashesToSend; + + // check if number of hashes to broadcast is correct + pickedHashes.Count.Should().Be(expectedCount); + + // check if number of hashes to broadcast (with MaxFeePerBlobGas >= current) is correct + expectedTxs.Count(t => t.MaxFeePerBlobGas >= (UInt256)currentPricePerBlobGasInGwei).Should().Be(expectedCount); + } + [Test] public void should_pick_tx_with_lowest_nonce_from_bucket() { @@ -560,4 +653,38 @@ public void should_rebroadcast_all_persistent_transactions_if_PeerNotificationTh pickedTxs[i].Nonce.Should().Be((UInt256)i); } } + + private (IList expectedTxs, IList expectedHashes) GetTxsAndHashesExpectedToBroadcast(Transaction[] transactions, int expectedCountTotal) + { + List expectedTxs = new(); + List expectedHashes = new(); + + for (int i = 0; i < expectedCountTotal; i++) + { + Transaction tx = transactions[i]; + + if (tx.CanBeBroadcast()) + { + expectedTxs.Add(tx); + } + else + { + expectedHashes.Add(tx.Hash); + } + } + + return (expectedTxs, expectedHashes); + } + + private void CheckCorrectness(IList pickedTxs, int expectedCount) + { + if (expectedCount > 0) + { + pickedTxs.Count.Should().Be(expectedCount); + } + else + { + pickedTxs.Should().BeNull(); + } + } } diff --git a/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.Blobs.cs b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.Blobs.cs new file mode 100644 index 00000000000..a8f2d8c241e --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.Blobs.cs @@ -0,0 +1,494 @@ +// SPDX-FileCopyrightText: 2022 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Threading.Tasks; +using FluentAssertions; +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Core.Test.Builders; +using Nethermind.Evm; +using Nethermind.Int256; +using NUnit.Framework; + +namespace Nethermind.TxPool.Test +{ + [TestFixture] + public partial class TxPoolTests + { + [Test] + public void should_reject_blob_tx_if_blobs_not_supported([Values(true, false)] bool isBlobSupportEnabled) + { + TxPoolConfig txPoolConfig = new() { BlobSupportEnabled = isBlobSupportEnabled }; + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider()); + + Transaction tx = Build.A.Transaction + .WithNonce(UInt256.Zero) + .WithShardBlobTxTypeAndFields() + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast).Should().Be(isBlobSupportEnabled + ? AcceptTxResult.Accepted + : AcceptTxResult.NotSupportedTxType); + } + + [Test] + public void blob_pool_size_should_be_correct([Values(true, false)] bool persistentStorageEnabled) + { + const int poolSize = 10; + TxPoolConfig txPoolConfig = new() + { + BlobSupportEnabled = true, + PersistentBlobStorageEnabled = persistentStorageEnabled, + PersistentBlobStorageSize = persistentStorageEnabled ? poolSize : 0, + InMemoryBlobPoolSize = persistentStorageEnabled ? 0 : poolSize + }; + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider()); + + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + for (int i = 0; i < poolSize; i++) + { + Transaction tx = Build.A.Transaction + .WithNonce((UInt256)i) + .WithShardBlobTxTypeAndFields() + .WithMaxFeePerGas(1.GWei() + (UInt256)(100 - i)) + .WithMaxPriorityFeePerGas(1.GWei() + (UInt256)(100 - i)) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast).Should().Be(AcceptTxResult.Accepted); + } + + _txPool.GetPendingTransactionsCount().Should().Be(0); + _txPool.GetPendingBlobTransactionsCount().Should().Be(poolSize); + } + + [TestCase(TxType.EIP1559, 0, 5, 100)] + [TestCase(TxType.Blob, 5, 0, 100)] + [TestCase(TxType.EIP1559, 10, 0, 10)] + [TestCase(TxType.Blob, 0, 15, 15)] + [TestCase(TxType.EIP1559, 20, 25, 20)] + [TestCase(TxType.Blob, 30, 35, 35)] + public void should_reject_txs_with_nonce_too_far_in_future(TxType txType, int maxPendingTxs, int maxPendingBlobTxs, int expectedNumberOfAcceptedTxs) + { + TxPoolConfig txPoolConfig = new() + { + BlobSupportEnabled = true, + Size = 100, + MaxPendingTxsPerSender = maxPendingTxs, + MaxPendingBlobTxsPerSender = maxPendingBlobTxs + }; + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + for (int nonce = 0; nonce < txPoolConfig.Size; nonce++) + { + Transaction tx = Build.A.Transaction + .WithNonce((UInt256)nonce) + .WithType(txType) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(tx, TxHandlingOptions.None).Should().Be(nonce > expectedNumberOfAcceptedTxs + ? AcceptTxResult.NonceTooFarInFuture + : AcceptTxResult.Accepted); + } + } + + [Test] + public void should_reject_tx_with_FeeTooLow_even_if_is_blob_type([Values(true, false)] bool isBlob, [Values(true, false)] bool persistentStorageEnabled) + { + const int poolSize = 10; + TxPoolConfig txPoolConfig = new() + { + BlobSupportEnabled = true, + Size = isBlob ? 0 : poolSize, + PersistentBlobStorageEnabled = persistentStorageEnabled, + PersistentBlobStorageSize = persistentStorageEnabled ? poolSize : 0, + InMemoryBlobPoolSize = persistentStorageEnabled ? 0 : poolSize + }; + + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + EnsureSenderBalance(TestItem.AddressB, UInt256.MaxValue); + + for (int i = 0; i < poolSize; i++) + { + Transaction tx = Build.A.Transaction + .WithNonce((UInt256)i) + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithMaxFeePerGas(1.GWei() + (UInt256)(100 - i)) + .WithMaxPriorityFeePerGas(1.GWei() + (UInt256)(100 - i)) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast).Should().Be(AcceptTxResult.Accepted); + } + + _txPool.GetPendingTransactionsCount().Should().Be(isBlob ? 0 : poolSize); + _txPool.GetPendingBlobTransactionsCount().Should().Be(isBlob ? poolSize : 0); + + Transaction feeTooLowTx = Build.A.Transaction + .WithNonce(UInt256.Zero) + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithMaxFeePerGas(1.GWei() + UInt256.One) + .WithMaxPriorityFeePerGas(1.GWei() + UInt256.One) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyB).TestObject; + + _txPool.SubmitTx(feeTooLowTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.FeeTooLow); + } + + [Test] + public void should_add_blob_tx_and_return_when_requested([Values(true, false)] bool isPersistentStorage) + { + TxPoolConfig txPoolConfig = new() + { + BlobSupportEnabled = true, + Size = 10, + PersistentBlobStorageEnabled = isPersistentStorage + }; + BlobTxStorage blobTxStorage = new(); + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider(), txStorage: blobTxStorage); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction blobTxAdded = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .WithNonce(UInt256.Zero) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(blobTxAdded, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.TryGetPendingTransaction(blobTxAdded.Hash!, out Transaction blobTxReturned); + + blobTxReturned.Should().BeEquivalentTo(blobTxAdded); + + blobTxStorage.TryGet(blobTxAdded.Hash, blobTxAdded.SenderAddress!, blobTxAdded.Timestamp, out Transaction blobTxFromDb).Should().Be(isPersistentStorage); // additional check for persistent db + if (isPersistentStorage) + { + blobTxFromDb.Should().BeEquivalentTo(blobTxAdded, options => options + .Excluding(t => t.GasBottleneck) // GasBottleneck is not encoded/decoded... + .Excluding(t => t.PoolIndex)); // ...as well as PoolIndex + } + } + + [Test] + public void should_not_throw_when_asking_for_non_existing_tx() + { + TxPoolConfig txPoolConfig = new() { Size = 10 }; + BlobTxStorage blobTxStorage = new(); + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider(), txStorage: blobTxStorage); + + _txPool.TryGetPendingTransaction(TestItem.KeccakA, out Transaction blobTxReturned).Should().BeFalse(); + blobTxReturned.Should().BeNull(); + + blobTxStorage.TryGet(TestItem.KeccakA, TestItem.AddressA, UInt256.One, out Transaction blobTxFromDb).Should().BeFalse(); + blobTxFromDb.Should().BeNull(); + } + + [TestCase(999_999_999, false)] + [TestCase(1_000_000_000, true)] + public void should_not_allow_to_add_blob_tx_with_MaxPriorityFeePerGas_lower_than_1GWei(int maxPriorityFeePerGas, bool expectedResult) + { + TxPoolConfig txPoolConfig = new() { BlobSupportEnabled = true, Size = 10 }; + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction tx = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithMaxFeePerGas((UInt256)maxPriorityFeePerGas) + .WithMaxPriorityFeePerGas((UInt256)maxPriorityFeePerGas) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(tx, TxHandlingOptions.None).Should().Be(expectedResult + ? AcceptTxResult.Accepted + : AcceptTxResult.FeeTooLow); + } + + [Test] + public void should_not_add_nonce_gap_blob_tx_even_to_not_full_TxPool([Values(true, false)] bool isBlob) + { + _txPool = CreatePool(new TxPoolConfig() { BlobSupportEnabled = true, Size = 128 }, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction firstTx = Build.A.Transaction + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .WithNonce(UInt256.Zero) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + Transaction nonceGapTx = Build.A.Transaction + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .WithNonce((UInt256)2) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(firstTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.SubmitTx(nonceGapTx, TxHandlingOptions.None).Should().Be(isBlob ? AcceptTxResult.NonceGap : AcceptTxResult.Accepted); + } + + [Test] + public void should_not_allow_to_have_pending_transactions_of_both_blob_type_and_other([Values(true, false)] bool firstIsBlob, [Values(true, false)] bool secondIsBlob) + { + Transaction GetTx(bool isBlob, UInt256 nonce) + { + return Build.A.Transaction + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .WithNonce(nonce) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + } + + _txPool = CreatePool(new TxPoolConfig() { BlobSupportEnabled = true, Size = 128 }, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction firstTx = GetTx(firstIsBlob, UInt256.Zero); + Transaction secondTx = GetTx(secondIsBlob, UInt256.One); + + _txPool.SubmitTx(firstTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.SubmitTx(secondTx, TxHandlingOptions.None).Should().Be(firstIsBlob ^ secondIsBlob ? AcceptTxResult.PendingTxsOfConflictingType : AcceptTxResult.Accepted); + } + + [Test] + public void should_remove_replaced_blob_tx_from_persistent_storage_and_cache() + { + TxPoolConfig txPoolConfig = new() + { + BlobSupportEnabled = true, + Size = 10, + PersistentBlobStorageEnabled = true + }; + BlobTxStorage blobTxStorage = new(); + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider(), txStorage: blobTxStorage); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction oldTx = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithNonce(UInt256.Zero) + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + Transaction newTx = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithNonce(UInt256.Zero) + .WithMaxFeePerGas(oldTx.MaxFeePerGas * 2) + .WithMaxPriorityFeePerGas(oldTx.MaxPriorityFeePerGas * 2) + .WithMaxFeePerBlobGas(oldTx.MaxFeePerBlobGas * 2) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + + _txPool.SubmitTx(oldTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.GetPendingBlobTransactionsCount().Should().Be(1); + _txPool.TryGetPendingTransaction(oldTx.Hash!, out Transaction blobTxReturned).Should().BeTrue(); + blobTxReturned.Should().BeEquivalentTo(oldTx); + blobTxStorage.TryGet(oldTx.Hash, oldTx.SenderAddress!, oldTx.Timestamp, out Transaction blobTxFromDb).Should().BeTrue(); + blobTxFromDb.Should().BeEquivalentTo(oldTx, options => options + .Excluding(t => t.GasBottleneck) // GasBottleneck is not encoded/decoded... + .Excluding(t => t.PoolIndex)); // ...as well as PoolIndex + + _txPool.SubmitTx(newTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.GetPendingBlobTransactionsCount().Should().Be(1); + _txPool.TryGetPendingTransaction(newTx.Hash!, out blobTxReturned).Should().BeTrue(); + blobTxReturned.Should().BeEquivalentTo(newTx); + blobTxStorage.TryGet(oldTx.Hash, oldTx.SenderAddress, oldTx.Timestamp, out blobTxFromDb).Should().BeFalse(); + blobTxStorage.TryGet(newTx.Hash, newTx.SenderAddress!, newTx.Timestamp, out blobTxFromDb).Should().BeTrue(); + blobTxFromDb.Should().BeEquivalentTo(newTx, options => options + .Excluding(t => t.GasBottleneck) // GasBottleneck is not encoded/decoded... + .Excluding(t => t.PoolIndex)); // ...as well as PoolIndex + } + + [Test] + public void should_keep_in_memory_only_light_blob_tx_equivalent_if_persistent_storage_enabled([Values(true, false)] bool isPersistentStorage) + { + TxPoolConfig txPoolConfig = new() + { + BlobSupportEnabled = true, + Size = 10, + PersistentBlobStorageEnabled = isPersistentStorage + }; + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction tx = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithNonce(UInt256.Zero) + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .WithMaxFeePerBlobGas(UInt256.One) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(tx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.GetPendingBlobTransactionsCount().Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(0); + + _txPool.TryGetBlobTxSortingEquivalent(tx.Hash!, out Transaction returned); + returned.Should().BeEquivalentTo(isPersistentStorage ? new LightTransaction(tx) : tx); + } + + [Test] + public void should_dump_GasBottleneck_of_blob_tx_to_zero_if_MaxFeePerBlobGas_is_lower_than_current([Values(true, false)] bool isBlob, [Values(true, false)] bool isPersistentStorage) + { + TxPoolConfig txPoolConfig = new() + { + BlobSupportEnabled = true, + Size = 10, + PersistentBlobStorageEnabled = isPersistentStorage + }; + _txPool = CreatePool(txPoolConfig, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + _headInfo.CurrentPricePerBlobGas = UInt256.MaxValue; + + Transaction tx = Build.A.Transaction + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithNonce(UInt256.Zero) + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .WithMaxFeePerBlobGas(isBlob ? UInt256.One : null) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(tx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.GetPendingBlobTransactionsCount().Should().Be(isBlob ? 1 : 0); + _txPool.GetPendingTransactionsCount().Should().Be(isBlob ? 0 : 1); + if (isBlob) + { + _txPool.TryGetBlobTxSortingEquivalent(tx.Hash!, out Transaction returned); + returned.GasBottleneck.Should().Be(UInt256.Zero); + returned.Should().BeEquivalentTo(isPersistentStorage ? new LightTransaction(tx) : tx, + options => options.Excluding(t => t.GasBottleneck)); + returned.Should().NotBeEquivalentTo(isPersistentStorage ? tx : new LightTransaction(tx)); + } + else + { + _txPool.TryGetPendingTransaction(tx.Hash!, out Transaction eip1559Tx); + eip1559Tx.Should().BeEquivalentTo(tx); + eip1559Tx.GasBottleneck.Should().Be(1.GWei()); + } + } + + [Test] + public void should_not_allow_to_replace_blob_tx_by_tx_with_less_blobs([Values(1, 2, 3, 4, 5, 6)] int blobsInFirstTx, [Values(1, 2, 3, 4, 5, 6)] int blobsInSecondTx) + { + bool shouldReplace = blobsInFirstTx <= blobsInSecondTx; + + _txPool = CreatePool(new TxPoolConfig() { BlobSupportEnabled = true, Size = 128 }, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction firstTx = Build.A.Transaction + .WithShardBlobTxTypeAndFields(blobsInFirstTx) + .WithNonce(UInt256.Zero) + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + Transaction secondTx = Build.A.Transaction + .WithShardBlobTxTypeAndFields(blobsInSecondTx) + .WithNonce(UInt256.Zero) + .WithMaxFeePerGas(firstTx.MaxFeePerGas * 2) + .WithMaxPriorityFeePerGas(firstTx.MaxPriorityFeePerGas * 2) + .WithMaxFeePerBlobGas(firstTx.MaxFeePerBlobGas * 2) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(firstTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + + _txPool.GetPendingBlobTransactionsCount().Should().Be(1); + + _txPool.SubmitTx(secondTx, TxHandlingOptions.None).Should().Be(shouldReplace ? AcceptTxResult.Accepted : AcceptTxResult.ReplacementNotAllowed); + _txPool.GetPendingBlobTransactionsCount().Should().Be(1); + _txPool.TryGetPendingTransaction(firstTx.Hash!, out Transaction returnedFirstTx).Should().Be(!shouldReplace); + _txPool.TryGetPendingTransaction(secondTx.Hash!, out Transaction returnedSecondTx).Should().Be(shouldReplace); + returnedFirstTx.Should().BeEquivalentTo(shouldReplace ? null : firstTx); + returnedSecondTx.Should().BeEquivalentTo(shouldReplace ? secondTx : null); + } + + [Test] + public void should_discard_tx_when_data_gas_cost_cause_overflow([Values(false, true)] bool supportsBlobs) + { + _txPool = CreatePool(new TxPoolConfig() { BlobSupportEnabled = true }, GetCancunSpecProvider()); + + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + UInt256.MaxValue.Divide(GasCostOf.Transaction * 2, out UInt256 halfOfMaxGasPriceWithoutOverflow); + + Transaction firstTransaction = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithMaxFeePerBlobGas(UInt256.Zero) + .WithNonce(UInt256.Zero) + .WithMaxFeePerGas(halfOfMaxGasPriceWithoutOverflow) + .WithMaxPriorityFeePerGas(halfOfMaxGasPriceWithoutOverflow) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + _txPool.SubmitTx(firstTransaction, TxHandlingOptions.PersistentBroadcast).Should().Be(AcceptTxResult.Accepted); + + Transaction transactionWithPotentialOverflow = Build.A.Transaction + .WithShardBlobTxTypeAndFields() + .WithMaxFeePerBlobGas(supportsBlobs + ? UInt256.One + : UInt256.Zero) + .WithNonce(UInt256.One) + .WithMaxFeePerGas(halfOfMaxGasPriceWithoutOverflow) + .WithMaxPriorityFeePerGas(halfOfMaxGasPriceWithoutOverflow) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + + _txPool.SubmitTx(transactionWithPotentialOverflow, TxHandlingOptions.PersistentBroadcast).Should().Be(supportsBlobs ? AcceptTxResult.Int256Overflow : AcceptTxResult.Accepted); + } + + [Test] + public async Task should_allow_to_have_pending_transaction_of_other_type_if_conflicting_one_was_included([Values(true, false)] bool firstIsBlob, [Values(true, false)] bool secondIsBlob) + { + Transaction GetTx(bool isBlob, UInt256 nonce) + { + return Build.A.Transaction + .WithType(isBlob ? TxType.Blob : TxType.EIP1559) + .WithShardBlobTxTypeAndFieldsIfBlobTx() + .WithMaxFeePerGas(1.GWei()) + .WithMaxPriorityFeePerGas(1.GWei()) + .WithNonce(nonce) + .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; + } + + _txPool = CreatePool(new TxPoolConfig() { BlobSupportEnabled = true, Size = 128 }, GetCancunSpecProvider()); + EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); + + Transaction firstTx = GetTx(firstIsBlob, UInt256.Zero); + Transaction secondTx = GetTx(secondIsBlob, UInt256.One); + + _txPool.SubmitTx(firstTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + + _txPool.GetPendingTransactionsCount().Should().Be(firstIsBlob ? 0 : 1); + _txPool.GetPendingBlobTransactionsCount().Should().Be(firstIsBlob ? 1 : 0); + _stateProvider.IncrementNonce(TestItem.AddressA); + await RaiseBlockAddedToMainAndWaitForTransactions(1); + + _txPool.GetPendingTransactionsCount().Should().Be(0); + _txPool.GetPendingBlobTransactionsCount().Should().Be(0); + _txPool.SubmitTx(secondTx, TxHandlingOptions.None).Should().Be(AcceptTxResult.Accepted); + _txPool.GetPendingTransactionsCount().Should().Be(secondIsBlob ? 0 : 1); + _txPool.GetPendingBlobTransactionsCount().Should().Be(secondIsBlob ? 1 : 0); + } + + [TestCase(0, 97)] + [TestCase(1, 131320)] + [TestCase(2, 262530)] + [TestCase(3, 393737)] + [TestCase(4, 524943)] + [TestCase(5, 656152)] + [TestCase(6, 787361)] + public void should_calculate_size_of_blob_tx_correctly(int numberOfBlobs, int expectedLength) + { + Transaction blobTx = Build.A.Transaction + .WithShardBlobTxTypeAndFields(numberOfBlobs) + .SignedAndResolved() + .TestObject; + blobTx.GetLength().Should().Be(expectedLength); + } + } +} diff --git a/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs index 3a1b0936813..aacc451c11c 100644 --- a/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs +++ b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs @@ -35,7 +35,7 @@ namespace Nethermind.TxPool.Test { [TestFixture] - public class TxPoolTests + public partial class TxPoolTests { private ILogManager _logManager; private IEthereumEcdsa _ethereumEcdsa; @@ -59,6 +59,8 @@ public void Setup() Block block = Build.A.Block.WithNumber(0).TestObject; _blockTree.Head.Returns(block); _blockTree.FindBestSuggestedHeader().Returns(Build.A.BlockHeader.WithNumber(10000000).TestObject); + + KzgPolynomialCommitments.InitializeAsync().Wait(); } [Test] @@ -97,7 +99,7 @@ public void should_ignore_transactions_with_different_chain_id() EthereumEcdsa ecdsa = new(BlockchainIds.Sepolia, _logManager); // default is mainnet, we're passing sepolia Transaction tx = Build.A.Transaction.SignedAndResolved(ecdsa, TestItem.PrivateKeyA).TestObject; AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.Invalid); } @@ -118,7 +120,7 @@ public void should_ignore_transactions_with_insufficient_intrinsic_gas() .TestObject; AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.Invalid); ; } @@ -129,7 +131,7 @@ public void should_not_ignore_old_scheme_signatures() Transaction tx = Build.A.Transaction.SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA, false).TestObject; EnsureSenderBalance(tx); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); result.Should().Be(AcceptTxResult.Accepted); } @@ -141,7 +143,7 @@ public void should_ignore_already_known() EnsureSenderBalance(tx); AcceptTxResult result1 = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); AcceptTxResult result2 = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); result1.Should().Be(AcceptTxResult.Accepted); result2.Should().Be(AcceptTxResult.AlreadyKnown); } @@ -156,7 +158,7 @@ public void should_add_valid_transactions_recovering_its_address() EnsureSenderBalance(tx); tx.SenderAddress = null; AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); result.Should().Be(AcceptTxResult.Accepted); } @@ -193,7 +195,7 @@ public void should_accept_1559_transactions_only_when_eip1559_enabled([Values(fa EnsureSenderBalance(tx); _blockTree.BlockAddedToMain += Raise.EventWith(_blockTree, new BlockReplacementEventArgs(Build.A.Block.WithGasLimit(10000000).TestObject)); AcceptTxResult result = txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - txPool.GetPendingTransactions().Length.Should().Be(eip1559Enabled ? 1 : 0); + txPool.GetPendingTransactionsCount().Should().Be(eip1559Enabled ? 1 : 0); result.Should().Be(eip1559Enabled ? AcceptTxResult.Accepted : AcceptTxResult.Invalid); } @@ -208,21 +210,13 @@ public void should_not_ignore_insufficient_funds_for_eip1559_transactions() .WithValue(5).SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; EnsureSenderBalance(tx.SenderAddress, tx.Value - 1); // we should have InsufficientFunds if balance < tx.Value + fee AcceptTxResult result = txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - txPool.GetPendingTransactions().Length.Should().Be(0); + txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.InsufficientFunds); EnsureSenderBalance(tx.SenderAddress, tx.Value); _blockTree.BlockAddedToMain += Raise.EventWith(_blockTree, new BlockReplacementEventArgs(Build.A.Block.WithGasLimit(10000000).TestObject)); result = txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); result.Should().Be(AcceptTxResult.InsufficientFunds); - txPool.GetPendingTransactions().Length.Should().Be(0); - } - - private static ISpecProvider GetLondonSpecProvider() - { - var specProvider = Substitute.For(); - specProvider.GetSpec(Arg.Any()).Returns(London.Instance); - specProvider.GetSpec(Arg.Any()).Returns(London.Instance); - return specProvider; + txPool.GetPendingTransactionsCount().Should().Be(0); } [TestCase(false, false, ExpectedResult = nameof(AcceptTxResult.Accepted))] @@ -247,7 +241,7 @@ public void should_ignore_insufficient_funds_transactions() _txPool = CreatePool(); Transaction tx = Build.A.Transaction.SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.InsufficientFunds); } @@ -259,7 +253,7 @@ public void should_ignore_old_nonce_transactions() EnsureSenderBalance(tx); _stateProvider.IncrementNonce(tx.SenderAddress); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.OldNonce); } @@ -320,7 +314,7 @@ public void should_ignore_overflow_transactions() .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; EnsureSenderBalance(tx); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.Int256Overflow); } @@ -338,7 +332,7 @@ public void should_ignore_overflow_transactions_gas_premium_and_fee_cap() .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; EnsureSenderBalance(tx.SenderAddress, UInt256.MaxValue); AcceptTxResult result = txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - txPool.GetPendingTransactions().Length.Should().Be(0); + txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.Int256Overflow); } @@ -352,7 +346,7 @@ public void should_ignore_block_gas_limit_exceeded() EnsureSenderBalance(tx); _headInfo.BlockGasLimit = Transaction.BaseTxGasCost * 4; AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.GasLimitExceeded); } @@ -373,7 +367,7 @@ public void should_accept_tx_when_base_fee_is_high() EnsureSenderBalance(tx); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); result.Should().Be(AcceptTxResult.Accepted); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); } [Test] @@ -385,7 +379,7 @@ public void should_ignore_tx_gas_limit_exceeded() .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; EnsureSenderBalance(tx); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.GasLimitExceeded); } @@ -432,7 +426,7 @@ public void should_handle_adding_tx_to_full_txPool_properly(int gasPrice, int va .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; tx.Value = (UInt256)(value * tx.GasLimit); EnsureSenderBalance(tx.SenderAddress, (UInt256)(15 * tx.GasLimit)); - _txPool.GetPendingTransactions().Length.Should().Be(30); + _txPool.GetPendingTransactionsCount().Should().Be(30); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.None); result.ToString().Should().Contain(expected); } @@ -475,9 +469,9 @@ public void should_handle_adding_1559_tx_to_full_txPool_properly(int gasPremium, .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; tx.Value = (UInt256)(value * tx.GasLimit); EnsureSenderBalance(tx.SenderAddress, (UInt256)(15 * tx.GasLimit)); - _txPool.GetPendingTransactions().Length.Should().Be(30); + _txPool.GetPendingTransactionsCount().Should().Be(30); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.None); - _txPool.GetPendingTransactions().Length.Should().Be(30); + _txPool.GetPendingTransactionsCount().Should().Be(30); result.ToString().Should().Contain(expected); } @@ -506,10 +500,10 @@ public void should_add_underpaid_txs_to_full_TxPool_only_if_local(bool isLocal) .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA) .TestObject; EnsureSenderBalance(tx.SenderAddress, UInt256.MaxValue); - _txPool.GetPendingTransactions().Length.Should().Be(30); + _txPool.GetPendingTransactionsCount().Should().Be(30); _txPool.GetOwnPendingTransactions().Length.Should().Be(0); AcceptTxResult result = _txPool.SubmitTx(tx, txHandlingOptions); - _txPool.GetPendingTransactions().Length.Should().Be(30); + _txPool.GetPendingTransactionsCount().Should().Be(30); _txPool.GetOwnPendingTransactions().Length.Should().Be(isLocal ? 1 : 0); result.ToString().Should().Contain(isLocal ? nameof(AcceptTxResult.FeeTooLowToCompete) : nameof(AcceptTxResult.FeeTooLow)); } @@ -604,13 +598,13 @@ public void should_add_tx_if_cost_of_executing_all_txs_in_bucket_exceeds_balance } } - _txPool.GetPendingTransactions().Length.Should().Be(8); // nonces 0-6 and 8 + _txPool.GetPendingTransactionsCount().Should().Be(8); // nonces 0-6 and 8 _txPool.GetPendingTransactions().Last().Nonce.Should().Be(8); _txPool.SubmitTx(transactions[8], TxHandlingOptions.PersistentBroadcast).Should().Be(AcceptTxResult.AlreadyKnown); _txPool.SubmitTx(transactions[7], TxHandlingOptions.PersistentBroadcast).Should().Be(AcceptTxResult.Accepted); - _txPool.GetPendingTransactions().Length.Should().Be(8); // nonces 0-7 - 8 was removed because of not enough balance + _txPool.GetPendingTransactionsCount().Should().Be(8); // nonces 0-7 - 8 was removed because of not enough balance _txPool.GetPendingTransactions().Last().Nonce.Should().Be(7); _txPool.GetPendingTransactions().Should().BeEquivalentTo(transactions.SkipLast(2)); } @@ -641,7 +635,7 @@ public void should_discard_tx_because_of_overflow_of_cumulative_cost_of_this_tx_ } transactions[2].GasPrice = 5; - _txPool.GetPendingTransactions().Length.Should().Be(2); + _txPool.GetPendingTransactionsCount().Should().Be(2); _txPool.SubmitTx(transactions[2], TxHandlingOptions.PersistentBroadcast).Should().Be(AcceptTxResult.Int256Overflow); } @@ -750,7 +744,7 @@ public void should_calculate_gasBottleneck_properly() } [Test] - public async Task should_remove_GasBottleneck_of_old_nonces() + public async Task should_remove_txs_with_old_nonces_when_updating_GasBottleneck() { _txPool = CreatePool(); Transaction[] transactions = new Transaction[5]; @@ -765,7 +759,7 @@ public async Task should_remove_GasBottleneck_of_old_nonces() .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; _txPool.SubmitTx(transactions[i], TxHandlingOptions.PersistentBroadcast); } - _txPool.GetPendingTransactions().Length.Should().Be(5); + _txPool.GetPendingTransactionsCount().Should().Be(5); for (int i = 0; i < 3; i++) { @@ -773,6 +767,7 @@ public async Task should_remove_GasBottleneck_of_old_nonces() } await RaiseBlockAddedToMainAndWaitForTransactions(5); + _txPool.GetPendingTransactionsCount().Should().Be(2); _txPool.GetPendingTransactions().Count(t => t.GasBottleneck == 0).Should().Be(0); _txPool.GetPendingTransactions().Max(t => t.GasBottleneck).Should().Be((UInt256)5); } @@ -858,7 +853,7 @@ public void broadcaster_should_work_well_when_there_are_no_txs_in_persistent_txs EnsureSenderBalance(transactionB); _txPool.SubmitTx(transactionB, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(2); + _txPool.GetPendingTransactionsCount().Should().Be(2); _txPool.GetOwnPendingTransactions().Length.Should().Be(1); Block block = Build.A.Block.WithTransactions(transactionA).TestObject; @@ -869,7 +864,7 @@ public void broadcaster_should_work_well_when_there_are_no_txs_in_persistent_txs _blockTree.BlockAddedToMain += Raise.EventWith(new object(), blockReplacementEventArgs); manualResetEvent.WaitOne(TimeSpan.FromMilliseconds(200)); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); _txPool.GetOwnPendingTransactions().Length.Should().Be(1); } @@ -891,7 +886,7 @@ public async Task should_remove_transactions_concurrently() var secondTask = Task.Run(() => DeleteTransactionsFromPool(transactionsForSecondTask)); var thirdTask = Task.Run(() => DeleteTransactionsFromPool(transactionsForThirdTask)); await Task.WhenAll(firstTask, secondTask, thirdTask); - _txPool.GetPendingTransactions().Should().HaveCount(transactionsPerPeer * 7); + _txPool.GetPendingTransactionsCount().Should().Be(transactionsPerPeer * 7); } } @@ -927,7 +922,7 @@ public void should_add_pending_transactions(bool sameTransactionSenderPerPeer, b { _txPool = CreatePool(); AddTransactionsToPool(sameTransactionSenderPerPeer, sameNoncePerPeer); - _txPool.GetPendingTransactions().Length.Should().Be(expectedTransactions); + _txPool.GetPendingTransactionsCount().Should().Be(expectedTransactions); } [TestCase(true, true, 10)] @@ -939,7 +934,7 @@ public void should_remove_tx_from_txPool_when_included_in_block(bool sameTransac _txPool = CreatePool(); AddTransactionsToPool(sameTransactionSenderPerPeer, sameNoncePerPeer); - _txPool.GetPendingTransactions().Length.Should().Be(expectedTransactions); + _txPool.GetPendingTransactionsCount().Should().Be(expectedTransactions); Transaction[] transactions = _txPool.GetPendingTransactions(); Block block = Build.A.Block.WithTransactions(transactions).TestObject; @@ -950,7 +945,7 @@ public void should_remove_tx_from_txPool_when_included_in_block(bool sameTransac _blockTree.BlockAddedToMain += Raise.EventWith(new object(), blockReplacementEventArgs); manualResetEvent.WaitOne(TimeSpan.FromMilliseconds(200)); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); } [TestCase(true, true, 10)] @@ -962,7 +957,7 @@ public void should_not_remove_txHash_from_hashCache_when_tx_removed_because_of_i _txPool = CreatePool(); AddTransactionsToPool(sameTransactionSenderPerPeer, sameNoncePerPeer); - _txPool.GetPendingTransactions().Length.Should().Be(expectedTransactions); + _txPool.GetPendingTransactionsCount().Should().Be(expectedTransactions); Transaction[] transactions = _txPool.GetPendingTransactions(); Block block = Build.A.Block.WithTransactions(transactions).TestObject; @@ -990,25 +985,17 @@ public void should_delete_pending_transactions() } [Test] - public void should_return_feeTooLowTooCompete_result_when_trying_to_send_transaction_with_same_nonce_for_same_address() + public void should_return_ReplacementNotAllowed_when_trying_to_send_transaction_with_same_nonce_and_same_fee_for_same_address() { _txPool = CreatePool(); var result1 = _txPool.SubmitTx(GetTransaction(TestItem.PrivateKeyA, TestItem.AddressA), TxHandlingOptions.PersistentBroadcast | TxHandlingOptions.ManagedNonce); result1.Should().Be(AcceptTxResult.Accepted); _txPool.GetOwnPendingTransactions().Length.Should().Be(1); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); var result2 = _txPool.SubmitTx(GetTransaction(TestItem.PrivateKeyA, TestItem.AddressB), TxHandlingOptions.PersistentBroadcast | TxHandlingOptions.ManagedNonce); - result2.Should().Be(AcceptTxResult.FeeTooLowToCompete); + result2.Should().Be(AcceptTxResult.ReplacementNotAllowed); _txPool.GetOwnPendingTransactions().Length.Should().Be(1); - _txPool.GetPendingTransactions().Length.Should().Be(1); - } - - [Test] - public void Should_not_try_to_load_transactions_from_storage() - { - var transaction = Build.A.Transaction.SignedAndResolved().TestObject; - _txPool = CreatePool(); - _txPool.TryGetPendingTransaction(transaction.Hash, out var retrievedTransaction).Should().BeFalse(); + _txPool.GetPendingTransactionsCount().Should().Be(1); } [Test] @@ -1082,7 +1069,7 @@ public async Task should_notify_peer_only_once() txPoolPeer.Id.Returns(TestItem.PublicKeyA); _txPool.AddPeer(txPoolPeer); Transaction tx = AddTransactionToPool(); - await Task.Delay(500); + await Task.Delay(1000); txPoolPeer.Received(1).SendNewTransactions(Arg.Any>(), false); } @@ -1123,7 +1110,7 @@ public void should_accept_access_list_transactions_only_when_eip2930_enabled([Va .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; EnsureSenderBalance(tx); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(eip2930Enabled ? 1 : 0); + _txPool.GetPendingTransactionsCount().Should().Be(eip2930Enabled ? 1 : 0); result.Should().Be(eip2930Enabled ? AcceptTxResult.Accepted : AcceptTxResult.Invalid); } @@ -1138,7 +1125,7 @@ public void When_MaxFeePerGas_is_lower_than_MaxPriorityFeePerGas_tx_is_invalid() .TestObject; EnsureSenderBalance(tx); AcceptTxResult result = _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); result.Should().Be(AcceptTxResult.Invalid); } @@ -1154,7 +1141,7 @@ public void should_accept_zero_MaxFeePerGas_and_zero_MaxPriorityFee_1559_tx() .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); } [Test] @@ -1169,7 +1156,7 @@ public void should_reject_zero_MaxFeePerGas_and_positive_MaxPriorityFee_1559_tx( .SignedAndResolved(_ethereumEcdsa, TestItem.PrivateKeyA).TestObject; EnsureSenderBalance(TestItem.AddressA, UInt256.MaxValue); _txPool.SubmitTx(tx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(0); + _txPool.GetPendingTransactionsCount().Should().Be(0); } [Test] @@ -1220,7 +1207,7 @@ public void should_replace_tx_with_same_sender_and_nonce_only_if_new_fee_is_at_l _txPool.SubmitTx(oldTx, TxHandlingOptions.PersistentBroadcast); _txPool.SubmitTx(newTx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); _txPool.GetPendingTransactions().First().Should().BeEquivalentTo(replaced ? newTx : oldTx); } @@ -1260,7 +1247,7 @@ public void should_replace_1559tx_with_same_sender_and_nonce_only_if_both_new_ma _txPool.SubmitTx(oldTx, TxHandlingOptions.PersistentBroadcast); _txPool.SubmitTx(newTx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); _txPool.GetPendingTransactions().First().Should().BeEquivalentTo(replaced ? newTx : oldTx); } @@ -1288,7 +1275,7 @@ public void should_always_replace_zero_fee_tx(int newGasPrice) _txPool.SubmitTx(oldTx, TxHandlingOptions.PersistentBroadcast); _txPool.SubmitTx(newTx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); _txPool.GetPendingTransactions().First().Should().BeEquivalentTo(newTx); } @@ -1318,7 +1305,7 @@ public void should_always_replace_zero_fee_tx_1559(int newMaxFeePerGas) _txPool.SubmitTx(oldTx, TxHandlingOptions.PersistentBroadcast); _txPool.SubmitTx(newTx, TxHandlingOptions.PersistentBroadcast); - _txPool.GetPendingTransactions().Length.Should().Be(1); + _txPool.GetPendingTransactionsCount().Should().Be(1); _txPool.GetPendingTransactions().First().Should().BeEquivalentTo(newTx); } @@ -1649,22 +1636,6 @@ public void Should_not_add_underpaid_tx_even_if_lower_nonces_are_expensive(int g result.Should().Be(expectedResult ? AcceptTxResult.Accepted : AcceptTxResult.FeeTooLowToCompete); } - [TestCase(0, 97)] - [TestCase(1, 131320)] - [TestCase(2, 262530)] - [TestCase(3, 393737)] - [TestCase(4, 524944)] - [TestCase(5, 656152)] - [TestCase(6, 787361)] - public void should_calculate_size_of_blob_tx_correctly(int numberOfBlobs, int expectedLength) - { - Transaction blobTx = Build.A.Transaction - .WithShardBlobTxTypeAndFields(numberOfBlobs) - .SignedAndResolved() - .TestObject; - blobTx.GetLength().Should().Be(expectedLength); - } - private IDictionary GetPeers(int limit = 100) { var peers = new Dictionary(); @@ -1684,17 +1655,20 @@ private TxPool CreatePool( ISpecProvider specProvider = null, ChainHeadInfoProvider chainHeadInfoProvider = null, IIncomingTxFilter incomingTxFilter = null, + ITxStorage txStorage = null, bool thereIsPriorityContract = false) { specProvider ??= MainnetSpecProvider.Instance; ITransactionComparerProvider transactionComparerProvider = new TransactionComparerProvider(specProvider, _blockTree); + txStorage ??= new BlobTxStorage(); _headInfo = chainHeadInfoProvider; _headInfo ??= new ChainHeadInfoProvider(specProvider, _blockTree, _stateProvider); return new TxPool( _ethereumEcdsa, + txStorage, _headInfo, config ?? new TxPoolConfig() { GasLimit = _txGasLimit }, new TxValidator(_specProvider.ChainId), @@ -1713,6 +1687,22 @@ private ITxPoolPeer GetPeer(PublicKey publicKey) return peer; } + private static ISpecProvider GetLondonSpecProvider() + { + var specProvider = Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(London.Instance); + specProvider.GetSpec(Arg.Any()).Returns(London.Instance); + return specProvider; + } + + private static ISpecProvider GetCancunSpecProvider() + { + var specProvider = Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(Cancun.Instance); + specProvider.GetSpec(Arg.Any()).Returns(Cancun.Instance); + return specProvider; + } + private Transaction[] AddTransactionsToPool(bool sameTransactionSenderPerPeer = true, bool sameNoncePerPeer = false, int transactionsPerPeer = 10) { var transactions = GetTransactions(GetPeers(transactionsPerPeer), sameTransactionSenderPerPeer, sameNoncePerPeer); diff --git a/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs b/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs index c55d49168da..f8a0033dfe7 100644 --- a/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs +++ b/src/Nethermind/Nethermind.TxPool/AcceptTxResult.cs @@ -66,16 +66,29 @@ namespace Nethermind.TxPool public static readonly AcceptTxResult OldNonce = new(10, nameof(OldNonce)); /// - /// A transaction with same nonce has been signed locally already and is awaiting in the pool. - /// (I would like to change this behaviour to allow local replacement) + /// Transaction is not allowed to replace the one already in the pool. Fee bump is too low or some requirements are not fulfilled /// - public static readonly AcceptTxResult OwnNonceAlreadyUsed = new(11, nameof(OwnNonceAlreadyUsed)); + public static readonly AcceptTxResult ReplacementNotAllowed = new(11, nameof(ReplacementNotAllowed)); /// /// Transaction sender has code hash that is not null. /// public static readonly AcceptTxResult SenderIsContract = new(12, nameof(SenderIsContract)); + /// + /// The nonce is too far in the future. + /// + public static readonly AcceptTxResult NonceTooFarInFuture = new(13, nameof(NonceTooFarInFuture)); + + /// + /// Ignores blob transactions if sender already have pending transactions of other types; ignore other types if has already pending blobs + /// + public static readonly AcceptTxResult PendingTxsOfConflictingType = new(14, nameof(PendingTxsOfConflictingType)); + + /// + /// Ignores transactions if tx type is not supported + /// + public static readonly AcceptTxResult NotSupportedTxType = new(15, nameof(NotSupportedTxType)); private int Id { get; } private string Code { get; } diff --git a/src/Nethermind/Nethermind.TxPool/BlobTxStorage.cs b/src/Nethermind/Nethermind.TxPool/BlobTxStorage.cs new file mode 100644 index 00000000000..803bb8bccab --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/BlobTxStorage.cs @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using DotNetty.Buffers; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Db; +using Nethermind.Int256; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.TxPool; + +public class BlobTxStorage : ITxStorage +{ + private readonly IDb _fullBlobTxsDb; + private readonly IDb _lightBlobTxsDb; + private static readonly TxDecoder _txDecoder = new(); + private static readonly LightTxDecoder _lightTxDecoder = new(); + + public BlobTxStorage() + { + _fullBlobTxsDb = new MemDb(); + _lightBlobTxsDb = new MemDb(); + } + + public BlobTxStorage(IColumnsDb database) + { + _fullBlobTxsDb = database.GetColumnDb(BlobTxsColumns.FullBlobTxs); + _lightBlobTxsDb = database.GetColumnDb(BlobTxsColumns.LightBlobTxs); + } + + public bool TryGet(in ValueKeccak hash, Address sender, in UInt256 timestamp, [NotNullWhen(true)] out Transaction? transaction) + { + Span txHashPrefixed = stackalloc byte[64]; + GetHashPrefixedByTimestamp(timestamp, hash, txHashPrefixed); + + byte[]? txBytes = _fullBlobTxsDb.Get(txHashPrefixed); + return TryDecodeFullTx(txBytes, sender, out transaction); + } + + public IEnumerable GetAll() + { + foreach (byte[] txBytes in _lightBlobTxsDb.GetAllValues()) + { + if (TryDecodeLightTx(txBytes, out LightTransaction? transaction)) + { + yield return transaction!; + } + } + } + + public void Add(Transaction transaction) + { + if (transaction?.Hash is null) + { + throw new ArgumentNullException(nameof(transaction)); + } + + Span txHashPrefixed = stackalloc byte[64]; + GetHashPrefixedByTimestamp(transaction.Timestamp, transaction.Hash, txHashPrefixed); + + int length = _txDecoder.GetLength(transaction, RlpBehaviors.InMempoolForm); + IByteBuffer byteBuffer = PooledByteBufferAllocator.Default.Buffer(length); + using NettyRlpStream rlpStream = new(byteBuffer); + rlpStream.Encode(transaction, RlpBehaviors.InMempoolForm); + + _fullBlobTxsDb.Set(txHashPrefixed, byteBuffer.AsSpan()); + _lightBlobTxsDb.Set(transaction.Hash, _lightTxDecoder.Encode(transaction)); + } + + public void Delete(in ValueKeccak hash, in UInt256 timestamp) + { + Span txHashPrefixed = stackalloc byte[64]; + GetHashPrefixedByTimestamp(timestamp, hash, txHashPrefixed); + + _fullBlobTxsDb.Remove(txHashPrefixed); + _lightBlobTxsDb.Remove(hash.BytesAsSpan); + } + + private static bool TryDecodeFullTx(byte[]? txBytes, Address sender, out Transaction? transaction) + { + if (txBytes is not null) + { + RlpStream rlpStream = new(txBytes); + transaction = Rlp.Decode(rlpStream, RlpBehaviors.InMempoolForm); + transaction.SenderAddress = sender; + return true; + } + + transaction = default; + return false; + } + + private static bool TryDecodeLightTx(byte[]? txBytes, out LightTransaction? lightTx) + { + if (txBytes is not null) + { + lightTx = _lightTxDecoder.Decode(txBytes); + return true; + } + + lightTx = default; + return false; + } + + private void GetHashPrefixedByTimestamp(UInt256 timestamp, ValueKeccak hash, Span txHashPrefixed) + { + timestamp.WriteBigEndian(txHashPrefixed); + hash.Bytes.CopyTo(txHashPrefixed[32..]); + } +} + +internal static class UInt256Extensions +{ + public static void WriteBigEndian(in this UInt256 value, Span output) + { + BinaryPrimitives.WriteUInt64BigEndian(output.Slice(0, 8), value.u3); + BinaryPrimitives.WriteUInt64BigEndian(output.Slice(8, 8), value.u2); + BinaryPrimitives.WriteUInt64BigEndian(output.Slice(16, 8), value.u1); + BinaryPrimitives.WriteUInt64BigEndian(output.Slice(24, 8), value.u0); + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Collections/BlobTxDistinctSortedPool.cs b/src/Nethermind/Nethermind.TxPool/Collections/BlobTxDistinctSortedPool.cs new file mode 100644 index 00000000000..a9c35680076 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Collections/BlobTxDistinctSortedPool.cs @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Logging; + +namespace Nethermind.TxPool.Collections; + +public class BlobTxDistinctSortedPool : TxDistinctSortedPool +{ + private readonly ILogger _logger; + + public BlobTxDistinctSortedPool(int capacity, IComparer comparer, ILogManager logManager) + : base(capacity, comparer, logManager) + { + _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); + } + + protected override IComparer GetReplacementComparer(IComparer comparer) + => comparer.GetBlobReplacementComparer(); + + public override void VerifyCapacity() + { + if (Count > _poolCapacity && _logger.IsWarn) + _logger.Warn($"Blob pool exceeds the config size {Count}/{_poolCapacity}"); + } + + /// + /// For tests only - to test sorting + /// + internal void TryGetBlobTxSortingEquivalent(Keccak hash, out Transaction? lightBlobTx) + => base.TryGetValue(hash, out lightBlobTx); +} diff --git a/src/Nethermind/Nethermind.TxPool/Collections/DistinctValueSortedPool.cs b/src/Nethermind/Nethermind.TxPool/Collections/DistinctValueSortedPool.cs index fd550cf972b..e6071d4947f 100644 --- a/src/Nethermind/Nethermind.TxPool/Collections/DistinctValueSortedPool.cs +++ b/src/Nethermind/Nethermind.TxPool/Collections/DistinctValueSortedPool.cs @@ -9,7 +9,7 @@ namespace Nethermind.TxPool.Collections { /// /// Keeps a distinct pool of with in groups based on . - /// Uses separate comparator to distinct between elements. If there is duplicate element added it uses ordering comparator and keeps the one that is larger. + /// Uses separate comparator to distinct between elements. If there is duplicate element added it uses ordering comparator and keeps the one that is larger. /// /// Type of keys of items, unique in pool. /// Type of items that are kept. diff --git a/src/Nethermind/Nethermind.TxPool/Collections/PersistentBlobTxDistinctSortedPool.cs b/src/Nethermind/Nethermind.TxPool/Collections/PersistentBlobTxDistinctSortedPool.cs new file mode 100644 index 00000000000..419e8a3e68d --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Collections/PersistentBlobTxDistinctSortedPool.cs @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Nethermind.Core; +using Nethermind.Core.Caching; +using Nethermind.Core.Crypto; +using Nethermind.Logging; + +namespace Nethermind.TxPool.Collections; + +public class PersistentBlobTxDistinctSortedPool : BlobTxDistinctSortedPool +{ + private readonly ITxStorage _blobTxStorage; + private readonly LruCache _blobTxCache; + private readonly ILogger _logger; + + public PersistentBlobTxDistinctSortedPool(ITxStorage blobTxStorage, ITxPoolConfig txPoolConfig, IComparer comparer, ILogManager logManager) + : base(txPoolConfig.PersistentBlobStorageSize, comparer, logManager) + { + _blobTxStorage = blobTxStorage ?? throw new ArgumentNullException(nameof(blobTxStorage)); + _blobTxCache = new(txPoolConfig.BlobCacheSize, txPoolConfig.BlobCacheSize, "blob txs cache"); + _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); + + RecreateLightTxCollectionAndCache(blobTxStorage); + } + + private void RecreateLightTxCollectionAndCache(ITxStorage blobTxStorage) + { + if (_logger.IsDebug) _logger.Debug("Recreating light collection of blob transactions and cache"); + int numberOfTxsInDb = 0; + int numberOfBlobsInDb = 0; + Stopwatch stopwatch = Stopwatch.StartNew(); + foreach (LightTransaction lightBlobTx in blobTxStorage.GetAll()) + { + if (base.TryInsert(lightBlobTx.Hash, lightBlobTx, out _)) + { + numberOfTxsInDb++; + numberOfBlobsInDb += lightBlobTx.BlobVersionedHashes?.Length ?? 0; + } + } + + if (_logger.IsInfo && numberOfTxsInDb != 0) + { + long loadingTime = stopwatch.ElapsedMilliseconds; + _logger.Info($"Loaded {numberOfTxsInDb} blob txs from persistent db, containing {numberOfBlobsInDb} blobs, in {loadingTime}ms"); + } + stopwatch.Stop(); + } + + public override bool TryInsert(ValueKeccak hash, Transaction fullBlobTx, out Transaction? removed) + { + if (base.TryInsert(fullBlobTx.Hash, new LightTransaction(fullBlobTx), out removed)) + { + _blobTxCache.Set(fullBlobTx.Hash, fullBlobTx); + _blobTxStorage.Add(fullBlobTx); + return true; + } + + return false; + } + + public override bool TryGetValue(ValueKeccak hash, [NotNullWhen(true)] out Transaction? fullBlobTx) + { + // Firstly check if tx is present in in-memory collection of light blob txs (without actual blobs). + // If not, just return false + if (base.TryGetValue(hash, out Transaction? lightTx)) + { + // tx is present in light collection. Try to get full blob tx from cache + if (_blobTxCache.TryGet(hash, out fullBlobTx)) + { + return true; + } + + // tx is present, but not cached, at this point we need to load it from db... + if (_blobTxStorage.TryGet(hash, lightTx.SenderAddress!, lightTx.Timestamp, out fullBlobTx)) + { + // ...and we are saving recently used blob tx to cache + _blobTxCache.Set(hash, fullBlobTx); + return true; + } + } + + fullBlobTx = default; + return false; + } + + protected override bool Remove(ValueKeccak hash, Transaction tx) + { + _blobTxCache.Delete(hash); + _blobTxStorage.Delete(hash, tx.Timestamp); + return base.Remove(hash, tx); + } + + public override void VerifyCapacity() + { + base.VerifyCapacity(); + + if (_logger.IsDebug && Count == _poolCapacity) + _logger.Debug($"Blob persistent storage has reached max size of {_poolCapacity}, blob txs can be evicted now"); + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs b/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs index 3b307b5b3ba..578b29700b4 100644 --- a/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs +++ b/src/Nethermind/Nethermind.TxPool/Collections/SortedPool.cs @@ -173,7 +173,7 @@ protected void UpdateWorstValue() => /// Bucket for same sender transactions. /// If element was removed. False if element was not present in pool. [MethodImpl(MethodImplOptions.Synchronized)] - public bool TryRemove(TKey key, out TValue? value, [NotNullWhen(true)] out ICollection? bucket) => + private bool TryRemove(TKey key, out TValue? value, [NotNullWhen(true)] out ICollection? bucket) => TryRemove(key, false, out value, out bucket); private bool TryRemove(TKey key, bool evicted, [NotNullWhen(true)] out TValue? value, out ICollection? bucket) @@ -255,6 +255,17 @@ public IEnumerable TakeWhile(TGroupKey groupKey, Predicate where return Enumerable.Empty(); } + /// + /// Checks if element is present. + /// + /// Key to check presence. + /// True if element is present in pool. + [MethodImpl(MethodImplOptions.Synchronized)] + public bool ContainsKey(TKey key) + { + return _cacheMap.ContainsKey(key); + } + /// /// Tries to get element. /// @@ -262,7 +273,7 @@ public IEnumerable TakeWhile(TGroupKey groupKey, Predicate where /// Returned element or null. /// If element retrieval succeeded. True if element was present in pool. [MethodImpl(MethodImplOptions.Synchronized)] - public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) + public virtual bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) { return _cacheMap.TryGetValue(key, out value) && value != null; } @@ -275,7 +286,7 @@ public bool TryGetValue(TKey key, [NotNullWhen(true)] out TValue? value) /// Element removed because of exceeding capacity /// If element was inserted. False if element was already present in pool. [MethodImpl(MethodImplOptions.Synchronized)] - public bool TryInsert(TKey key, TValue value, out TValue? removed) + public virtual bool TryInsert(TKey key, TValue value, out TValue? removed) { if (CanInsert(key, value)) { @@ -390,6 +401,9 @@ protected virtual bool Remove(TKey key, TValue value) private void UpdateIsFull() => _isFull = _cacheMap.Count >= _capacity; + [MethodImpl(MethodImplOptions.Synchronized)] + public bool ContainsBucket(TGroupKey groupKey) => + _buckets.ContainsKey(groupKey); [MethodImpl(MethodImplOptions.Synchronized)] public bool TryGetBucket(TGroupKey groupKey, out TValue[] items) diff --git a/src/Nethermind/Nethermind.TxPool/Collections/TxDistinctSortedPool.cs b/src/Nethermind/Nethermind.TxPool/Collections/TxDistinctSortedPool.cs index 9326ceb3ec4..0a10db31551 100644 --- a/src/Nethermind/Nethermind.TxPool/Collections/TxDistinctSortedPool.cs +++ b/src/Nethermind/Nethermind.TxPool/Collections/TxDistinctSortedPool.cs @@ -19,10 +19,15 @@ namespace Nethermind.TxPool.Collections public class TxDistinctSortedPool : DistinctValueSortedPool { private readonly List _transactionsToRemove = new(); + protected int _poolCapacity; + private readonly ILogger _logger; + public TxDistinctSortedPool(int capacity, IComparer comparer, ILogManager logManager) : base(capacity, comparer, CompetingTransactionEqualityComparer.Instance, logManager) { + _poolCapacity = capacity; + _logger = logManager?.GetClassLogger() ?? throw new ArgumentNullException(nameof(logManager)); } protected override IComparer GetUniqueComparer(IComparer comparer) => comparer.GetPoolUniqueTxComparer(); @@ -124,5 +129,11 @@ public void UpdateGroup(Address groupKey, Account groupValue, Func _poolCapacity && _logger.IsWarn) + _logger.Warn($"TxPool exceeds the config size {Count}/{_poolCapacity}"); + } } } diff --git a/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs b/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs index 93efff0b187..3a5c7111ad1 100644 --- a/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs +++ b/src/Nethermind/Nethermind.TxPool/Collections/TxSortedPoolExtensions.cs @@ -20,6 +20,9 @@ public static IComparer GetPoolUniqueTxComparerByNonce(this ICompar public static IComparer GetReplacementComparer(this IComparer comparer) => CompareReplacedTxByFee.Instance.ThenBy(comparer); + public static IComparer GetBlobReplacementComparer(this IComparer comparer) + => CompareReplacedBlobTx.Instance.ThenBy(comparer); + public static Address? MapTxToGroup(this Transaction value) => value.SenderAddress; } } diff --git a/src/Nethermind/Nethermind.TxPool/Comparison/CompareReplacedBlobTx.cs b/src/Nethermind/Nethermind.TxPool/Comparison/CompareReplacedBlobTx.cs new file mode 100644 index 00000000000..122741ed1f2 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Comparison/CompareReplacedBlobTx.cs @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Nethermind.Core; + +namespace Nethermind.TxPool.Comparison; + +/// +/// Compare fee of newcomer blob transaction with fee of transaction intended to be replaced +/// +public class CompareReplacedBlobTx : IComparer +{ + public static readonly CompareReplacedBlobTx Instance = new(); + + private CompareReplacedBlobTx() { } + + // To replace old blob transaction, new transaction needs to have fee at least 2x higher than current fee. + // 2x higher must be MaxPriorityFeePerGas, MaxFeePerGas and MaxFeePerDataGas + public int Compare(Transaction? newTx, Transaction? oldTx) + { + if (ReferenceEquals(newTx, oldTx)) return TxComparisonResult.NotDecided; + if (ReferenceEquals(null, oldTx)) return TxComparisonResult.KeepOld; + if (ReferenceEquals(null, newTx)) return TxComparisonResult.TakeNew; + + // do not allow to replace blob tx by the one with lower number of blobs + if (oldTx.BlobVersionedHashes is null || newTx.BlobVersionedHashes is null) return TxComparisonResult.KeepOld; + if (oldTx.BlobVersionedHashes.Length > newTx.BlobVersionedHashes.Length) return TxComparisonResult.KeepOld; + + if (oldTx.MaxFeePerGas * 2 > newTx.MaxFeePerGas) return TxComparisonResult.KeepOld; + if (oldTx.MaxPriorityFeePerGas * 2 > newTx.MaxPriorityFeePerGas) return TxComparisonResult.KeepOld; + if (oldTx.MaxFeePerBlobGas * 2 > newTx.MaxFeePerBlobGas) return TxComparisonResult.KeepOld; + + // if we are here, it means that all new fees are at least 2x higher than old ones, so replacement is allowed + return TxComparisonResult.TakeNew; + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Filters/BalanceTooLowFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/BalanceTooLowFilter.cs index 1266c8c9af4..dc8e0925519 100644 --- a/src/Nethermind/Nethermind.TxPool/Filters/BalanceTooLowFilter.cs +++ b/src/Nethermind/Nethermind.TxPool/Filters/BalanceTooLowFilter.cs @@ -14,11 +14,13 @@ namespace Nethermind.TxPool.Filters internal sealed class BalanceTooLowFilter : IIncomingTxFilter { private readonly TxDistinctSortedPool _txs; + private readonly TxDistinctSortedPool _blobTxs; private readonly ILogger _logger; - public BalanceTooLowFilter(TxDistinctSortedPool txs, ILogger logger) + public BalanceTooLowFilter(TxDistinctSortedPool txs, TxDistinctSortedPool blobTxs, ILogger logger) { _txs = txs; + _blobTxs = blobTxs; _logger = logger; } @@ -34,11 +36,14 @@ public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingO UInt256 cumulativeCost = UInt256.Zero; bool overflow = false; - Transaction[] transactions = _txs.GetBucketSnapshot(tx.SenderAddress!); // since unknownSenderFilter will run before this one + Transaction[] sameTypeTxs = tx.SupportsBlobs + ? _blobTxs.GetBucketSnapshot(tx.SenderAddress!) // it will create a snapshot of light txs (without actual blobs) + : _txs.GetBucketSnapshot(tx.SenderAddress!); + // tx.SenderAddress! as unknownSenderFilter will run before this one - for (int i = 0; i < transactions.Length; i++) + for (int i = 0; i < sameTypeTxs.Length; i++) { - Transaction otherTx = transactions[i]; + Transaction otherTx = sameTypeTxs[i]; if (otherTx.Nonce < account.Nonce) { continue; diff --git a/src/Nethermind/Nethermind.TxPool/Filters/BalanceZeroFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/BalanceZeroFilter.cs index b69eaa41a47..30d457dd049 100644 --- a/src/Nethermind/Nethermind.TxPool/Filters/BalanceZeroFilter.cs +++ b/src/Nethermind/Nethermind.TxPool/Filters/BalanceZeroFilter.cs @@ -43,8 +43,7 @@ public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingO AcceptTxResult.InsufficientFunds.WithMessage($"Balance is {balance} less than sending value {tx.Value}"); } - if (UInt256.MultiplyOverflow(tx.MaxFeePerGas, (UInt256)tx.GasLimit, out UInt256 txCostAndValue) || - UInt256.AddOverflow(txCostAndValue, tx.Value, out txCostAndValue)) + if (tx.IsOverflowInTxCostAndValue(out UInt256 txCostAndValue)) { Metrics.PendingTransactionsBalanceBelowValue++; if (_logger.IsTrace) diff --git a/src/Nethermind/Nethermind.TxPool/Filters/FeeTooLowFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/FeeTooLowFilter.cs index c7f0b4bbc9f..432930567b1 100644 --- a/src/Nethermind/Nethermind.TxPool/Filters/FeeTooLowFilter.cs +++ b/src/Nethermind/Nethermind.TxPool/Filters/FeeTooLowFilter.cs @@ -11,21 +11,23 @@ namespace Nethermind.TxPool.Filters { /// - /// Filters out transactions where gas fee properties were set too low or where the sender has not enough balance. + /// Filters out transactions where gas fee properties were set too low. /// internal sealed class FeeTooLowFilter : IIncomingTxFilter { private readonly IChainHeadSpecProvider _specProvider; private readonly IChainHeadInfoProvider _headInfo; private readonly TxDistinctSortedPool _txs; + private readonly TxDistinctSortedPool _blobTxs; private readonly bool _thereIsPriorityContract; private readonly ILogger _logger; - public FeeTooLowFilter(IChainHeadInfoProvider headInfo, TxDistinctSortedPool txs, bool thereIsPriorityContract, ILogger logger) + public FeeTooLowFilter(IChainHeadInfoProvider headInfo, TxDistinctSortedPool txs, TxDistinctSortedPool blobTxs, bool thereIsPriorityContract, ILogger logger) { _specProvider = headInfo.SpecProvider; _headInfo = headInfo; _txs = txs; + _blobTxs = blobTxs; _thereIsPriorityContract = thereIsPriorityContract; _logger = logger; } @@ -51,7 +53,8 @@ public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingO AcceptTxResult.FeeTooLow.WithMessage("Affordable gas price is 0"); } - if (_txs.IsFull() && _txs.TryGetLast(out Transaction? lastTx) + TxDistinctSortedPool relevantPool = (tx.SupportsBlobs ? _blobTxs : _txs); + if (relevantPool.IsFull() && relevantPool.TryGetLast(out Transaction? lastTx) && affordableGasPrice <= lastTx?.GasBottleneck) { Metrics.PendingTransactionsTooLowFee++; diff --git a/src/Nethermind/Nethermind.TxPool/Filters/FutureNonceFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/FutureNonceFilter.cs new file mode 100644 index 00000000000..e3e74abd59b --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Filters/FutureNonceFilter.cs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Int256; + +namespace Nethermind.TxPool.Filters; + +public class FutureNonceFilter : IIncomingTxFilter +{ + private readonly ITxPoolConfig _txPoolConfig; + + public FutureNonceFilter(ITxPoolConfig txPoolConfig) + { + _txPoolConfig = txPoolConfig; + } + + public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingOptions txHandlingOptions) + { + int relevantMaxPendingTxsPerSender = (tx.SupportsBlobs + ? _txPoolConfig.MaxPendingBlobTxsPerSender + : _txPoolConfig.MaxPendingTxsPerSender); + + // MaxPendingTxsPerSender/MaxPendingBlobTxsPerSender equal 0 means no limit + if (relevantMaxPendingTxsPerSender == 0) + { + return AcceptTxResult.Accepted; + } + + UInt256 currentNonce = state.SenderAccount.Nonce; + bool overflow = UInt256.AddOverflow(currentNonce, (UInt256)relevantMaxPendingTxsPerSender, out UInt256 maxAcceptedNonce); + + // Overflow means that gap between current nonce of sender and UInt256.MaxValue is lower than allowed number + // of pending transactions. As lower nonces were rejected earlier, here it means tx accepted. + // So we are rejecting tx only if there is no overflow. + if (tx.Nonce > maxAcceptedNonce && !overflow) + { + Metrics.PendingTransactionsNonceTooFarInFuture++; + return AcceptTxResult.NonceTooFarInFuture; + } + + return AcceptTxResult.Accepted; + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Filters/GapNonceFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/GapNonceFilter.cs index 7c91adf19f8..0acd2ec69d8 100644 --- a/src/Nethermind/Nethermind.TxPool/Filters/GapNonceFilter.cs +++ b/src/Nethermind/Nethermind.TxPool/Filters/GapNonceFilter.cs @@ -15,23 +15,28 @@ namespace Nethermind.TxPool.Filters internal sealed class GapNonceFilter : IIncomingTxFilter { private readonly TxDistinctSortedPool _txs; + private readonly TxDistinctSortedPool _blobTxs; private readonly ILogger _logger; - public GapNonceFilter(TxDistinctSortedPool txs, ILogger logger) + public GapNonceFilter(TxDistinctSortedPool txs, TxDistinctSortedPool blobTxs, ILogger logger) { _txs = txs; + _blobTxs = blobTxs; _logger = logger; } public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingOptions handlingOptions) { bool isLocal = (handlingOptions & TxHandlingOptions.PersistentBroadcast) != 0; - if (isLocal || !_txs.IsFull()) + bool nonceGapsAllowed = isLocal || !_txs.IsFull(); + if (nonceGapsAllowed && !tx.SupportsBlobs) { return AcceptTxResult.Accepted; } - int numberOfSenderTxsInPending = _txs.GetBucketCount(tx.SenderAddress!); // since unknownSenderFilter will run before this one + int numberOfSenderTxsInPending = tx.SupportsBlobs + ? _blobTxs.GetBucketCount(tx.SenderAddress!) + : _txs.GetBucketCount(tx.SenderAddress!); // since unknownSenderFilter will run before this one UInt256 currentNonce = state.SenderAccount.Nonce; long nextNonceInOrder = (long)currentNonce + numberOfSenderTxsInPending; bool isTxNonceNextInOrder = tx.Nonce <= nextNonceInOrder; diff --git a/src/Nethermind/Nethermind.TxPool/Filters/NotSupportedTxFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/NotSupportedTxFilter.cs new file mode 100644 index 00000000000..2f2e264e3d6 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Filters/NotSupportedTxFilter.cs @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Logging; + +namespace Nethermind.TxPool.Filters; + +/// +/// Filters out transactions types that are not supported +/// +internal sealed class NotSupportedTxFilter : IIncomingTxFilter +{ + private readonly ITxPoolConfig _txPoolConfig; + private readonly ILogger _logger; + + public NotSupportedTxFilter(ITxPoolConfig txPoolConfig, ILogger logger) + { + _txPoolConfig = txPoolConfig; + _logger = logger; + } + + public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingOptions txHandlingOptions) + { + if (!_txPoolConfig.BlobSupportEnabled && tx.SupportsBlobs) + { + Metrics.PendingTransactionsNotSupportedTxType++; + if (_logger.IsTrace) _logger.Trace($"Skipped adding transaction {tx.ToString(" ")}, blob transactions are not supported."); + return AcceptTxResult.NotSupportedTxType; + } + + return AcceptTxResult.Accepted; + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Filters/PriorityFeeTooLowFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/PriorityFeeTooLowFilter.cs new file mode 100644 index 00000000000..8026c49808a --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Filters/PriorityFeeTooLowFilter.cs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Diagnostics; +using Nethermind.Core; +using Nethermind.Core.Extensions; +using Nethermind.Int256; +using Nethermind.Logging; + +namespace Nethermind.TxPool.Filters; + +public class PriorityFeeTooLowFilter : IIncomingTxFilter +{ + private readonly ILogger _logger; + private static readonly UInt256 _minBlobsPriorityFee = 1.GWei(); + + public PriorityFeeTooLowFilter(ILogger logger) + { + _logger = logger; + } + + public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingOptions handlingOptions) + { + if (tx.SupportsBlobs && tx.MaxPriorityFeePerGas < _minBlobsPriorityFee) + { + Metrics.PendingTransactionsTooLowPriorityFee++; + if (_logger.IsTrace) _logger.Trace($"Skipped adding transaction {tx.ToString(" ")}, too low priority fee with options {handlingOptions} from {new StackTrace()}"); + return AcceptTxResult.FeeTooLow.WithMessage($"MaxPriorityFeePerGas for blob transaction needs to be at least {_minBlobsPriorityFee} (1 GWei), is {tx.MaxPriorityFeePerGas}."); + } + + return AcceptTxResult.Accepted; + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Filters/TxTypeTxFilter.cs b/src/Nethermind/Nethermind.TxPool/Filters/TxTypeTxFilter.cs new file mode 100644 index 00000000000..be79182e8c7 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/Filters/TxTypeTxFilter.cs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.TxPool.Collections; + +namespace Nethermind.TxPool.Filters; + +/// +/// Ignores blob transactions if sender already have pending transactions of other types; ignore other types if has already pending blobs +/// +public class TxTypeTxFilter : IIncomingTxFilter +{ + private readonly TxDistinctSortedPool _txs; + private readonly TxDistinctSortedPool _blobTxs; + + public TxTypeTxFilter(TxDistinctSortedPool txs, TxDistinctSortedPool blobTxs) + { + _txs = txs; + _blobTxs = blobTxs; + } + + public AcceptTxResult Accept(Transaction tx, TxFilteringState state, TxHandlingOptions txHandlingOptions) + { + TxDistinctSortedPool otherTxTypePool = (tx.SupportsBlobs ? _txs : _blobTxs); + if (otherTxTypePool.ContainsBucket(tx.SenderAddress!)) // as unknownSenderFilter will run before this one + { + Metrics.PendingTransactionsConflictingTxType++; + return AcceptTxResult.PendingTxsOfConflictingType; + } + return AcceptTxResult.Accepted; + } +} diff --git a/src/Nethermind/Nethermind.TxPool/IChainHeadInfoProvider.cs b/src/Nethermind/Nethermind.TxPool/IChainHeadInfoProvider.cs index ce089c74908..dc4de5f6b43 100644 --- a/src/Nethermind/Nethermind.TxPool/IChainHeadInfoProvider.cs +++ b/src/Nethermind/Nethermind.TxPool/IChainHeadInfoProvider.cs @@ -18,6 +18,8 @@ public interface IChainHeadInfoProvider public UInt256 CurrentBaseFee { get; } + public UInt256 CurrentPricePerBlobGas { get; } + event EventHandler HeadChanged; } } diff --git a/src/Nethermind/Nethermind.TxPool/ITxPool.cs b/src/Nethermind/Nethermind.TxPool/ITxPool.cs index 8cd41cec842..91d8d725a1a 100644 --- a/src/Nethermind/Nethermind.TxPool/ITxPool.cs +++ b/src/Nethermind/Nethermind.TxPool/ITxPool.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Int256; @@ -12,14 +13,21 @@ namespace Nethermind.TxPool public interface ITxPool { int GetPendingTransactionsCount(); + int GetPendingBlobTransactionsCount(); Transaction[] GetPendingTransactions(); /// - /// Grouped by sender address, sorted by nonce and later tx pool sorting + /// Non-blob txs grouped by sender address, sorted by nonce and later tx pool sorting /// /// IDictionary GetPendingTransactionsBySender(); + /// + /// Blob txs light equivalences grouped by sender address, sorted by nonce and later tx pool sorting + /// + /// + IDictionary GetPendingLightBlobTransactionsBySender(); + /// /// from a specific sender, sorted by nonce and later tx pool sorting /// @@ -27,10 +35,12 @@ public interface ITxPool Transaction[] GetPendingTransactionsBySender(Address address); void AddPeer(ITxPoolPeer peer); void RemovePeer(PublicKey nodeId); + bool ContainsTx(Keccak hash, TxType txType); AcceptTxResult SubmitTx(Transaction tx, TxHandlingOptions handlingOptions); bool RemoveTransaction(Keccak? hash); bool IsKnown(Keccak hash); bool TryGetPendingTransaction(Keccak hash, out Transaction? transaction); + bool TryGetPendingBlobTransaction(Keccak hash, [NotNullWhen(true)] out Transaction? blobTransaction); UInt256 GetLatestPendingNonce(Address address); event EventHandler NewDiscovered; event EventHandler NewPending; diff --git a/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs b/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs index aec0917217c..9f7d9ef9d0b 100644 --- a/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs +++ b/src/Nethermind/Nethermind.TxPool/ITxPoolConfig.cs @@ -13,6 +13,27 @@ public interface ITxPoolConfig : IConfig [ConfigItem(DefaultValue = "2048", Description = "Max number of transactions held in mempool (more transactions in mempool mean more memory used")] int Size { get; set; } + [ConfigItem(DefaultValue = "false", Description = "If true, blob transactions support will be enabled")] + bool BlobSupportEnabled { get; set; } + + [ConfigItem(DefaultValue = "false", Description = "If true, all blob transactions would be stored in persistent db")] + bool PersistentBlobStorageEnabled { get; set; } + + [ConfigItem(DefaultValue = "16384", Description = "Max number of full blob transactions stored in the database (increasing the number of transactions in the blob pool also results in higher memory usage). Default value use max 13GB (16386*128KB*6blobs), for 1-blob txs it's 2GB (16386*128KB)")] + int PersistentBlobStorageSize { get; set; } + + [ConfigItem(DefaultValue = "256", Description = "Max number of full blob transactions stored in memory as a cache for persistent storage. Default value use max 200MB (256*128KB*6blobs), for 1-blob txs it's 33MB (256*128KB)")] + int BlobCacheSize { get; set; } + + [ConfigItem(DefaultValue = "512", Description = "Max number of full blob transactions stored in memory. Used only if persistent storage is disabled")] + int InMemoryBlobPoolSize { get; set; } + + [ConfigItem(DefaultValue = "0", Description = "Max number of pending transactions per single sender. Set it to 0 to disable the limit.")] + int MaxPendingTxsPerSender { get; set; } + + [ConfigItem(DefaultValue = "16", Description = "Max number of pending blob transactions per single sender. Set it to 0 to disable the limit.")] + int MaxPendingBlobTxsPerSender { get; set; } + [ConfigItem(DefaultValue = "524288", Description = "Max number of cached hashes of already known transactions." + "It is set automatically by the memory hint.")] diff --git a/src/Nethermind/Nethermind.TxPool/ITxStorage.cs b/src/Nethermind/Nethermind.TxPool/ITxStorage.cs new file mode 100644 index 00000000000..9f7c3f170d3 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/ITxStorage.cs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Int256; + +namespace Nethermind.TxPool; + +public interface ITxStorage +{ + bool TryGet(in ValueKeccak hash, Address sender, in UInt256 timestamp, [NotNullWhen(true)] out Transaction? transaction); + IEnumerable GetAll(); + void Add(Transaction transaction); + void Delete(in ValueKeccak hash, in UInt256 timestamp); +} diff --git a/src/Nethermind/Nethermind.TxPool/LightTransaction.cs b/src/Nethermind/Nethermind.TxPool/LightTransaction.cs new file mode 100644 index 00000000000..b8dd4f0e416 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/LightTransaction.cs @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + + +using System.Collections.Generic; +using System.Linq; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Int256; + +namespace Nethermind.TxPool; + +/// +/// For sorting reasons - without storing full, large txs in memory +/// +public class LightTransaction : Transaction +{ + private static readonly Dictionary _blobVersionedHashesCache = + Enumerable.Range(1, Eip4844Constants.MaxBlobsPerBlock).ToDictionary(i => i, i => new byte[i][]); + + + public LightTransaction(Transaction fullTx) + { + Type = TxType.Blob; + Hash = fullTx.Hash; + SenderAddress = fullTx.SenderAddress; + Nonce = fullTx.Nonce; + Value = fullTx.Value; + GasLimit = fullTx.GasLimit; + GasPrice = fullTx.GasPrice; // means MaxPriorityFeePerGas + DecodedMaxFeePerGas = fullTx.DecodedMaxFeePerGas; + MaxFeePerBlobGas = fullTx.MaxFeePerBlobGas; + BlobVersionedHashes = _blobVersionedHashesCache[fullTx.BlobVersionedHashes!.Length]; + GasBottleneck = fullTx.GasBottleneck; + Timestamp = fullTx.Timestamp; + PoolIndex = fullTx.PoolIndex; + _size = fullTx.GetLength(); + } + + public LightTransaction( + UInt256 timestamp, + Address sender, + UInt256 nonce, + Keccak hash, + UInt256 value, + long gasLimit, + UInt256 gasPrice, + UInt256 maxFeePerGas, + UInt256 maxFeePerBlobGas, + byte[][] blobVersionHashes, + ulong poolIndex, + int size) + { + Type = TxType.Blob; + Hash = hash; + SenderAddress = sender; + Nonce = nonce; + Value = value; + GasLimit = gasLimit; + GasPrice = gasPrice; // means MaxPriorityFeePerGas + DecodedMaxFeePerGas = maxFeePerGas; + MaxFeePerBlobGas = maxFeePerBlobGas; + BlobVersionedHashes = blobVersionHashes; + Timestamp = timestamp; + PoolIndex = poolIndex; + _size = size; + } +} diff --git a/src/Nethermind/Nethermind.TxPool/LightTxDecoder.cs b/src/Nethermind/Nethermind.TxPool/LightTxDecoder.cs new file mode 100644 index 00000000000..aa5680d7427 --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/LightTxDecoder.cs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Serialization.Rlp; + +namespace Nethermind.TxPool; + +public class LightTxDecoder : TxDecoder +{ + private int GetLength(Transaction tx) + { + return Rlp.LengthOf(tx.Timestamp) + + Rlp.LengthOf(tx.SenderAddress) + + Rlp.LengthOf(tx.Nonce) + + Rlp.LengthOf(tx.Hash) + + Rlp.LengthOf(tx.Value) + + Rlp.LengthOf(tx.GasLimit) + + Rlp.LengthOf(tx.GasPrice) + + Rlp.LengthOf(tx.DecodedMaxFeePerGas) + + Rlp.LengthOf(tx.MaxFeePerBlobGas!.Value) + + Rlp.LengthOf(tx.BlobVersionedHashes!) + + Rlp.LengthOf(tx.PoolIndex) + + Rlp.LengthOf(tx.GetLength()); + + } + + public byte[] Encode(Transaction tx) + { + RlpStream rlpStream = new(GetLength(tx)); + + rlpStream.Encode(tx.Timestamp); + rlpStream.Encode(tx.SenderAddress); + rlpStream.Encode(tx.Nonce); + rlpStream.Encode(tx.Hash); + rlpStream.Encode(tx.Value); + rlpStream.Encode(tx.GasLimit); + rlpStream.Encode(tx.GasPrice); + rlpStream.Encode(tx.DecodedMaxFeePerGas); + rlpStream.Encode(tx.MaxFeePerBlobGas!.Value); + rlpStream.Encode(tx.BlobVersionedHashes!); + rlpStream.Encode(tx.PoolIndex); + rlpStream.Encode(tx.GetLength()); + + return rlpStream.Data!; + } + + public LightTransaction Decode(byte[] data) + { + RlpStream rlpStream = new(data); + return new LightTransaction( + rlpStream.DecodeUInt256(), + rlpStream.DecodeAddress()!, + rlpStream.DecodeUInt256(), + rlpStream.DecodeKeccak()!, + rlpStream.DecodeUInt256(), + rlpStream.DecodeLong(), + rlpStream.DecodeUInt256(), + rlpStream.DecodeUInt256(), + rlpStream.DecodeUInt256(), + rlpStream.DecodeByteArrays(), + rlpStream.DecodeUlong(), + rlpStream.DecodeInt()); + } +} diff --git a/src/Nethermind/Nethermind.TxPool/Metrics.cs b/src/Nethermind/Nethermind.TxPool/Metrics.cs index 546d095d04d..53721d957fe 100644 --- a/src/Nethermind/Nethermind.TxPool/Metrics.cs +++ b/src/Nethermind/Nethermind.TxPool/Metrics.cs @@ -20,18 +20,29 @@ public static class Metrics [Description("Number of pending transactions received from peers.")] public static long PendingTransactionsReceived { get; set; } + [CounterMetric] + [Description("Number of hashes of pending transactions received from peers.")] + public static long PendingTransactionsHashesReceived { get; set; } + [CounterMetric] [Description("Number of pending transactions received that were ignored.")] public static long PendingTransactionsDiscarded { get; set; } + [CounterMetric] + [Description("Number of pending transactions received that were ignored because of not supported transaction type.")] + public static long PendingTransactionsNotSupportedTxType { get; set; } + [CounterMetric] [Description( "Number of pending transactions received that were ignored because of not having preceding nonce of this sender in TxPool.")] public static long PendingTransactionsNonceGap { get; set; } [CounterMetric] - [Description( - "Number of pending transactions received that were ignored because of fee lower than the lowest fee in transaction pool.")] + [Description("Number of pending transactions received that were ignored because of priority fee lower than minimal requirement.")] + public static long PendingTransactionsTooLowPriorityFee { get; set; } + + [CounterMetric] + [Description("Number of pending transactions received that were ignored because of fee lower than the lowest fee in transaction pool.")] public static long PendingTransactionsTooLowFee { get; set; } [CounterMetric] @@ -60,10 +71,13 @@ public static class Metrics public static long PendingTransactionsGasLimitTooHigh { get; set; } [CounterMetric] - [Description( - "Number of pending transactions received that were ignored after passing early rejections as balance is too low to compete with lowest effective fee in transaction pool.")] + [Description("Number of pending transactions received that were ignored after passing early rejections as balance is too low to compete with lowest effective fee in transaction pool.")] public static long PendingTransactionsPassedFiltersButCannotCompeteOnFees { get; set; } + [CounterMetric] + [Description("Number of pending transactions received that were trying to replace tx with the same sender and nonce and failed.")] + public static long PendingTransactionsPassedFiltersButCannotReplace { get; set; } + [CounterMetric] [Description("Number of pending transactions that reached filters which are resource expensive")] public static long PendingTransactionsWithExpensiveFiltering { get; set; } @@ -80,6 +94,14 @@ public static class Metrics [Description("Number of transactions with already used nonce.")] public static long PendingTransactionsLowNonce { get; set; } + [CounterMetric] + [Description("Number of transactions with nonce too far in future.")] + public static long PendingTransactionsNonceTooFarInFuture { get; set; } + + [CounterMetric] + [Description("Number of transactions rejected because of already pending tx of other type (allowed blob txs or others, not both at once).")] + public static long PendingTransactionsConflictingTxType { get; set; } + [CounterMetric] [Description("Number of pending transactions added to transaction pool.")] public static long PendingTransactionsAdded { get; set; } @@ -88,6 +110,10 @@ public static class Metrics [Description("Number of pending 1559-type transactions added to transaction pool.")] public static long Pending1559TransactionsAdded { get; set; } + [CounterMetric] + [Description("Number of pending blob-type transactions added to transaction pool.")] + public static long PendingBlobTransactionsAdded { get; set; } + [CounterMetric] [Description("Number of pending transactions evicted from transaction pool.")] public static long PendingTransactionsEvicted { get; set; } @@ -96,6 +122,14 @@ public static class Metrics [Description("Ratio of 1559-type transactions in the block.")] public static float Eip1559TransactionsRatio { get; set; } + [GaugeMetric] + [Description("Number of blob transactions in the block.")] + public static long BlobTransactionsInBlock { get; set; } + + [GaugeMetric] + [Description("Number of blobs in the block.")] + public static long BlobsInBlock { get; set; } + [GaugeMetric] [Description("Ratio of transactions in the block absent in hashCache.")] public static float DarkPoolRatioLevel1 { get; set; } @@ -106,6 +140,10 @@ public static class Metrics [GaugeMetric] [Description("Number of transactions in pool.")] - public static float TransactionCount { get; set; } + public static long TransactionCount { get; set; } + + [GaugeMetric] + [Description("Number of blob transactions in pool.")] + public static long BlobTransactionCount { get; set; } } } diff --git a/src/Nethermind/Nethermind.TxPool/NullBlobTxStorage.cs b/src/Nethermind/Nethermind.TxPool/NullBlobTxStorage.cs new file mode 100644 index 00000000000..9d0982b882a --- /dev/null +++ b/src/Nethermind/Nethermind.TxPool/NullBlobTxStorage.cs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Int256; + +namespace Nethermind.TxPool; + +public class NullBlobTxStorage : ITxStorage +{ + public static NullBlobTxStorage Instance { get; } = new(); + + public bool TryGet(in ValueKeccak hash, Address sender, in UInt256 timestamp, [NotNullWhen(true)] out Transaction? transaction) + { + transaction = default; + return false; + } + + public IEnumerable GetAll() => Array.Empty(); + + public void Add(Transaction transaction) { } + + public void Delete(in ValueKeccak hash, in UInt256 timestamp) { } +} diff --git a/src/Nethermind/Nethermind.TxPool/NullTxPool.cs b/src/Nethermind/Nethermind.TxPool/NullTxPool.cs index e1812faf9bb..760f3ea29d6 100644 --- a/src/Nethermind/Nethermind.TxPool/NullTxPool.cs +++ b/src/Nethermind/Nethermind.TxPool/NullTxPool.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using Nethermind.Core; using Nethermind.Core.Crypto; using Nethermind.Int256; @@ -16,19 +17,25 @@ private NullTxPool() { } public static NullTxPool Instance { get; } = new(); public int GetPendingTransactionsCount() => 0; - + public int GetPendingBlobTransactionsCount() => 0; public Transaction[] GetPendingTransactions() => Array.Empty(); - public Transaction[] GetOwnPendingTransactions() => Array.Empty(); - public Transaction[] GetPendingTransactionsBySender(Address address) => Array.Empty(); - public IDictionary GetPendingTransactionsBySender() => new Dictionary(); + public IDictionary GetPendingTransactionsBySender() + => new Dictionary(); + + public IDictionary GetPendingLightBlobTransactionsBySender() + => new Dictionary(); + + public IEnumerable GetPendingBlobTransactions() => Array.Empty(); public void AddPeer(ITxPoolPeer peer) { } public void RemovePeer(PublicKey nodeId) { } + public bool ContainsTx(Keccak hash, TxType txType) => false; + public AcceptTxResult SubmitTx(Transaction tx, TxHandlingOptions txHandlingOptions) => AcceptTxResult.Accepted; public bool RemoveTransaction(Keccak? hash) => false; @@ -41,7 +48,12 @@ public bool TryGetPendingTransaction(Keccak hash, out Transaction? transaction) return false; } - public UInt256 ReserveOwnTransactionNonce(Address address) => UInt256.Zero; + public bool TryGetPendingBlobTransaction(Keccak hash, [NotNullWhen(true)] out Transaction? blobTransaction) + { + blobTransaction = null; + return false; + } + public UInt256 GetLatestPendingNonce(Address address) => 0; diff --git a/src/Nethermind/Nethermind.TxPool/TransactionExtensions.cs b/src/Nethermind/Nethermind.TxPool/TransactionExtensions.cs index b6888c71d0e..9336ae51a56 100644 --- a/src/Nethermind/Nethermind.TxPool/TransactionExtensions.cs +++ b/src/Nethermind/Nethermind.TxPool/TransactionExtensions.cs @@ -21,6 +21,10 @@ public static int GetLength(this Transaction tx) return tx.GetLength(_transactionSizeCalculator); } + public static bool CanPayBaseFee(this Transaction tx, UInt256 currentBaseFee) => tx.MaxFeePerGas >= currentBaseFee; + + public static bool CanPayForBlobGas(this Transaction tx, UInt256 currentPricePerBlobGas) => !tx.SupportsBlobs || tx.MaxFeePerBlobGas >= currentPricePerBlobGas; + public static bool CanBeBroadcast(this Transaction tx) => !tx.SupportsBlobs && tx.GetLength() <= MaxSizeOfTxForBroadcast; internal static UInt256 CalculateGasPrice(this Transaction tx, bool eip1559Enabled, in UInt256 baseFee) @@ -74,7 +78,18 @@ internal static bool IsOverflowWhenAddingTxCostToCumulative(this Transaction tx, overflow |= UInt256.AddOverflow(currentCost, maxTxCost, out cumulativeCost); overflow |= UInt256.AddOverflow(cumulativeCost, tx.Value, out cumulativeCost); + if (tx.SupportsBlobs) + { + // if tx.SupportsBlobs and has BlobVersionedHashes = null, it will throw on earlier step of validation, in TxValidator + overflow |= UInt256.MultiplyOverflow(Eip4844Constants.BlobGasPerBlob, (UInt256)tx.BlobVersionedHashes!.Length, out UInt256 blobGas); + overflow |= UInt256.MultiplyOverflow(blobGas, tx.MaxFeePerBlobGas ?? UInt256.MaxValue, out UInt256 blobGasCost); + overflow |= UInt256.AddOverflow(cumulativeCost, blobGasCost, out cumulativeCost); + } + return overflow; } + + internal static bool IsOverflowInTxCostAndValue(this Transaction tx, out UInt256 txCost) + => IsOverflowWhenAddingTxCostToCumulative(tx, UInt256.Zero, out txCost); } } diff --git a/src/Nethermind/Nethermind.TxPool/TxBroadcaster.cs b/src/Nethermind/Nethermind.TxPool/TxBroadcaster.cs index 7702f0e4ed9..546ff325865 100644 --- a/src/Nethermind/Nethermind.TxPool/TxBroadcaster.cs +++ b/src/Nethermind/Nethermind.TxPool/TxBroadcaster.cs @@ -37,9 +37,9 @@ internal class TxBroadcaster : IDisposable private readonly ConcurrentDictionary _peers = new(); /// - /// Transactions published locally (initiated by this node users) or reorganised. + /// Transactions published locally (initiated by this node users). /// - private readonly SortedPool _persistentTxs; + private readonly TxDistinctSortedPool _persistentTxs; /// /// Transactions added by external peers between timer elapses. @@ -81,6 +81,7 @@ public TxBroadcaster(IComparer comparer, _timer.Start(); } + // only for testing reasons internal Transaction[] GetSnapshot() => _persistentTxs.GetSnapshot(); public void Broadcast(Transaction tx, bool isPersistent) @@ -100,7 +101,7 @@ private void StartBroadcast(Transaction tx) NotifyPeersAboutLocalTx(tx); if (tx.Hash is not null) { - _persistentTxs.TryInsert(tx.Hash, tx); + _persistentTxs.TryInsert(tx.Hash, tx.SupportsBlobs ? new LightTransaction(tx) : tx); } } @@ -112,9 +113,12 @@ private void BroadcastOnce(Transaction tx) } } - public void BroadcastOnce(ITxPoolPeer peer, Transaction[] txs) + public void AnnounceOnce(ITxPoolPeer peer, Transaction[] txs) { - Notify(peer, txs, false); + if (txs.Length > 0) + { + Notify(peer, txs, false); + } } public void BroadcastPersistentTxs() @@ -187,20 +191,26 @@ public void BroadcastPersistentTxs() { if (numberOfPersistentTxsToBroadcast > 0) { - if (tx.MaxFeePerGas >= _headInfo.CurrentBaseFee) + if (!tx.CanPayBaseFee(_headInfo.CurrentBaseFee)) { - numberOfPersistentTxsToBroadcast--; - if (tx.CanBeBroadcast()) - { - persistentTxsToSend ??= new List(numberOfPersistentTxsToBroadcast); - persistentTxsToSend.Add(tx); - } - else + continue; + } + + if (tx.CanBeBroadcast()) + { + persistentTxsToSend ??= new List(numberOfPersistentTxsToBroadcast); + persistentTxsToSend.Add(tx); + } + else + { + if (!tx.CanPayForBlobGas(_headInfo.CurrentPricePerBlobGas)) { - persistentHashesToSend ??= new List(numberOfPersistentTxsToBroadcast); - persistentHashesToSend.Add(tx); + continue; } + persistentHashesToSend ??= new List(numberOfPersistentTxsToBroadcast); + persistentHashesToSend.Add(tx); } + numberOfPersistentTxsToBroadcast--; } else { @@ -296,9 +306,17 @@ private void NotifyPeersAboutLocalTx(Transaction tx) public bool TryGetPersistentTx(Keccak hash, out Transaction? transaction) { - return _persistentTxs.TryGetValue(hash, out transaction); + if (_persistentTxs.TryGetValue(hash, out transaction) && !transaction.SupportsBlobs) + { + return true; + } + + transaction = default; + return false; } + public bool ContainsTx(Keccak hash) => _persistentTxs.ContainsKey(hash); + public bool AddPeer(ITxPoolPeer peer) { return _peers.TryAdd(peer.Id, peer); diff --git a/src/Nethermind/Nethermind.TxPool/TxPool.cs b/src/Nethermind/Nethermind.TxPool/TxPool.cs index 56f7012c876..944833fb757 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPool.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPool.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; @@ -35,15 +36,13 @@ public class TxPool : ITxPool, IDisposable private readonly IIncomingTxFilter[] _postHashFilters; private readonly HashCache _hashCache = new(); - private readonly TxBroadcaster _broadcaster; private readonly TxDistinctSortedPool _transactions; + private readonly BlobTxDistinctSortedPool _blobTransactions; private readonly IChainHeadSpecProvider _specProvider; - private readonly IAccountStateProvider _accounts; - private readonly IChainHeadInfoProvider _headInfo; private readonly ITxPoolConfig _txPoolConfig; @@ -60,12 +59,14 @@ public class TxPool : ITxPool, IDisposable private readonly ITimer? _timer; private Transaction[]? _transactionSnapshot; + private Transaction[]? _blobTransactionSnapshot; /// /// This class stores all known pending transactions that can be used for block production /// (by miners or validators) or simply informing other nodes about known pending transactions (broadcasting). /// /// Used to recover sender addresses from transaction signatures. + /// /// /// /// @@ -74,8 +75,8 @@ public class TxPool : ITxPool, IDisposable /// /// /// - /// Tx storage used to reject known transactions. public TxPool(IEthereumEcdsa ecdsa, + ITxStorage blobTxStorage, IChainHeadInfoProvider chainHeadInfoProvider, ITxPoolConfig txPoolConfig, ITxValidator validator, @@ -94,15 +95,25 @@ public TxPool(IEthereumEcdsa ecdsa, MemoryAllowance.MemPoolSize = txPoolConfig.Size; AddNodeInfoEntryForTxPool(); - _transactions = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, comparer, logManager); + // Capture closures once rather than per invocation + _updateBucket = UpdateBucket; + _broadcaster = new TxBroadcaster(comparer, TimerFactory.Default, txPoolConfig, chainHeadInfoProvider, logManager, transactionsGossipPolicy); + _transactions = new TxDistinctSortedPool(MemoryAllowance.MemPoolSize, comparer, logManager); + _blobTransactions = txPoolConfig is { BlobSupportEnabled: true, PersistentBlobStorageEnabled: true } + ? new PersistentBlobTxDistinctSortedPool(blobTxStorage, _txPoolConfig, comparer, logManager) + : new BlobTxDistinctSortedPool(txPoolConfig.BlobSupportEnabled ? _txPoolConfig.InMemoryBlobPoolSize : 0, comparer, logManager); + if (_blobTransactions.Count > 0) _blobTransactions.UpdatePool(_accounts, _updateBucket); + _headInfo.HeadChanged += OnHeadChange; _preHashFilters = new IIncomingTxFilter[] { + new NotSupportedTxFilter(txPoolConfig, _logger), new GasLimitTxFilter(_headInfo, txPoolConfig, _logger), - new FeeTooLowFilter(_headInfo, _transactions, thereIsPriorityContract, _logger), + new PriorityFeeTooLowFilter(_logger), + new FeeTooLowFilter(_headInfo, _transactions, _blobTransactions, thereIsPriorityContract, _logger), new MalformedTxFilter(_specProvider, validator, _logger) }; @@ -111,10 +122,12 @@ public TxPool(IEthereumEcdsa ecdsa, new NullHashTxFilter(), // needs to be first as it assigns the hash new AlreadyKnownTxFilter(_hashCache, _logger), new UnknownSenderFilter(ecdsa, _logger), + new TxTypeTxFilter(_transactions, _blobTransactions), // has to be after UnknownSenderFilter as it uses sender new BalanceZeroFilter(thereIsPriorityContract, _logger), - new BalanceTooLowFilter(_transactions, _logger), + new BalanceTooLowFilter(_transactions, _blobTransactions, _logger), new LowNonceFilter(_logger), // has to be after UnknownSenderFilter as it uses sender - new GapNonceFilter(_transactions, _logger), + new FutureNonceFilter(txPoolConfig), + new GapNonceFilter(_transactions, _blobTransactions, _logger), }; if (incomingTxFilter is not null) @@ -126,9 +139,6 @@ public TxPool(IEthereumEcdsa ecdsa, _postHashFilters = postHashFilters.ToArray(); - // Capture closures once rather than per invocation - _updateBucket = UpdateBucket; - int? reportMinutes = txPoolConfig.ReportMinutes; if (_logger.IsInfo && reportMinutes.HasValue) { @@ -148,17 +158,24 @@ public TxPool(IEthereumEcdsa ecdsa, public IDictionary GetPendingTransactionsBySender() => _transactions.GetBucketSnapshot(); + public IDictionary GetPendingLightBlobTransactionsBySender() => + _blobTransactions.GetBucketSnapshot(); + public Transaction[] GetPendingTransactionsBySender(Address address) => _transactions.GetBucketSnapshot(address); + // only for testing reasons internal Transaction[] GetOwnPendingTransactions() => _broadcaster.GetSnapshot(); + public int GetPendingBlobTransactionsCount() => _blobTransactions.Count; + private void OnHeadChange(object? sender, BlockReplacementEventArgs e) { try { // Clear snapshot _transactionSnapshot = null; + _blobTransactionSnapshot = null; _hashCache.ClearCurrentBlockCache(); _headBlocksChannel.Writer.TryWrite(e); } @@ -185,6 +202,7 @@ private void ProcessNewHeads() UpdateBuckets(); _broadcaster.BroadcastPersistentTxs(); Metrics.TransactionCount = _transactions.Count; + Metrics.BlobTransactionCount = _blobTransactions.Count; } catch (Exception e) { @@ -225,6 +243,8 @@ private void RemoveProcessedTransactions(Transaction[] blockTransactions) long discoveredForPendingTxs = 0; long discoveredForHashCache = 0; long eip1559Txs = 0; + long blobTxs = 0; + long blobs = 0; for (int i = 0; i < blockTransactions.Length; i++) { @@ -245,6 +265,12 @@ private void RemoveProcessedTransactions(Transaction[] blockTransactions) { eip1559Txs++; } + + if (transaction.SupportsBlobs) + { + blobTxs++; + blobs += transaction.BlobVersionedHashes?.Length ?? 0; + } } long transactionsInBlock = blockTransactions.Length; @@ -253,6 +279,8 @@ private void RemoveProcessedTransactions(Transaction[] blockTransactions) Metrics.DarkPoolRatioLevel1 = (float)discoveredForHashCache / transactionsInBlock; Metrics.DarkPoolRatioLevel2 = (float)discoveredForPendingTxs / transactionsInBlock; Metrics.Eip1559TransactionsRatio = (float)eip1559Txs / transactionsInBlock; + Metrics.BlobTransactionsInBlock = blobTxs; + Metrics.BlobsInBlock = blobs; } } @@ -267,7 +295,8 @@ public void AddPeer(ITxPoolPeer peer) { if (_broadcaster.AddPeer(peer)) { - _broadcaster.BroadcastOnce(peer, _transactionSnapshot ??= _transactions.GetSnapshot()); + _broadcaster.AnnounceOnce(peer, _transactionSnapshot ??= _transactions.GetSnapshot()); + _broadcaster.AnnounceOnce(peer, _blobTransactionSnapshot ??= _blobTransactions.GetSnapshot()); if (_logger.IsTrace) _logger.Trace($"Added a peer to TX pool: {peer}"); } @@ -312,8 +341,11 @@ public AcceptTxResult SubmitTx(Transaction tx, TxHandlingOptions handlingOptions accepted = AddCore(tx, state, startBroadcast); if (accepted) { - // Clear snapshot - _transactionSnapshot = null; + // Clear proper snapshot + if (tx.SupportsBlobs) + _blobTransactionSnapshot = null; + else + _transactionSnapshot = null; } } @@ -351,39 +383,50 @@ private AcceptTxResult AddCore(Transaction tx, TxFilteringState state, bool isPe { bool eip1559Enabled = _specProvider.GetCurrentHeadSpec().IsEip1559Enabled; UInt256 effectiveGasPrice = tx.CalculateEffectiveGasPrice(eip1559Enabled, _headInfo.CurrentBaseFee); + TxDistinctSortedPool relevantPool = (tx.SupportsBlobs ? _blobTransactions : _transactions); - _transactions.TryGetBucketsWorstValue(tx.SenderAddress!, out Transaction? worstTx); + relevantPool.TryGetBucketsWorstValue(tx.SenderAddress!, out Transaction? worstTx); tx.GasBottleneck = (worstTx is null || effectiveGasPrice <= worstTx.GasBottleneck) ? effectiveGasPrice : worstTx.GasBottleneck; - bool inserted = _transactions.TryInsert(tx.Hash!, tx, out Transaction? removed); - if (inserted && tx.Hash != removed?.Hash) - { - _transactions.UpdateGroup(tx.SenderAddress!, state.SenderAccount, UpdateBucketWithAddedTransaction); - Metrics.PendingTransactionsAdded++; - if (tx.Supports1559) { Metrics.Pending1559TransactionsAdded++; } + bool inserted = relevantPool.TryInsert(tx.Hash!, tx, out Transaction? removed); - if (removed is not null) - { - EvictedPending?.Invoke(this, new TxEventArgs(removed)); - // transaction which was on last position in sorted TxPool and was deleted to give - // a place for a newly added tx (with higher priority) is now removed from hashCache - // to give it opportunity to come back to TxPool in the future, when fees drops - _hashCache.DeleteFromLongTerm(removed.Hash!); - Metrics.PendingTransactionsEvicted++; - } + if (!inserted) + { + // it means it failed on adding to the pool - it is possible when new tx has the same sender + // and nonce as already existent tx and is not good enough to replace it + Metrics.PendingTransactionsPassedFiltersButCannotReplace++; + return AcceptTxResult.ReplacementNotAllowed; } - else + + if (tx.Hash == removed?.Hash) { - if (isPersistentBroadcast && inserted) + // it means it was added and immediately evicted - pool was full of better txs + if (isPersistentBroadcast) { - // it means it was added and immediately evicted - we are adding only to persistent broadcast + // we are adding only to persistent broadcast - not good enough for standard pool, + // but can be good enough for TxBroadcaster pool - for local txs only _broadcaster.Broadcast(tx, isPersistentBroadcast); } Metrics.PendingTransactionsPassedFiltersButCannotCompeteOnFees++; return AcceptTxResult.FeeTooLowToCompete; } + + relevantPool.UpdateGroup(tx.SenderAddress!, state.SenderAccount, UpdateBucketWithAddedTransaction); + Metrics.PendingTransactionsAdded++; + if (tx.Supports1559) { Metrics.Pending1559TransactionsAdded++; } + if (tx.SupportsBlobs) { Metrics.PendingBlobTransactionsAdded++; } + + if (removed is not null) + { + EvictedPending?.Invoke(this, new TxEventArgs(removed)); + // transaction which was on last position in sorted TxPool and was deleted to give + // a place for a newly added tx (with higher priority) is now removed from hashCache + // to give it opportunity to come back to TxPool in the future, when fees drops + _hashCache.DeleteFromLongTerm(removed.Hash!); + Metrics.PendingTransactionsEvicted++; + } } _broadcaster.Broadcast(tx, isPersistentBroadcast); @@ -391,6 +434,7 @@ private AcceptTxResult AddCore(Transaction tx, TxFilteringState state, bool isPe _hashCache.SetLongTerm(tx.Hash!); NewPending?.Invoke(this, new TxEventArgs(tx)); Metrics.TransactionCount = _transactions.Count; + Metrics.BlobTransactionCount = _blobTransactions.Count; return AcceptTxResult.Accepted; } @@ -430,7 +474,12 @@ private AcceptTxResult AddCore(Transaction tx, TxFilteringState state, bool isPe previousTxBottleneck ??= tx.CalculateAffordableGasPrice(_specProvider.GetCurrentHeadSpec().IsEip1559Enabled, _headInfo.CurrentBaseFee, balance); - if (tx.Nonce == currentNonce + i) + // it is not affecting non-blob txs - for them MaxFeePerBlobGas is null so check is skipped + if (tx.MaxFeePerBlobGas < _headInfo.CurrentPricePerBlobGas) + { + gasBottleneck = UInt256.Zero; + } + else if (tx.Nonce == currentNonce + i) { UInt256 effectiveGasPrice = tx.CalculateEffectiveGasPrice(_specProvider.GetCurrentHeadSpec().IsEip1559Enabled, @@ -460,10 +509,11 @@ private void UpdateBuckets() { lock (_locker) { - // ensure the capacity of the pool - if (_transactions.Count > _txPoolConfig.Size) - if (_logger.IsWarn) _logger.Warn($"TxPool exceeds the config size {_transactions.Count}/{_txPoolConfig.Size}"); + _transactions.VerifyCapacity(); _transactions.UpdatePool(_accounts, _updateBucket); + + _blobTransactions.VerifyCapacity(); + _blobTransactions.UpdatePool(_accounts, _updateBucket); } } @@ -530,7 +580,9 @@ public bool RemoveTransaction(Keccak? hash) bool hasBeenRemoved; lock (_locker) { - hasBeenRemoved = _transactions.TryRemove(hash, out Transaction? transaction); + hasBeenRemoved = _transactions.TryRemove(hash, out Transaction? transaction) + || _blobTransactions.TryRemove(hash, out transaction); + if (transaction is null || !hasBeenRemoved) return false; if (hasBeenRemoved) @@ -546,32 +598,48 @@ public bool RemoveTransaction(Keccak? hash) return hasBeenRemoved; } + public bool ContainsTx(Keccak hash, TxType txType) => txType == TxType.Blob + ? _blobTransactions.ContainsKey(hash) + : _transactions.ContainsKey(hash) || _broadcaster.ContainsTx(hash); + public bool TryGetPendingTransaction(Keccak hash, out Transaction? transaction) { lock (_locker) { - if (!_transactions.TryGetValue(hash, out transaction)) - { - _broadcaster.TryGetPersistentTx(hash, out transaction); - - // commented out as it puts too much pressure on the database - // and it not really required in any scenario - // * tx recovery usually will fetch from pending - // * get tx via RPC usually will fetch from block or from pending - // * internal tx pool scenarios are handled directly elsewhere - // transaction = _txStorage.Get(hash); - } + return _transactions.TryGetValue(hash, out transaction) + || _blobTransactions.TryGetValue(hash, out transaction) + || _broadcaster.TryGetPersistentTx(hash, out transaction); } + } - return transaction is not null; + public bool TryGetPendingBlobTransaction(Keccak hash, [NotNullWhen(true)] out Transaction? blobTransaction) + { + lock (_locker) + { + return _blobTransactions.TryGetValue(hash, out blobTransaction); + } } + // only for tests - to test sorting + internal void TryGetBlobTxSortingEquivalent(Keccak hash, out Transaction? transaction) + => _blobTransactions.TryGetBlobTxSortingEquivalent(hash, out transaction); + + // should own transactions (in broadcaster) be also checked here? + // maybe it should use NonceManager, as it already has info about local txs? public UInt256 GetLatestPendingNonce(Address address) { UInt256 maxPendingNonce = _accounts.GetAccount(address).Nonce; + bool hasPendingTxs = _transactions.GetBucketCount(address) > 0; + if (!hasPendingTxs && !(_blobTransactions.GetBucketCount(address) > 0)) + { + // sender doesn't have txs in any pool, quick return + return maxPendingNonce; + } + + TxDistinctSortedPool relevantPool = (hasPendingTxs ? _transactions : _blobTransactions); // we are not doing any updating, but lets just use a thread-safe method without any data copying like snapshot - _transactions.UpdateGroup(address, (_, transactions) => + relevantPool.UpdateGroup(address, (_, transactions) => { // This is under the assumption that the addressTransactions are sorted by Nonce. if (transactions.Count > 0) @@ -654,26 +722,35 @@ private static void WriteTxPoolReport(ILogger logger) if (float.IsNaN(receivedDiscarded)) receivedDiscarded = 0; logger.Info(@$" -Txn Pool State ({Metrics.TransactionCount:N0} txns queued) +------------------------------------------------ +TxPool: {Metrics.TransactionCount:N0} txns queued +BlobPool: {Metrics.BlobTransactionCount:N0} txns queued ------------------------------------------------ Sent * Transactions: {Metrics.PendingTransactionsSent,24:N0} * Hashes: {Metrics.PendingTransactionsHashesSent,24:N0} ------------------------------------------------ -Total Received: {Metrics.PendingTransactionsReceived,24:N0} +Received +* Transactions: {Metrics.PendingTransactionsReceived,24:N0} +* Hashes: {Metrics.PendingTransactionsHashesReceived,24:N0} ------------------------------------------------ Discarded at Filter Stage: -1. GasLimitTooHigh: {Metrics.PendingTransactionsGasLimitTooHigh,24:N0} -2. Too Low Fee: {Metrics.PendingTransactionsTooLowFee,24:N0} -3. Malformed {Metrics.PendingTransactionsMalformed,24:N0} -4. Duplicate: {Metrics.PendingTransactionsKnown,24:N0} -5. Unknown Sender: {Metrics.PendingTransactionsUnresolvableSender,24:N0} -6. Zero Balance: {Metrics.PendingTransactionsZeroBalance,24:N0} -7. Balance < tx.value: {Metrics.PendingTransactionsBalanceBelowValue,24:N0} -8. Nonce used: {Metrics.PendingTransactionsLowNonce,24:N0} -9. Nonces skipped: {Metrics.PendingTransactionsNonceGap,24:N0} -10. Balance Too Low: {Metrics.PendingTransactionsTooLowBalance,24:N0} -11. Cannot Compete: {Metrics.PendingTransactionsPassedFiltersButCannotCompeteOnFees,24:N0} +1. NotSupportedTxType {Metrics.PendingTransactionsNotSupportedTxType,24:N0} +2. GasLimitTooHigh: {Metrics.PendingTransactionsGasLimitTooHigh,24:N0} +3. TooLow PriorityFee: {Metrics.PendingTransactionsTooLowPriorityFee,24:N0} +4. Too Low Fee: {Metrics.PendingTransactionsTooLowFee,24:N0} +5. Malformed {Metrics.PendingTransactionsMalformed,24:N0} +6. Duplicate: {Metrics.PendingTransactionsKnown,24:N0} +7. Unknown Sender: {Metrics.PendingTransactionsUnresolvableSender,24:N0} +8. Conflicting TxType {Metrics.PendingTransactionsConflictingTxType,24:N0} +9. NonceTooFarInFuture {Metrics.PendingTransactionsNonceTooFarInFuture,24:N0} +10. Zero Balance: {Metrics.PendingTransactionsZeroBalance,24:N0} +11. Balance < tx.value: {Metrics.PendingTransactionsBalanceBelowValue,24:N0} +12. Balance Too Low: {Metrics.PendingTransactionsTooLowBalance,24:N0} +13. Nonce used: {Metrics.PendingTransactionsLowNonce,24:N0} +14. Nonces skipped: {Metrics.PendingTransactionsNonceGap,24:N0} +15. Failed replacement {Metrics.PendingTransactionsPassedFiltersButCannotReplace,24:N0} +16. Cannot Compete: {Metrics.PendingTransactionsPassedFiltersButCannotCompeteOnFees,24:N0} ------------------------------------------------ Validated via State: {Metrics.PendingTransactionsWithExpensiveFiltering,24:N0} ------------------------------------------------ @@ -685,13 +762,21 @@ Txn Pool State ({Metrics.TransactionCount:N0} txns queued) ------------------------------------------------ Total Added: {Metrics.PendingTransactionsAdded,24:N0} * Eip1559 Added: {Metrics.Pending1559TransactionsAdded,24:N0} +* Blob Added: {Metrics.PendingBlobTransactionsAdded,24:N0} ------------------------------------------------ Total Evicted: {Metrics.PendingTransactionsEvicted,24:N0} ------------------------------------------------ -Ratios: +Ratios in last block: * Eip1559 Transactions: {Metrics.Eip1559TransactionsRatio,24:P5} * DarkPool Level1: {Metrics.DarkPoolRatioLevel1,24:P5} * DarkPool Level2: {Metrics.DarkPoolRatioLevel2,24:P5} +Amounts: +* Blob txs: {Metrics.BlobTransactionsInBlock,24:N0} +* Blobs: {Metrics.BlobsInBlock,24:N0} +------------------------------------------------ +Db usage: +* BlobDb writes: {Db.Metrics.BlobTransactionsDbWrites,24:N0} +* BlobDb reads: {Db.Metrics.BlobTransactionsDbReads,24:N0} ------------------------------------------------ "); } diff --git a/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs b/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs index 7e08186f337..071ed94dd04 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPoolConfig.cs @@ -7,6 +7,15 @@ public class TxPoolConfig : ITxPoolConfig { public int PeerNotificationThreshold { get; set; } = 5; public int Size { get; set; } = 2048; + public bool BlobSupportEnabled { get; set; } = false; + public bool PersistentBlobStorageEnabled { get; set; } = false; + public int PersistentBlobStorageSize { get; set; } = 16 * 1024; // theoretical max - 13GB (128KB * 6 * 16384); for one-blob txs - 2GB (128KB * 1 * 16384); + // practical max - something between, but closer to 2GB than 12GB. Geth is limiting it to 10GB. + // every day about 21600 blobs will be included (7200 blocks per day * 3 blob target) + public int BlobCacheSize { get; set; } = 256; + public int InMemoryBlobPoolSize { get; set; } = 512; // it is used when persistent pool is disabled + public int MaxPendingTxsPerSender { get; set; } = 0; + public int MaxPendingBlobTxsPerSender { get; set; } = 16; public int HashCacheSize { get; set; } = 512 * 1024; public long? GasLimit { get; set; } = null; public int? ReportMinutes { get; set; } = null; diff --git a/src/Nethermind/Nethermind.TxPool/TxPoolInfoProvider.cs b/src/Nethermind/Nethermind.TxPool/TxPoolInfoProvider.cs index 198a130ee1a..d1034581fca 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPoolInfoProvider.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPoolInfoProvider.cs @@ -21,6 +21,8 @@ public TxPoolInfoProvider(IAccountStateProvider accountStateProvider, ITxPool tx public TxPoolInfo GetInfo() { + // only std txs are picked here. Should we add blobs? + // BTW this class should be rewritten or removed - a lot of unnecessary allocations var groupedTransactions = _txPool.GetPendingTransactionsBySender(); var pendingTransactions = new Dictionary>(); var queuedTransactions = new Dictionary>();