Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
{
Expand Down
45 changes: 45 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestHelper.cs
Original file line number Diff line number Diff line change
@@ -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<Signature> 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<Signature> 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();
}
}
37 changes: 7 additions & 30 deletions src/Nethermind/Nethermind.Xdc.Test/QuorumCertificateManagerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,25 @@ public static IEnumerable<TestCaseData> QcCases()
//Base valid control case
PrivateKey[] keys = keyBuilder.Generate(20).ToArray();
IEnumerable<Address> 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))]
Expand Down Expand Up @@ -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<Signature> 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<Signature> 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();
}
}
78 changes: 78 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/XdcBlockProducerTest.cs
Original file line number Diff line number Diff line change
@@ -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<ISpecProvider>();
IXdcReleaseSpec xdcReleaseSpec = Substitute.For<IXdcReleaseSpec>();
xdcReleaseSpec.MinePeriod.Returns(2);
xdcReleaseSpec.EpochLength.Returns(900);
xdcReleaseSpec.GasLimitBoundDivisor.Returns(1);
specProvider.GetSpec(Arg.Any<ForkActivation>()).Returns(xdcReleaseSpec);
var epochManager = Substitute.For<IEpochSwitchManager>();
IWorldState stateProvider = Substitute.For<IWorldState>();
stateProvider.HasStateForBlock(Arg.Any<BlockHeader>()).Returns(true);

PrivateKey[] masterNodes = XdcTestHelper.GeneratePrivateKeys(108);
epochManager
.GetEpochSwitchInfo(Arg.Any<XdcBlockHeader>())
.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<IBlockchainProcessor>();
processor.Process(Arg.Any<Block>(), Arg.Any<ProcessingOptions>(), Arg.Any<IBlockTracer>()).Returns(args => args.ArgAt<Block>(0));

XdcBlockProducer producer = new XdcBlockProducer(
epochManager,
Substitute.For<ISnapshotManager>(),
xdcContext,
Substitute.For<ITxSource>(),
processor,
sealer,
Substitute.For<IBlockTree>(),
stateProvider,
Substitute.For<IGasLimitCalculator>(),
Substitute.For<ITimestamper>(),
specProvider,
Substitute.For<ILogManager>(),
Substitute.For<IDifficultyCalculator>(),
Substitute.For<IBlocksConfig>());
XdcHeaderValidator headerValidator = new XdcHeaderValidator(Substitute.For<IBlockTree>(), new XdcSealValidator(Substitute.For<ISnapshotManager>(), 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);
}
}
106 changes: 106 additions & 0 deletions src/Nethermind/Nethermind.Xdc/XdcBlockProducer.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
4 changes: 4 additions & 0 deletions src/Nethermind/Nethermind.Xdc/XdcConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion src/Nethermind/Nethermind.Xdc/XdcHeaderValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ protected override bool Validate<TOrphaned>(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))
{
Expand Down
6 changes: 4 additions & 2 deletions src/Nethermind/Nethermind.Xdc/XdcSealValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/Nethermind/Nethermind.Xdc/XdcSealer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public Task<Block> 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);
}
}