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");
+}