diff --git a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs index fa9bdc05a386..08c73ddd43e8 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcBlockHeaderBuilder.cs @@ -29,7 +29,7 @@ public XdcBlockHeaderBuilder() Address.Zero, UInt256.One, 1, - 30_000_000, + XdcConstants.TargetGasLimit, 1_700_000_000, new byte[] { 1, 2, 3 }) { diff --git a/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestHelper.cs b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestHelper.cs new file mode 100644 index 000000000000..1455135ebba1 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestHelper.cs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Core.Crypto; +using Nethermind.Crypto; +using Nethermind.Serialization.Rlp; +using Nethermind.Xdc.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Nethermind.Xdc.Test; +internal class XdcTestHelper +{ + public static PrivateKey[] GeneratePrivateKeys(int count) + { + var keyBuilder = new PrivateKeyGenerator(); + return keyBuilder.Generate(count).ToArray(); + } + + public static QuorumCertificate CreateQc(BlockRoundInfo roundInfo, ulong gapNumber, PrivateKey[] keys) + { + EthereumEcdsa ecdsa = new EthereumEcdsa(0); + var qcEncoder = new VoteDecoder(); + + IEnumerable signatures = CreateVoteSignatures(roundInfo, gapNumber, keys); + + return new QuorumCertificate(roundInfo, signatures.ToArray(), gapNumber); + } + + public static Signature[] CreateVoteSignatures(BlockRoundInfo roundInfo, ulong gapnumber, PrivateKey[] keys) + { + EthereumEcdsa ecdsa = new EthereumEcdsa(0); + var encoder = new VoteDecoder(); + IEnumerable signatures = keys.Select(k => + { + var stream = new KeccakRlpStream(); + encoder.Encode(stream, new Vote(roundInfo, gapnumber), RlpBehaviors.ForSealing); + return ecdsa.Sign(k, stream.GetValueHash()); + }).ToArray(); + return signatures.ToArray(); + } +} diff --git a/src/Nethermind/Nethermind.Xdc.Test/QuorumCertificateManagerTest.cs b/src/Nethermind/Nethermind.Xdc.Test/QuorumCertificateManagerTest.cs index 920430a35dce..05fe1d05c2c9 100644 --- a/src/Nethermind/Nethermind.Xdc.Test/QuorumCertificateManagerTest.cs +++ b/src/Nethermind/Nethermind.Xdc.Test/QuorumCertificateManagerTest.cs @@ -55,25 +55,25 @@ public static IEnumerable QcCases() //Base valid control case PrivateKey[] keys = keyBuilder.Generate(20).ToArray(); IEnumerable
masterNodes = keys.Select(k => k.Address); - yield return new TestCaseData(CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 0, keys), headerBuilder, keys.Select(k => k.Address), true); + yield return new TestCaseData(XdcTestHelper.CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 0, keys), headerBuilder, keys.Select(k => k.Address), true); //Not enough signatures - yield return new TestCaseData(CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 0, keys.Take(13).ToArray()), headerBuilder, keys.Select(k => k.Address), false); + yield return new TestCaseData(XdcTestHelper.CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 0, keys.Take(13).ToArray()), headerBuilder, keys.Select(k => k.Address), false); //1 Vote is not master node - yield return new TestCaseData(CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 0, keys), headerBuilder, keys.Skip(1).Select(k => k.Address), false); + yield return new TestCaseData(XdcTestHelper.CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 0, keys), headerBuilder, keys.Skip(1).Select(k => k.Address), false); //Wrong gap number - yield return new TestCaseData(CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 1, keys), headerBuilder, masterNodes, false); + yield return new TestCaseData(XdcTestHelper.CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 1), 1, keys), headerBuilder, masterNodes, false); //Wrong block number in QC - yield return new TestCaseData(CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 2), 0, keys), headerBuilder, masterNodes, false); + yield return new TestCaseData(XdcTestHelper.CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 1, 2), 0, keys), headerBuilder, masterNodes, false); //Wrong hash in QC - yield return new TestCaseData(CreateQc(new BlockRoundInfo(Hash256.Zero, 1, 1), 0, keys), headerBuilder, masterNodes, false); + yield return new TestCaseData(XdcTestHelper.CreateQc(new BlockRoundInfo(Hash256.Zero, 1, 1), 0, keys), headerBuilder, masterNodes, false); //Wrong round number in QC - yield return new TestCaseData(CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 0, 1), 0, keys), headerBuilder, masterNodes, false); + yield return new TestCaseData(XdcTestHelper.CreateQc(new BlockRoundInfo(headerBuilder.TestObject.Hash!, 0, 1), 0, keys), headerBuilder, masterNodes, false); } [TestCaseSource(nameof(QcCases))] @@ -101,27 +101,4 @@ public void VerifyCertificate_QcWithDifferentParameters_ReturnsExpected(QuorumCe Assert.That(quorumCertificateManager.VerifyCertificate(quorumCert, xdcBlockHeaderBuilder.TestObject, out _), Is.EqualTo(expected)); } - - private static QuorumCertificate CreateQc(BlockRoundInfo roundInfo, ulong gapNumber, PrivateKey[] keys) - { - EthereumEcdsa ecdsa = new EthereumEcdsa(0); - var qcEncoder = new VoteDecoder(); - - IEnumerable signatures = CreateVoteSignatures(roundInfo, gapNumber, keys); - - return new QuorumCertificate(roundInfo, signatures.ToArray(), gapNumber); - } - - private static Signature[] CreateVoteSignatures(BlockRoundInfo roundInfo, ulong gapnumber, PrivateKey[] keys) - { - EthereumEcdsa ecdsa = new EthereumEcdsa(0); - var encoder = new VoteDecoder(); - IEnumerable signatures = keys.Select(k => - { - var stream = new KeccakRlpStream(); - encoder.Encode(stream, new Vote(roundInfo, gapnumber), RlpBehaviors.ForSealing); - return ecdsa.Sign(k, stream.GetValueHash()); - }).ToArray(); - return signatures.ToArray(); - } } diff --git a/src/Nethermind/Nethermind.Xdc.Test/XdcBlockProducerTest.cs b/src/Nethermind/Nethermind.Xdc.Test/XdcBlockProducerTest.cs new file mode 100644 index 000000000000..14abbf3656b4 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc.Test/XdcBlockProducerTest.cs @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Blockchain; +using Nethermind.Config; +using Nethermind.Consensus; +using Nethermind.Consensus.Processing; +using Nethermind.Consensus.Transactions; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Core.Test.Builders; +using Nethermind.Crypto; +using Nethermind.Evm.State; +using Nethermind.Evm.Tracing; +using Nethermind.Logging; +using Nethermind.Xdc.Spec; +using NSubstitute; +using NUnit.Framework; +using System.Linq; +using System.Threading.Tasks; + +namespace Nethermind.Xdc.Test; +internal class XdcBlockProducerTest +{ + [Test] + public async Task BuildBlock_HasCorrectQC_ProducesValidHeader() + { + ISpecProvider specProvider = Substitute.For(); + IXdcReleaseSpec xdcReleaseSpec = Substitute.For(); + xdcReleaseSpec.MinePeriod.Returns(2); + xdcReleaseSpec.EpochLength.Returns(900); + xdcReleaseSpec.GasLimitBoundDivisor.Returns(1); + specProvider.GetSpec(Arg.Any()).Returns(xdcReleaseSpec); + var epochManager = Substitute.For(); + IWorldState stateProvider = Substitute.For(); + stateProvider.HasStateForBlock(Arg.Any()).Returns(true); + + PrivateKey[] masterNodes = XdcTestHelper.GeneratePrivateKeys(108); + epochManager + .GetEpochSwitchInfo(Arg.Any()) + .Returns(new Types.EpochSwitchInfo(masterNodes.Select(m => m.Address).ToArray(), [], [], new Types.BlockRoundInfo(Hash256.Zero, 0, 0))); + + ISealer sealer = new XdcSealer(new Signer(0, new ProtectedPrivateKey(masterNodes[1], ""), NullLogManager.Instance)); + + XdcBlockHeader parent = Build.A.XdcBlockHeader().TestObject; + + var xdcContext = new XdcContext(); + xdcContext.CurrentRound = 1; + xdcContext.HighestQC = XdcTestHelper.CreateQc(new Types.BlockRoundInfo(parent.Hash!, 0, parent.Number), 0, masterNodes); + + IBlockchainProcessor processor = Substitute.For(); + processor.Process(Arg.Any(), Arg.Any(), Arg.Any()).Returns(args => args.ArgAt(0)); + + XdcBlockProducer producer = new XdcBlockProducer( + epochManager, + Substitute.For(), + xdcContext, + Substitute.For(), + processor, + sealer, + Substitute.For(), + stateProvider, + Substitute.For(), + Substitute.For(), + specProvider, + Substitute.For(), + Substitute.For(), + Substitute.For()); + XdcHeaderValidator headerValidator = new XdcHeaderValidator(Substitute.For(), new XdcSealValidator(Substitute.For(), epochManager, specProvider), specProvider, NullLogManager.Instance); + + Block? block = await producer.BuildBlock(parent); + + Assert.That(block, Is.Not.Null); + bool actual = headerValidator.Validate(block.Header, parent, false, out string? error); + Assert.That(actual, Is.True); + } +} diff --git a/src/Nethermind/Nethermind.Xdc/XdcBlockProducer.cs b/src/Nethermind/Nethermind.Xdc/XdcBlockProducer.cs new file mode 100644 index 000000000000..eefdcb9f9be1 --- /dev/null +++ b/src/Nethermind/Nethermind.Xdc/XdcBlockProducer.cs @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited +// SPDX-License-Identifier: LGPL-3.0-only + +using Nethermind.Blockchain; +using Nethermind.Config; +using Nethermind.Consensus; +using Nethermind.Consensus.Processing; +using Nethermind.Consensus.Producers; +using Nethermind.Consensus.Transactions; +using Nethermind.Core; +using Nethermind.Core.Crypto; +using Nethermind.Core.Specs; +using Nethermind.Evm.State; +using Nethermind.Evm.Tracing; +using Nethermind.Int256; +using Nethermind.Logging; +using Nethermind.Xdc.RLP; +using Nethermind.Xdc.Spec; +using Nethermind.Xdc.Types; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Nethermind.Xdc; +internal class XdcBlockProducer : BlockProducerBase +{ + private readonly IEpochSwitchManager epochSwitchManager; + private readonly ISnapshotManager snapshotManager; + private readonly XdcContext xdcContext; + private readonly ISealer sealer; + private readonly ISpecProvider specProvider; + private readonly ILogManager logManager; + private static readonly ExtraConsensusDataDecoder _extraConsensusDataDecoder = new(); + + public XdcBlockProducer(IEpochSwitchManager epochSwitchManager, ISnapshotManager snapshotManager, XdcContext xdcContext, ITxSource txSource, IBlockchainProcessor processor, ISealer sealer, IBlockTree blockTree, IWorldState stateProvider, IGasLimitCalculator? gasLimitCalculator, ITimestamper? timestamper, ISpecProvider specProvider, ILogManager logManager, IDifficultyCalculator? difficultyCalculator, IBlocksConfig? blocksConfig) : base(txSource, processor, sealer, blockTree, stateProvider, gasLimitCalculator, timestamper, specProvider, logManager, difficultyCalculator, blocksConfig) + { + this.epochSwitchManager = epochSwitchManager; + this.snapshotManager = snapshotManager; + this.xdcContext = xdcContext; + this.sealer = sealer; + this.specProvider = specProvider; + this.logManager = logManager; + } + + protected override BlockHeader PrepareBlockHeader(BlockHeader parent, PayloadAttributes payloadAttributes) + { + if (parent is not XdcBlockHeader xdcParent) + throw new ArgumentException("Only XDC header are supported."); + + QuorumCertificate highestCert = xdcContext.HighestQC; + var currentRound = xdcContext.CurrentRound; + + //TODO maybe some sanity checks here for round and hash + + byte[] extra = [XdcConstants.ConsensusVersion, .. _extraConsensusDataDecoder.Encode(new ExtraFieldsV2(currentRound, highestCert)).Bytes]; + + Address blockAuthor = sealer.Address; + XdcBlockHeader xdcBlockHeader = new( + parent.Hash!, + Keccak.OfAnEmptySequenceRlp, + blockAuthor, + UInt256.Zero, + parent.Number + 1, + //This should probably use TargetAdjustedGasLimitCalculator + XdcConstants.TargetGasLimit, + 0, + extra) + { + Author = blockAuthor, + }; + + IXdcReleaseSpec spec = specProvider.GetXdcSpec(xdcBlockHeader, currentRound); + + xdcBlockHeader.Timestamp = payloadAttributes?.Timestamp ?? parent.Timestamp + (ulong)spec.MinePeriod; + + xdcBlockHeader.Difficulty = 1; + xdcBlockHeader.TotalDifficulty = 1; + + xdcBlockHeader.BaseFeePerGas = BaseFeeCalculator.Calculate(parent, spec); + + xdcBlockHeader.MixHash = Hash256.Zero; + + if (epochSwitchManager.IsEpochSwitchAtBlock(xdcBlockHeader)) + { + (Address[] masternodes, Address[] penalties) = snapshotManager.CalculateNextEpochMasternodes(xdcBlockHeader, spec); + + xdcBlockHeader.Validators = new byte[masternodes.Length * Address.Size]; + + for (int i = 0; i < masternodes.Length; i++) + { + Array.Copy(masternodes[i].Bytes, 0, xdcBlockHeader.Validators, i * Address.Size, Address.Size); + } + + xdcBlockHeader.Penalties = new byte[penalties.Length * Address.Size]; + + for (int i = 0; i < penalties.Length; i++) + { + Array.Copy(penalties[i].Bytes, 0, xdcBlockHeader.Penalties, i * Address.Size, Address.Size); + } + } + return xdcBlockHeader; + } +} diff --git a/src/Nethermind/Nethermind.Xdc/XdcConstants.cs b/src/Nethermind/Nethermind.Xdc/XdcConstants.cs index bb2f201b12ec..b225e63e4a30 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcConstants.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcConstants.cs @@ -25,8 +25,12 @@ internal static class XdcConstants public const int InMemoryRound2Epochs = 65536; // One epoch ~ 0.5h, 65536 epochs ~ 3.7y, ~10MB memory + public const long TargetGasLimit = 84000000; // XDC default gas limit per block + public const byte ConsensusVersion = 0x02; + public const int GasLimitBoundDivisor = 1024; // The bound divisor of gas limit adjustment per block + // --- Compile-time constants --- public const int InMemorySnapshots = 128; // Number of recent vote snapshots to keep in memory public const int BlockSignersCacheLimit = 9000; diff --git a/src/Nethermind/Nethermind.Xdc/XdcHeaderValidator.cs b/src/Nethermind/Nethermind.Xdc/XdcHeaderValidator.cs index a27f13914255..d5ed9f1c5cc8 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcHeaderValidator.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcHeaderValidator.cs @@ -66,7 +66,7 @@ protected override bool Validate(BlockHeader header, BlockHeader? par protected override bool ValidateSeal(BlockHeader header, BlockHeader parent, bool isUncle, ref string? error) { if (_sealValidator is XdcSealValidator xdcSealValidator) - return xdcSealValidator.ValidateParams(header, parent, out error); + return xdcSealValidator.ValidateParams(parent, header, out error); if (!_sealValidator.ValidateParams(parent, header, isUncle)) { diff --git a/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs b/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs index b43bfdba5927..629b60addfab 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs @@ -89,12 +89,14 @@ public bool ValidateParams(BlockHeader parent, BlockHeader header, out string er } else { - if (xdcHeader.Validators?.Length != 0) + if (xdcHeader.Validators is not null && + xdcHeader.Validators.Length != 0) { error = "Validators are not empty in non-epoch switch header."; return false; } - if (xdcHeader.Penalties?.Length != 0) + if (xdcHeader.Penalties is not null && + xdcHeader.Penalties?.Length != 0) { error = "Penalties are not empty in non-epoch switch header."; return false; diff --git a/src/Nethermind/Nethermind.Xdc/XdcSealer.cs b/src/Nethermind/Nethermind.Xdc/XdcSealer.cs index a08026a7dd84..49ce730e2459 100644 --- a/src/Nethermind/Nethermind.Xdc/XdcSealer.cs +++ b/src/Nethermind/Nethermind.Xdc/XdcSealer.cs @@ -34,6 +34,8 @@ public Task SealBlock(Block block, CancellationToken cancellationToken) KeccakRlpStream hashStream = new KeccakRlpStream(); _xdcHeaderDecoder.Encode(hashStream, xdcBlockHeader, RlpBehaviors.ForSealing); xdcBlockHeader.Validator = signer.Sign(hashStream.GetValueHash()).BytesWithRecovery; + + xdcBlockHeader.Hash = xdcBlockHeader.CalculateHash().ToHash256(); return Task.FromResult(block); } }