diff --git a/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs b/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs index cbb844bafe94..549b9a6b5df3 100644 --- a/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs +++ b/src/Nethermind/Nethermind.Consensus.AuRa/InitializationSteps/InitializeBlockchainAuRa.cs @@ -100,7 +100,7 @@ protected override TxPool.TxPool CreateTxPool(IChainHeadInfoProvider chainHeadIn api.LogManager, CreateTxPoolTxComparer(txPriorityContract, localDataSource), _txGossipPolicy, - new TxFilterAdapter(api.BlockTree, txPoolFilter, api.LogManager, api.SpecProvider), + [new TxFilterAdapter(api.BlockTree, txPoolFilter, api.LogManager, api.SpecProvider)], api.HeadTxValidator, txPriorityContract is not null || localDataSource is not null); } diff --git a/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs index d55e86785092..52bec3b1946a 100644 --- a/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs +++ b/src/Nethermind/Nethermind.TxPool.Test/TxPoolTests.cs @@ -2517,7 +2517,7 @@ private TxPool CreatePool( _logManager, transactionComparerProvider.GetDefaultComparer(), ShouldGossip.Instance, - incomingTxFilter, + incomingTxFilter is null ? null : [incomingTxFilter], new HeadTxValidator(), thereIsPriorityContract); } diff --git a/src/Nethermind/Nethermind.TxPool/TxPool.cs b/src/Nethermind/Nethermind.TxPool/TxPool.cs index 0fb7dec31535..ae30dc43ec30 100644 --- a/src/Nethermind/Nethermind.TxPool/TxPool.cs +++ b/src/Nethermind/Nethermind.TxPool/TxPool.cs @@ -94,7 +94,7 @@ public class TxPool : ITxPool, IAsyncDisposable /// /// /// - /// + /// /// /// public TxPool(IEthereumEcdsa ecdsa, @@ -105,7 +105,7 @@ public TxPool(IEthereumEcdsa ecdsa, ILogManager? logManager, IComparer comparer, ITxGossipPolicy? transactionsGossipPolicy = null, - IIncomingTxFilter? incomingTxFilter = null, + IIncomingTxFilter[]? incomingTxFilters = null, [KeyFilter(ITxValidator.HeadTxValidatorKey)] ITxValidator? headTxValidator = null, bool thereIsPriorityContract = false) { @@ -174,9 +174,9 @@ public TxPool(IEthereumEcdsa ecdsa, new DelegatedAccountFilter(_specProvider, _transactions, _blobTransactions, chainHeadInfoProvider.ReadOnlyStateProvider, _pendingDelegations), ]; - if (incomingTxFilter is not null) + if (incomingTxFilters is not null) { - postHashFilters.Add(incomingTxFilter); + postHashFilters.AddRange(incomingTxFilters); } postHashFilters.Add(new DeployedCodeFilter(chainHeadInfoProvider.ReadOnlyStateProvider, _specProvider)); diff --git a/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs new file mode 100644 index 000000000000..e981b9de86b2 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using System.Threading.Tasks; +using Nethermind.Consensus; +using Nethermind.Core; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Logging; +using Nethermind.TxPool; +using Nethermind.Xdc.Spec; +using Nethermind.Xdc.Test.Helpers; +using Nethermind.Xdc.TxPool; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Xdc.Test; + +[Parallelizable(ParallelScope.All)] +internal class BlackListedAddressFilterTests +{ + private static readonly Address BlackListed = TestItem.AddressA; + + private static BlackListedAddressFilter CreateFilter(ulong headNumber, bool blackListingEnabled, ISpecProvider? specProvider = null) + { + IChainHeadInfoProvider chainHeadInfoProvider = Substitute.For(); + chainHeadInfoProvider.HeadNumber.Returns(headNumber); + + IXdcReleaseSpec xdcSpec = Substitute.For(); + xdcSpec.IsBlackListingEnabled.Returns(blackListingEnabled); + HashSet
blackList = [BlackListed]; + xdcSpec.BlackListedAddresses.Returns(blackList); + + specProvider ??= Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(xdcSpec); + + return new BlackListedAddressFilter(chainHeadInfoProvider, specProvider, LimboLogs.Instance); + } + + private static AcceptTxResult Accept(BlackListedAddressFilter filter, Transaction tx) + { + TxFilteringState state = default; + return filter.Accept(tx, ref state, TxHandlingOptions.None); + } + + private static Transaction BuildTx(Address? sender, Address? to) => + Build.A.Transaction.WithSenderAddress(sender).WithTo(to).TestObject; + + [TestCase(true, false, true, false, TestName = "Blacklisted sender rejected once activated")] + [TestCase(true, false, false, true, TestName = "Blacklisted sender allowed before activation")] + [TestCase(false, true, true, false, TestName = "Blacklisted recipient rejected once activated")] + [TestCase(false, true, false, true, TestName = "Blacklisted recipient allowed before activation")] + [TestCase(false, false, true, true, TestName = "Unlisted addresses accepted once activated")] + [TestCase(false, false, false, true, TestName = "Unlisted addresses accepted before activation")] + public void Accept_ChecksSenderAndRecipient(bool blackListSender, bool blackListRecipient, bool blackListingEnabled, bool expectedAccepted) + { + BlackListedAddressFilter filter = CreateFilter(headNumber: 100, blackListingEnabled); + Transaction tx = BuildTx(blackListSender ? BlackListed : TestItem.AddressB, blackListRecipient ? BlackListed : TestItem.AddressC); + + Assert.That((bool)Accept(filter, tx), Is.EqualTo(expectedAccepted)); + } + + [TestCase(true, "sender")] + [TestCase(false, "recipient")] + public void Accept_BlackListedAddress_ReportsRoleWithoutDisconnectingPeer(bool blackListSender, string expectedRole) + { + BlackListedAddressFilter filter = CreateFilter(headNumber: 100, blackListingEnabled: true); + Transaction tx = blackListSender ? BuildTx(BlackListed, TestItem.AddressC) : BuildTx(TestItem.AddressB, BlackListed); + + AcceptTxResult result = Accept(filter, tx); + + Assert.That(result, Is.EqualTo(XdcAcceptTxResult.BlackListedSender)); + Assert.That(result.ToString(), Does.Contain(expectedRole)); + Assert.That(result, Is.Not.EqualTo(AcceptTxResult.Invalid), "Invalid makes TxFloodController disconnect the relaying peer"); + } + + [Test] + public void Accept_ContractCreation_IsAccepted() + { + BlackListedAddressFilter filter = CreateFilter(headNumber: 100, blackListingEnabled: true); + + Assert.That(Accept(filter, BuildTx(TestItem.AddressB, to: null)), Is.EqualTo(AcceptTxResult.Accepted)); + } + + [Test] + public void Accept_UsesSpecOfBlockAfterHead() + { + const ulong headNumber = 1234; + ISpecProvider specProvider = Substitute.For(); + BlackListedAddressFilter filter = CreateFilter(headNumber, blackListingEnabled: true, specProvider); + + Accept(filter, BuildTx(TestItem.AddressB, TestItem.AddressC)); + + specProvider.Received().GetSpec(Arg.Is(f => f.BlockNumber == headNumber + 1)); + } + + [TestCase(true, true, false, TestName = "Pool rejects blacklisted sender")] + [TestCase(false, true, false, TestName = "Pool rejects blacklisted recipient")] + [TestCase(true, false, true, TestName = "Pool accepts blacklisted sender before activation")] + public async Task SubmitTx_BlackListedAddress_IsRejectedOnPoolAdmission(bool blackListSender, bool blackListingEnabled, bool expectedAccepted) + { + using XdcTestBlockchain chain = await XdcTestBlockchain.Create(5, false); + chain.ChangeReleaseSpec(spec => + { + spec.BlackListedAddresses = [blackListSender ? TestItem.AddressB : TestItem.AddressC]; + spec.IsBlackListingEnabled = blackListingEnabled; + }); + + Transaction tx = Build.A.Transaction + .WithSenderAddress(TestItem.AddressB) + .WithTo(TestItem.AddressC) + .WithValue(1) + .WithType(TxType.Legacy) + .WithNonce(chain.TxPool.GetLatestPendingNonce(TestItem.AddressB)) + .TestObject; + new Signer(chain.SpecProvider.ChainId, TestItem.PrivateKeyB, NullLogManager.Instance).TrySign(tx); + tx.Hash = tx.CalculateHash(); + + AcceptTxResult result = chain.TxPool.SubmitTx(tx, TxHandlingOptions.None); + + Assert.That((bool)result, Is.EqualTo(expectedAccepted), result.ToString()); + if (!expectedAccepted) + Assert.That(result, Is.EqualTo(blackListSender ? XdcAcceptTxResult.BlackListedSender : XdcAcceptTxResult.BlackListedRecipient)); + Assert.That(chain.TxPool.GetPendingTransactions(), Has.Length.EqualTo(expectedAccepted ? 1 : 0)); + } +} diff --git a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs index 2b10461dadca..83c03cde2bdd 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs @@ -185,7 +185,10 @@ protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder, ctx.Resolve(), new XdcTransactionComparerProvider(ctx.Resolve(), ctx.Resolve()).GetDefaultComparer(), ctx.Resolve(), - new SignTransactionFilter(ctx.Resolve(), ctx.Resolve(), ctx.Resolve()), + [ + new SignTransactionFilter(ctx.Resolve(), ctx.Resolve(), ctx.Resolve()), + new BlackListedAddressFilter(ctx.Resolve(), ctx.Resolve(), ctx.Resolve()) + ], ctx.Resolve() ); diff --git a/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs b/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs index 3ce55bcf14de..0edc081267c5 100644 --- a/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs +++ b/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs @@ -40,7 +40,10 @@ protected override ITxPool CreateTxPool(IChainHeadInfoProvider chainHeadInfoProv _api.LogManager, CreateTxPoolTxComparer(), _txGossipPolicy, - new SignTransactionFilter(snapshotManager, _api.BlockTree, XdcSpecProvider), + [ + new SignTransactionFilter(snapshotManager, _api.BlockTree, XdcSpecProvider), + new BlackListedAddressFilter(chainHeadInfoProvider, XdcSpecProvider, _api.LogManager) + ], _api.HeadTxValidator, true ); diff --git a/src/Nethermind/Nethermind.Xdc/README.md b/src/Nethermind/Nethermind.Xdc/README.md index 72e16b44eccb..648002bf5d5c 100644 --- a/src/Nethermind/Nethermind.Xdc/README.md +++ b/src/Nethermind/Nethermind.Xdc/README.md @@ -416,7 +416,8 @@ closed on mainnet. - Post-`TipTrc21Fee`, gas fees are paid to the **candidate owner** of the block beneficiary rather than the beneficiary itself. -- Post-`BlackListHFNumber`, transactions with a blacklisted sender or recipient are rejected. +- Post-`BlackListHFNumber`, transactions with a blacklisted sender or recipient are rejected during execution, + and on pool admission, so they are never gossiped. ### Block execution context @@ -434,6 +435,9 @@ value, and forces blob base fee to zero, since XDC enables the `BLOBBASEFEE` opc - [`SignTransactionFilter`](TxPool/SignTransactionFilter.cs) accepts fee-exempt transactions only from current epoch candidates, and only when the signed block is recent. +- [`BlackListedAddressFilter`](TxPool/BlackListedAddressFilter.cs) rejects transactions with a blacklisted + sender or recipient once `BlackListHFNumber` activates, so they never reach a block or a peer. The rejection + code is deliberately not `AcceptTxResult.Invalid`, which would disconnect the relaying peer. - [`XdcTxGossipPolicy`](TxPool/XdcTxGossipPolicy.cs) withholds the DEX/lending family; sign and randomize transactions are gossiped normally. - [`XdcTxFilterPipeline`](TxPool/XdcTxFilterPipeline.cs) lets the fee-exempt transactions bypass the diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs new file mode 100644 index 000000000000..5e7f4991208e --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.Core.Specs; +using Nethermind.Logging; +using Nethermind.TxPool; +using Nethermind.TxPool.Filters; +using Nethermind.Xdc.Spec; + +namespace Nethermind.Xdc.TxPool; + +/// +/// Rejects transactions whose sender or recipient is blacklisted, keeping them out of the pool and out of gossip. +/// +/// +/// The blacklist is a consensus rule enforced during execution by ; +/// this filter is the mempool-admission counterpart, so that such transactions are dropped on submission rather than +/// when a block containing them is processed. Activation is read from the spec of the block the transaction would land +/// in, one past the current head, matching . +/// +internal sealed class BlackListedAddressFilter( + IChainHeadInfoProvider chainHeadInfoProvider, + ISpecProvider specProvider, + ILogManager logManager) : IIncomingTxFilter +{ + private readonly ILogger _logger = logManager.GetClassLogger(); + + public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions) + { + IXdcReleaseSpec spec = specProvider.GetXdcSpec(chainHeadInfoProvider.HeadNumber + 1); + + if (!spec.IsBlackListingEnabled) + return AcceptTxResult.Accepted; + + if (IsBlackListed(spec, tx.SenderAddress)) + return Reject(tx, XdcAcceptTxResult.BlackListedSender); + + if (IsBlackListed(spec, tx.To)) + return Reject(tx, XdcAcceptTxResult.BlackListedRecipient); + + return AcceptTxResult.Accepted; + } + + private AcceptTxResult Reject(Transaction tx, AcceptTxResult result) + { + if (_logger.IsTrace) _logger.Trace($"Skipped adding transaction {tx.ToString(" ")}, {result}."); + return result; + } + + private static bool IsBlackListed(IXdcReleaseSpec spec, Address? address) => + address is not null && spec.BlackListedAddresses.Contains(address); +} diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs new file mode 100644 index 000000000000..ad7192d1a5ea --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.TxPool; + +namespace Nethermind.Xdc.TxPool; + +internal static class XdcAcceptTxResult +{ + private const int BlackListedAddressId = 1000; + private const string BlackListedAddressCode = "BlackListedAddress"; + + public static AcceptTxResult BlackListedSender { get; } = new(BlackListedAddressId, BlackListedAddressCode, "Transaction sender is blacklisted"); + public static AcceptTxResult BlackListedRecipient { get; } = new(BlackListedAddressId, BlackListedAddressCode, "Transaction recipient is blacklisted"); +}