From ee004e3cfc6a1736c0f142ec2d436445272ec037 Mon Sep 17 00:00:00 2001 From: ak88 Date: Tue, 18 Aug 2026 17:18:33 +0200 Subject: [PATCH 1/6] feat(xdc): reject blacklisted addresses on pool admission The XDC blacklist was only enforced during execution, in XdcTransactionProcessor.ValidateSender. A transaction with a blacklisted sender or recipient was therefore accepted into the pool and gossiped to peers, only to be skipped by the block producer or to invalidate a block that included it. The Go reference client checks the denylist in both places: core/txpool/validation.go on pool admission (for RPC submissions and p2p-received transactions alike) and core/state_processor.go during block processing. This adds the missing mempool-level check as a new incoming tx filter, reading activation and the address set from the spec at the current head. Since the pool takes a single custom filter, a small composite runs it alongside the existing SignTransactionFilter. Co-Authored-By: Claude Opus 5 --- .../BlackListedAddressFilterTests.cs | 90 +++++++++++++++++++ .../Helpers/XdcTestBlockchain.cs | 4 +- .../Nethermind.Xdc/InitializeBlockchainXdc.cs | 4 +- src/Nethermind/Nethermind.Xdc/README.md | 4 +- .../TxPool/BlackListedAddressFilter.cs | 44 +++++++++ .../TxPool/CompositeIncomingTxFilter.cs | 29 ++++++ 6 files changed, 172 insertions(+), 3 deletions(-) create mode 100644 src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs create mode 100644 src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs create mode 100644 src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs diff --git a/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs new file mode 100644 index 000000000000..3f63b92f353b --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System.Collections.Generic; +using Nethermind.Blockchain; +using Nethermind.Core; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.TxPool; +using Nethermind.Xdc.Spec; +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(Block? head, bool blackListingEnabled, ISpecProvider? specProvider = null) + { + IBlockTree blockTree = Substitute.For(); + blockTree.Head.Returns(head); + + IXdcReleaseSpec xdcSpec = Substitute.For(); + xdcSpec.IsBlackListingEnabled.Returns(blackListingEnabled); + xdcSpec.BlackListedAddresses.Returns(new HashSet
{ BlackListed }); + + specProvider ??= Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(xdcSpec); + + return new BlackListedAddressFilter(blockTree, specProvider); + } + + private static Block HeadBlock(ulong number = 100) => + Build.A.Block.WithHeader(Build.A.XdcBlockHeader().WithNumber(number).TestObject).TestObject; + + 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(HeadBlock(), blackListingEnabled); + Transaction tx = BuildTx(blackListSender ? BlackListed : TestItem.AddressB, blackListRecipient ? BlackListed : TestItem.AddressC); + + Assert.That((bool)Accept(filter, tx), Is.EqualTo(expectedAccepted)); + } + + [Test] + public void Accept_NullHead_ReturnsSyncing() + { + BlackListedAddressFilter filter = CreateFilter(head: null, blackListingEnabled: true); + + Assert.That(Accept(filter, BuildTx(TestItem.AddressB, TestItem.AddressC)), Is.EqualTo(AcceptTxResult.Syncing)); + } + + [Test] + public void Accept_ContractCreation_IsAccepted() + { + BlackListedAddressFilter filter = CreateFilter(HeadBlock(), blackListingEnabled: true); + + Assert.That(Accept(filter, BuildTx(TestItem.AddressB, to: null)), Is.EqualTo(AcceptTxResult.Accepted)); + } + + [Test] + public void Accept_UsesSpecOfCurrentHead() + { + const ulong headNumber = 1234; + ISpecProvider specProvider = Substitute.For(); + BlackListedAddressFilter filter = CreateFilter(HeadBlock(headNumber), blackListingEnabled: true, specProvider); + + Accept(filter, BuildTx(TestItem.AddressB, TestItem.AddressC)); + + specProvider.Received().GetSpec(Arg.Is(f => f.BlockNumber == headNumber)); + } +} diff --git a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs index 2b10461dadca..c325a7a86dec 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs @@ -185,7 +185,9 @@ 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 CompositeIncomingTxFilter( + new SignTransactionFilter(ctx.Resolve(), ctx.Resolve(), ctx.Resolve()), + new BlackListedAddressFilter(ctx.Resolve(), ctx.Resolve())), ctx.Resolve() ); diff --git a/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs b/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs index 3ce55bcf14de..1e0cd3f6b0a0 100644 --- a/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs +++ b/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs @@ -40,7 +40,9 @@ protected override ITxPool CreateTxPool(IChainHeadInfoProvider chainHeadInfoProv _api.LogManager, CreateTxPoolTxComparer(), _txGossipPolicy, - new SignTransactionFilter(snapshotManager, _api.BlockTree, XdcSpecProvider), + new CompositeIncomingTxFilter( + new SignTransactionFilter(snapshotManager, _api.BlockTree, XdcSpecProvider), + new BlackListedAddressFilter(_api.BlockTree, XdcSpecProvider)), _api.HeadTxValidator, true ); diff --git a/src/Nethermind/Nethermind.Xdc/README.md b/src/Nethermind/Nethermind.Xdc/README.md index 72e16b44eccb..19ede9328307 100644 --- a/src/Nethermind/Nethermind.Xdc/README.md +++ b/src/Nethermind/Nethermind.Xdc/README.md @@ -416,7 +416,9 @@ 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 also on pool admission by [`BlackListedAddressFilter`](TxPool/BlackListedAddressFilter.cs) so they are + never gossiped. ### Block execution context diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs new file mode 100644 index 000000000000..f8bd36b6806d --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Blockchain; +using Nethermind.Core; +using Nethermind.Core.Specs; +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 at the current head. +/// +internal sealed class BlackListedAddressFilter(IBlockTree blockTree, ISpecProvider specProvider) : IIncomingTxFilter +{ + public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions) + { + if (blockTree.Head is null) + return AcceptTxResult.Syncing; + + IXdcReleaseSpec spec = specProvider.GetXdcSpec(blockTree.Head.Number); + + if (!spec.IsBlackListingEnabled) + return AcceptTxResult.Accepted; + + if (IsBlackListed(spec, tx.SenderAddress)) + return AcceptTxResult.Invalid.WithMessage("Transaction sender is blacklisted"); + + if (IsBlackListed(spec, tx.To)) + return AcceptTxResult.Invalid.WithMessage("Transaction recipient is blacklisted"); + + return AcceptTxResult.Accepted; + } + + private static bool IsBlackListed(IXdcReleaseSpec spec, Address? address) => + address is not null && spec.BlackListedAddresses.Contains(address); +} diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs b/src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs new file mode 100644 index 000000000000..cd38163116b9 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core; +using Nethermind.TxPool; +using Nethermind.TxPool.Filters; + +namespace Nethermind.Xdc.TxPool; + +/// +/// Runs the given filters in order, returning the first non-accepting result. +/// +/// +/// The pool takes a single custom incoming filter, so XDC-specific filters are combined here. +/// +internal sealed class CompositeIncomingTxFilter(params IIncomingTxFilter[] filters) : IIncomingTxFilter +{ + public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions) + { + foreach (IIncomingTxFilter filter in filters) + { + AcceptTxResult result = filter.Accept(tx, ref state, txHandlingOptions); + if (!result) + return result; + } + + return AcceptTxResult.Accepted; + } +} From b60b297561bc277be88b8592900da8b268e386ac Mon Sep 17 00:00:00 2001 From: ak88 Date: Tue, 18 Aug 2026 17:31:30 +0200 Subject: [PATCH 2/6] fix(xdc): use collection expression in blacklist filter tests Co-Authored-By: Claude Opus 5 --- .../Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs index 3f63b92f353b..32400698451e 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs @@ -26,7 +26,8 @@ private static BlackListedAddressFilter CreateFilter(Block? head, bool blackList IXdcReleaseSpec xdcSpec = Substitute.For(); xdcSpec.IsBlackListingEnabled.Returns(blackListingEnabled); - xdcSpec.BlackListedAddresses.Returns(new HashSet
{ BlackListed }); + HashSet
blackList = [BlackListed]; + xdcSpec.BlackListedAddresses.Returns(blackList); specProvider ??= Substitute.For(); specProvider.GetSpec(Arg.Any()).Returns(xdcSpec); From 377551e9b184e96553da16ba39a2d37f2d25baa7 Mon Sep 17 00:00:00 2001 From: ak88 Date: Tue, 18 Aug 2026 17:45:00 +0200 Subject: [PATCH 3/6] refactor(txpool): take an array of custom incoming filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses review feedback on the XDC blacklist filter: - TxPool now takes IIncomingTxFilter[] rather than a single filter, so XDC can register both of its filters without a composite wrapper. - BlackListedAddressFilter reads the head from IChainHeadInfoProvider, like every other pool filter that needs it, and gates on the spec of head + 1 — the block the transaction would land in — matching SignTransactionFilter. This also removes the null-head branch. - The rejection no longer uses AcceptTxResult.Invalid, which makes TxFloodController disconnect the relaying peer: pool admission is judged against the local head, so two honest nodes one block apart across BlackListHFNumber would drop each other. The transaction is dropped and the peer is kept. - Log the drop at debug level, and list the filter in the README's transaction pool section. - Cover pool admission end to end through TxPool.SubmitTx with blacklisting activated, which also exercises the registration. Co-Authored-By: Claude Opus 5 --- .../InitializeBlockchainAuRa.cs | 2 +- .../Nethermind.TxPool.Test/TxPoolTests.cs | 2 +- src/Nethermind/Nethermind.TxPool/TxPool.cs | 8 +-- .../BlackListedAddressFilterTests.cs | 66 ++++++++++++++----- .../Helpers/XdcTestBlockchain.cs | 5 +- .../Nethermind.Xdc/InitializeBlockchainXdc.cs | 5 +- src/Nethermind/Nethermind.Xdc/README.md | 6 +- .../TxPool/BlackListedAddressFilter.cs | 27 +++++--- .../TxPool/CompositeIncomingTxFilter.cs | 29 -------- .../TxPool/XdcAcceptTxResult.cs | 17 +++++ 10 files changed, 101 insertions(+), 66 deletions(-) delete mode 100644 src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs create mode 100644 src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs 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 index 32400698451e..73375e3316e2 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs @@ -2,12 +2,16 @@ // SPDX-License-Identifier: LGPL-3.0-only using System.Collections.Generic; -using Nethermind.Blockchain; +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; @@ -19,10 +23,10 @@ internal class BlackListedAddressFilterTests { private static readonly Address BlackListed = TestItem.AddressA; - private static BlackListedAddressFilter CreateFilter(Block? head, bool blackListingEnabled, ISpecProvider? specProvider = null) + private static BlackListedAddressFilter CreateFilter(ulong headNumber, bool blackListingEnabled, ISpecProvider? specProvider = null) { - IBlockTree blockTree = Substitute.For(); - blockTree.Head.Returns(head); + IChainHeadInfoProvider chainHeadInfoProvider = Substitute.For(); + chainHeadInfoProvider.HeadNumber.Returns(headNumber); IXdcReleaseSpec xdcSpec = Substitute.For(); xdcSpec.IsBlackListingEnabled.Returns(blackListingEnabled); @@ -32,12 +36,9 @@ private static BlackListedAddressFilter CreateFilter(Block? head, bool blackList specProvider ??= Substitute.For(); specProvider.GetSpec(Arg.Any()).Returns(xdcSpec); - return new BlackListedAddressFilter(blockTree, specProvider); + return new BlackListedAddressFilter(chainHeadInfoProvider, specProvider, LimboLogs.Instance); } - private static Block HeadBlock(ulong number = 100) => - Build.A.Block.WithHeader(Build.A.XdcBlockHeader().WithNumber(number).TestObject).TestObject; - private static AcceptTxResult Accept(BlackListedAddressFilter filter, Transaction tx) { TxFilteringState state = default; @@ -55,37 +56,70 @@ private static Transaction BuildTx(Address? sender, Address? to) => [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(HeadBlock(), blackListingEnabled); + 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)); } [Test] - public void Accept_NullHead_ReturnsSyncing() + public void Accept_BlackListedAddress_DoesNotReturnInvalidSoPeerIsKept() { - BlackListedAddressFilter filter = CreateFilter(head: null, blackListingEnabled: true); + BlackListedAddressFilter filter = CreateFilter(headNumber: 100, blackListingEnabled: true); + + AcceptTxResult result = Accept(filter, BuildTx(BlackListed, TestItem.AddressC)); - Assert.That(Accept(filter, BuildTx(TestItem.AddressB, TestItem.AddressC)), Is.EqualTo(AcceptTxResult.Syncing)); + Assert.That(result, Is.EqualTo(XdcAcceptTxResult.BlackListedAddress)); + Assert.That(result, Is.Not.EqualTo(AcceptTxResult.Invalid), "Invalid makes TxFloodController disconnect the relaying peer"); } [Test] public void Accept_ContractCreation_IsAccepted() { - BlackListedAddressFilter filter = CreateFilter(HeadBlock(), blackListingEnabled: true); + BlackListedAddressFilter filter = CreateFilter(headNumber: 100, blackListingEnabled: true); Assert.That(Accept(filter, BuildTx(TestItem.AddressB, to: null)), Is.EqualTo(AcceptTxResult.Accepted)); } [Test] - public void Accept_UsesSpecOfCurrentHead() + public void Accept_UsesSpecOfBlockAfterHead() { const ulong headNumber = 1234; ISpecProvider specProvider = Substitute.For(); - BlackListedAddressFilter filter = CreateFilter(HeadBlock(headNumber), blackListingEnabled: true, specProvider); + BlackListedAddressFilter filter = CreateFilter(headNumber, blackListingEnabled: true, specProvider); Accept(filter, BuildTx(TestItem.AddressB, TestItem.AddressC)); - specProvider.Received().GetSpec(Arg.Is(f => f.BlockNumber == headNumber)); + 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(XdcAcceptTxResult.BlackListedAddress)); + 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 c325a7a86dec..83c03cde2bdd 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs @@ -185,9 +185,10 @@ protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder, ctx.Resolve(), new XdcTransactionComparerProvider(ctx.Resolve(), ctx.Resolve()).GetDefaultComparer(), ctx.Resolve(), - new CompositeIncomingTxFilter( + [ new SignTransactionFilter(ctx.Resolve(), ctx.Resolve(), ctx.Resolve()), - new BlackListedAddressFilter(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 1e0cd3f6b0a0..0edc081267c5 100644 --- a/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs +++ b/src/Nethermind/Nethermind.Xdc/InitializeBlockchainXdc.cs @@ -40,9 +40,10 @@ protected override ITxPool CreateTxPool(IChainHeadInfoProvider chainHeadInfoProv _api.LogManager, CreateTxPoolTxComparer(), _txGossipPolicy, - new CompositeIncomingTxFilter( + [ new SignTransactionFilter(snapshotManager, _api.BlockTree, XdcSpecProvider), - new BlackListedAddressFilter(_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 19ede9328307..648002bf5d5c 100644 --- a/src/Nethermind/Nethermind.Xdc/README.md +++ b/src/Nethermind/Nethermind.Xdc/README.md @@ -417,8 +417,7 @@ 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 during execution, - and also on pool admission by [`BlackListedAddressFilter`](TxPool/BlackListedAddressFilter.cs) so they are - never gossiped. + and on pool admission, so they are never gossiped. ### Block execution context @@ -436,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 index f8bd36b6806d..50e265baa195 100644 --- a/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs +++ b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs @@ -1,9 +1,9 @@ // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited // SPDX-License-Identifier: LGPL-3.0-only -using Nethermind.Blockchain; using Nethermind.Core; using Nethermind.Core.Specs; +using Nethermind.Logging; using Nethermind.TxPool; using Nethermind.TxPool.Filters; using Nethermind.Xdc.Spec; @@ -16,29 +16,38 @@ namespace Nethermind.Xdc.TxPool; /// /// 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 at the current head. +/// 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(IBlockTree blockTree, ISpecProvider specProvider) : IIncomingTxFilter +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) { - if (blockTree.Head is null) - return AcceptTxResult.Syncing; - - IXdcReleaseSpec spec = specProvider.GetXdcSpec(blockTree.Head.Number); + IXdcReleaseSpec spec = specProvider.GetXdcSpec(chainHeadInfoProvider.HeadNumber + 1); if (!spec.IsBlackListingEnabled) return AcceptTxResult.Accepted; if (IsBlackListed(spec, tx.SenderAddress)) - return AcceptTxResult.Invalid.WithMessage("Transaction sender is blacklisted"); + return Reject(tx, "sender"); if (IsBlackListed(spec, tx.To)) - return AcceptTxResult.Invalid.WithMessage("Transaction recipient is blacklisted"); + return Reject(tx, "recipient"); return AcceptTxResult.Accepted; } + private AcceptTxResult Reject(Transaction tx, string role) + { + if (_logger.IsDebug) _logger.Debug($"Skipped adding transaction {tx.ToString(" ")}, blacklisted {role}."); + return XdcAcceptTxResult.BlackListedAddress.WithMessage($"Transaction {role} is blacklisted"); + } + private static bool IsBlackListed(IXdcReleaseSpec spec, Address? address) => address is not null && spec.BlackListedAddresses.Contains(address); } diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs b/src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs deleted file mode 100644 index cd38163116b9..000000000000 --- a/src/Nethermind/Nethermind.Xdc/TxPool/CompositeIncomingTxFilter.cs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited -// SPDX-License-Identifier: LGPL-3.0-only - -using Nethermind.Core; -using Nethermind.TxPool; -using Nethermind.TxPool.Filters; - -namespace Nethermind.Xdc.TxPool; - -/// -/// Runs the given filters in order, returning the first non-accepting result. -/// -/// -/// The pool takes a single custom incoming filter, so XDC-specific filters are combined here. -/// -internal sealed class CompositeIncomingTxFilter(params IIncomingTxFilter[] filters) : IIncomingTxFilter -{ - public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandlingOptions txHandlingOptions) - { - foreach (IIncomingTxFilter filter in filters) - { - AcceptTxResult result = filter.Accept(tx, ref state, txHandlingOptions); - if (!result) - return result; - } - - return AcceptTxResult.Accepted; - } -} diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs new file mode 100644 index 000000000000..871ff28547e6 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.TxPool; + +namespace Nethermind.Xdc.TxPool; + +internal static class XdcAcceptTxResult +{ + // Ids outside the range used by AcceptTxResult's own values, which are compared by id. + // Deliberately not AcceptTxResult.Invalid: TxFloodController disconnects a peer that relays an + // Invalid transaction, and pool admission is judged against the local head, so two honest nodes + // one block apart across BlackListHFNumber would drop each other. + private const int BlackListedAddressId = 1000; + + public static AcceptTxResult BlackListedAddress { get; } = new(BlackListedAddressId, nameof(BlackListedAddress)); +} From b3a6d8ca710bb09a113a9ad1de5284690b19c8fd Mon Sep 17 00:00:00 2001 From: ak88 Date: Tue, 18 Aug 2026 22:50:24 +0200 Subject: [PATCH 4/6] Update src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs --- src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs index 871ff28547e6..44cd928e61e0 100644 --- a/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs +++ b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs @@ -7,10 +7,6 @@ namespace Nethermind.Xdc.TxPool; internal static class XdcAcceptTxResult { - // Ids outside the range used by AcceptTxResult's own values, which are compared by id. - // Deliberately not AcceptTxResult.Invalid: TxFloodController disconnects a peer that relays an - // Invalid transaction, and pool admission is judged against the local head, so two honest nodes - // one block apart across BlackListHFNumber would drop each other. private const int BlackListedAddressId = 1000; public static AcceptTxResult BlackListedAddress { get; } = new(BlackListedAddressId, nameof(BlackListedAddress)); From 720189d2007e209ce9ac5e6bab0636a9dedd1b78 Mon Sep 17 00:00:00 2001 From: ak88 Date: Tue, 18 Aug 2026 23:14:26 +0200 Subject: [PATCH 5/6] perf(xdc): preallocate blacklist rejection results The rejection message was interpolated per transaction on a peer-reachable path. Bake it into two statics instead, one per role, like every other pool filter's preallocated AcceptTxResult. Co-Authored-By: Claude Opus 5 --- .../BlackListedAddressFilterTests.cs | 13 ++++++++----- .../TxPool/BlackListedAddressFilter.cs | 10 +++++----- .../Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs | 4 +++- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs index 73375e3316e2..e981b9de86b2 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/BlackListedAddressFilterTests.cs @@ -62,14 +62,17 @@ public void Accept_ChecksSenderAndRecipient(bool blackListSender, bool blackList Assert.That((bool)Accept(filter, tx), Is.EqualTo(expectedAccepted)); } - [Test] - public void Accept_BlackListedAddress_DoesNotReturnInvalidSoPeerIsKept() + [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, BuildTx(BlackListed, TestItem.AddressC)); + AcceptTxResult result = Accept(filter, tx); - Assert.That(result, Is.EqualTo(XdcAcceptTxResult.BlackListedAddress)); + 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"); } @@ -119,7 +122,7 @@ public async Task SubmitTx_BlackListedAddress_IsRejectedOnPoolAdmission(bool bla Assert.That((bool)result, Is.EqualTo(expectedAccepted), result.ToString()); if (!expectedAccepted) - Assert.That(result, Is.EqualTo(XdcAcceptTxResult.BlackListedAddress)); + 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/TxPool/BlackListedAddressFilter.cs b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs index 50e265baa195..eb4c7a48f7df 100644 --- a/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs +++ b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs @@ -34,18 +34,18 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl return AcceptTxResult.Accepted; if (IsBlackListed(spec, tx.SenderAddress)) - return Reject(tx, "sender"); + return Reject(tx, XdcAcceptTxResult.BlackListedSender); if (IsBlackListed(spec, tx.To)) - return Reject(tx, "recipient"); + return Reject(tx, XdcAcceptTxResult.BlackListedRecipient); return AcceptTxResult.Accepted; } - private AcceptTxResult Reject(Transaction tx, string role) + private AcceptTxResult Reject(Transaction tx, AcceptTxResult result) { - if (_logger.IsDebug) _logger.Debug($"Skipped adding transaction {tx.ToString(" ")}, blacklisted {role}."); - return XdcAcceptTxResult.BlackListedAddress.WithMessage($"Transaction {role} is blacklisted"); + if (_logger.IsDebug) _logger.Debug($"Skipped adding transaction {tx.ToString(" ")}, {result}."); + return result; } private static bool IsBlackListed(IXdcReleaseSpec spec, Address? address) => diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs index 44cd928e61e0..ad7192d1a5ea 100644 --- a/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs +++ b/src/Nethermind/Nethermind.Xdc/TxPool/XdcAcceptTxResult.cs @@ -8,6 +8,8 @@ namespace Nethermind.Xdc.TxPool; internal static class XdcAcceptTxResult { private const int BlackListedAddressId = 1000; + private const string BlackListedAddressCode = "BlackListedAddress"; - public static AcceptTxResult BlackListedAddress { get; } = new(BlackListedAddressId, nameof(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"); } From a289e8f77514932523b16aca164306c6c0a78401 Mon Sep 17 00:00:00 2001 From: ak88 Date: Wed, 19 Aug 2026 08:52:18 +0200 Subject: [PATCH 6/6] Apply suggestions from code review Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> --- .../Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs index eb4c7a48f7df..5e7f4991208e 100644 --- a/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs +++ b/src/Nethermind/Nethermind.Xdc/TxPool/BlackListedAddressFilter.cs @@ -44,7 +44,7 @@ public AcceptTxResult Accept(Transaction tx, ref TxFilteringState state, TxHandl private AcceptTxResult Reject(Transaction tx, AcceptTxResult result) { - if (_logger.IsDebug) _logger.Debug($"Skipped adding transaction {tx.ToString(" ")}, {result}."); + if (_logger.IsTrace) _logger.Trace($"Skipped adding transaction {tx.ToString(" ")}, {result}."); return result; }