diff --git a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs index 79b96eff3bd..5083dd4e1aa 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs @@ -111,6 +111,12 @@ public XdcBlockHeaderBuilder WithExtraConsensusData(ExtraFieldsV2 extraFieldsV2) return this; } + public new XdcBlockHeaderBuilder WithTimestamp(ulong timestamp) + { + TestObjectInternal.Timestamp = timestamp; + return this; + } + public XdcBlockHeaderBuilder WithValidator(Signature signature) { XdcTestObjectInternal.Validator = signature.Bytes.ToArray(); diff --git a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs index dc3197d911e..2f5e2c3b3f3 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs @@ -171,7 +171,7 @@ protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder, .AddSingleton((_) => BlockProducer) //.AddSingleton((_) => BlockProducerRunner) .AddSingleton() - .AddSingleton((_) => Timestamper) + .AddSingleton((ctx) => ctx.Resolve()) .AddSingleton() .AddSingleton((ctx) => diff --git a/src/Nethermind/Nethermind.Xdc.Test/SignTransactionManagerTests.cs b/src/Nethermind/Nethermind.Xdc.Test/SignTransactionManagerTests.cs new file mode 100644 index 00000000000..bc7f3fa2733 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc.Test/SignTransactionManagerTests.cs @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using System; +using Nethermind.Blockchain; +using Nethermind.Consensus; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.Logging; +using Nethermind.TxPool; +using Nethermind.Xdc.Spec; +using Nethermind.Xdc.Types; +using NSubstitute; +using NUnit.Framework; + +namespace Nethermind.Xdc.Test; + +[Parallelizable(ParallelScope.All)] +internal class SignTransactionManagerTests +{ + // window = MergeSignRange(15) * MinePeriod(2) * MaxSignableBlockPeriods(2) = 60s. + [TestCase(0L, true)] + [TestCase(60L, true)] + [TestCase(61L, false)] + [TestCase(86_400L, false)] + public void OnBlockAddedToMain_SignsOnlyRecentHeadBlocks(long secondsBehind, bool shouldSign) + { + IXdcReleaseSpec spec = Substitute.For(); + spec.MergeSignRange.Returns(15); + spec.MinePeriod.Returns(2); + + ISpecProvider specProvider = Substitute.For(); + specProvider.GetSpec(Arg.Any()).Returns(spec); + + ManualTimestamper timestamper = new(); + + XdcBlockHeader header = Build.A.XdcBlockHeader() + .WithNumber(spec.MergeSignRange) + .WithTimestamp((ulong)(timestamper.UnixTime.SecondsLong - secondsBehind)) + .WithExtraConsensusData(new ExtraFieldsV2(1, new QuorumCertificate(new BlockRoundInfo(Hash256.Zero, 0, 0), null, 0))) + .TestObject; + Block block = new(header); + + ISigner signer = Substitute.For(); + signer.Address.Returns(TestItem.AddressA); + signer.TrySign(Arg.Any()).Returns(true); + + ITxPool txPool = Substitute.For(); + txPool.SubmitTx(Arg.Any(), Arg.Any()).Returns(AcceptTxResult.Accepted); + + IBlockTree blockTree = Substitute.For(); + blockTree.WasProcessed(block.Number, block.Hash!).Returns(true); + blockTree.FindBestSuggestedHeader().Returns(header); // bestSuggested == head => not syncing + blockTree.Head.Returns(block); + + ISnapshotManager snapshotManager = Substitute.For(); + snapshotManager.GetSnapshotByBlockNumber(Arg.Any(), Arg.Any()) + .Returns(new Snapshot(block.Number, TestItem.KeccakA, [TestItem.AddressA])); + + SignTransactionManager manager = new( + new Lazy(() => signer), + new Lazy(() => txPool), + blockTree, snapshotManager, specProvider, timestamper, LimboLogs.Instance); + manager.Start(); + + blockTree.BlockAddedToMain += Raise.EventWith(new BlockReplacementEventArgs(block)); + + txPool.Received(shouldSign ? 1 : 0).SubmitTx(Arg.Any(), Arg.Any()); + } +} diff --git a/src/Nethermind/Nethermind.Xdc/SignTransactionManager.cs b/src/Nethermind/Nethermind.Xdc/SignTransactionManager.cs index d7184e29005..6fe198c7bc7 100644 --- a/src/Nethermind/Nethermind.Xdc/SignTransactionManager.cs +++ b/src/Nethermind/Nethermind.Xdc/SignTransactionManager.cs @@ -26,6 +26,7 @@ internal class SignTransactionManager( IBlockTree blockTree, ISnapshotManager snapshotManager, ISpecProvider specProvider, + ITimestamper timestamper, ILogManager logManager) : ISignTransactionManager, IStartable, IDisposable { // Lazy: ISigner and ITxPool are registered during InitializeBlockchain, after this class is instantiated. @@ -34,6 +35,7 @@ internal class SignTransactionManager( private readonly IBlockTree _blockTree = blockTree; private readonly ISnapshotManager _snapshotManager = snapshotManager; private readonly ISpecProvider _specProvider = specProvider; + private readonly ITimestamper _timestamper = timestamper; private readonly ILogger _logger = logManager.GetClassLogger(); private readonly AssociativeKeyCache _alreadySigned = new(128); @@ -76,6 +78,11 @@ private void OnBlockAddedToMain(object? sender, BlockReplacementEventArgs e) if (spec is null) return; + // Sign only recent head blocks; older ones are replayed during catch-up. + long window = spec.MergeSignRange * spec.MinePeriod * XdcConstants.MaxSignableBlockPeriods; + if ((long)xdcHeader.Timestamp + window < _timestamper.UnixTime.SecondsLong) + return; + if (xdcHeader.Number % spec.MergeSignRange != 0) return; diff --git a/src/Nethermind/Nethermind.Xdc/XdcConstants.cs b/src/Nethermind/Nethermind.Xdc/XdcConstants.cs index 0d21f802e2d..7b8ff33af57 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcConstants.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcConstants.cs @@ -60,4 +60,7 @@ internal static class XdcConstants // 4-byte selector + 32-byte block number + 32-byte block hash public const int SignTransactionDataLength = 68; + + // Only sign recent head blocks. + public const int MaxSignableBlockPeriods = 2; }