Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6254410
calculator
ak88 Nov 14, 2025
d95b2c0
Merge branch 'master' into feature/xdc-reward-handler
ak88 Nov 14, 2025
6dd5ba6
state reader
ak88 Nov 17, 2025
f7e2c2e
masternode voting contract
ak88 Nov 18, 2025
55190f7
fix
ak88 Nov 18, 2025
0f5adc3
abi json
ak88 Nov 19, 2025
6594334
working load test
ak88 Nov 19, 2025
a491b29
Merge branch 'master' into feature/xdc-state-reader
ak88 Nov 19, 2025
aab0908
test
ak88 Nov 19, 2025
e321be3
test
ak88 Nov 20, 2025
5861105
test getCandidates
ak88 Nov 21, 2025
c157ad0
cleanup
ak88 Nov 21, 2025
840b59d
format
ak88 Nov 21, 2025
7864c1b
remove var
ak88 Nov 21, 2025
1504dff
Merge branch 'master' into feature/xdc-state-reader
ak88 Nov 23, 2025
5b1fc8f
format
ak88 Nov 24, 2025
23a7c12
review comments
ak88 Nov 24, 2025
e847dfd
implement calculate rewards base flow and get signing txs
cicr99 Nov 24, 2025
fe9df29
implement rewards per signer and distribution calculations
cicr99 Nov 24, 2025
ffb5e9b
Merge branch 'feature/xdc-state-reader' into feature/xdc-reward-handler
cicr99 Nov 25, 2025
3536869
implement getCadidateOwner
cicr99 Nov 25, 2025
a350bcd
refactors and add comments to the code
cicr99 Nov 25, 2025
11347ba
add block signer contract address to the configuration spec
cicr99 Nov 26, 2025
f6ea634
implement reward module tests
cicr99 Dec 3, 2025
ba630e3
implement hook for reward calculation in hotstuff pipeline
cicr99 Dec 4, 2025
3a632bf
refactor and format
cicr99 Dec 4, 2025
30b0d61
Merge branch 'master' into feature/xdc-reward-handler
cicr99 Dec 4, 2025
a1d0ddd
format
cicr99 Dec 4, 2025
9a93a79
fix xdc test
cicr99 Dec 4, 2025
327724a
wire IRewardClaculator in ConfigureContainer in XdcTestBlockchain
cicr99 Dec 4, 2025
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
3 changes: 2 additions & 1 deletion src/Nethermind/Chains/xdc.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@
"eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"eip155Block": 3,
"eip158Block": 3,
"byzantiumBlock": 4
"byzantiumBlock": 4,
"blockSignerContract": "0x0000000000000000000000000000000000000089"
},
"genesis": {
"nonce": "0x0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Test.Builders;
using Nethermind.Xdc.Spec;

namespace Nethermind.Xdc.Test.Helpers;

public static class TransactionBuilderXdcExtensions
{
// function sign(uint256 _blockNumber, bytes32 _blockHash)
// selector = 0xe341eaa4
private static ReadOnlySpan<byte> SignSelector => new byte[] { 0xE3, 0x41, 0xEA, 0xA4 };

/// <summary>Sets 'To' to the XDC block-signer contract from the spec.</summary>
public static TransactionBuilder<Transaction> ToBlockSignerContract(
this TransactionBuilder<Transaction> b, IXdcReleaseSpec spec)
=> b.To(spec.BlockSignerContract);

/// <summary>
/// Appends ABI-encoded calldata for sign(uint256 _blockNumber, bytes32 _blockHash).
/// Calldata = 4-byte selector + 32-byte big-endian uint + 32-byte bytes32 (68 bytes total).
/// </summary>
public static TransactionBuilder<Transaction> WithXdcSigningData(
this TransactionBuilder<Transaction> b, long blockNumber, Hash256 blockHash)
=> b.WithData(CreateSigningCalldata(blockNumber, blockHash));

private static byte[] CreateSigningCalldata(long blockNumber, Hash256 blockHash)
{
Span<byte> data = stackalloc byte[68]; // 4 + 32 + 32

// 0..3: selector
SignSelector.CopyTo(data);

// 4..35: uint256 blockNumber (big-endian, right-aligned in 32 bytes)
var be = BitConverter.GetBytes((ulong)blockNumber);
if (BitConverter.IsLittleEndian) Array.Reverse(be);
// last 8 bytes of that 32 are the ulong
for (int i = 0; i < 8; i++) data[4 + 24 + i] = be[i];

// 36..67: bytes32 blockHash
blockHash.Bytes.CopyTo(data.Slice(36, 32));

return data.ToArray();
}
}
31 changes: 28 additions & 3 deletions src/Nethermind/Nethermind.Xdc.Test/Helpers/XdcTestBlockchain.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Nethermind.Consensus.Comparers;
using Nethermind.Consensus.Processing;
using Nethermind.Consensus.Producers;
using Nethermind.Consensus.Rewards;
using Nethermind.Core;
using Nethermind.Core.Specs;
using Nethermind.Core.Test.Blockchain;
Expand Down Expand Up @@ -55,7 +56,7 @@ public static async Task<XdcTestBlockchain> Create(int blocksToAdd = 3, bool use

if (testConfiguration.SuggestGenesisOnStart)
{
// The block added event is not waited by genesis, but its needed to wait here so that `AddBlock` wait correctly.
// The block added event is not waited by genesis, but it's needed to wait here so that `AddBlock` waits correctly.
Task newBlockWaiter = chain.BlockTree.WaitForNewBlock(chain.CancellationToken);
chain.MainProcessingContext.GenesisLoader.Load();
await newBlockWaiter;
Expand Down Expand Up @@ -173,7 +174,16 @@ protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder,
.AddSingleton<Configuration>()
.AddSingleton<FromContainer>()
.AddSingleton<FromXdcContainer>()
.AddScoped<IGenesisBuilder, XdcTestGenesisBuilder>()
.AddScoped<IGenesisBuilder>(ctx =>
new XdcTestGenesisBuilder(
ctx.Resolve<ISpecProvider>(),
ctx.Resolve<IWorldState>(),
ctx.Resolve<ISnapshotManager>(),
ctx.Resolve<IEnumerable<IGenesisPostProcessor>>().ToArray(),
ctx.Resolve<Configuration>(),
MasterNodeCandidates
)
)
.AddSingleton<IBlockProducer, TestXdcBlockProducer>()
.AddSingleton((ctx) => new CandidateContainer(MasterNodeCandidates))
.AddSingleton<ISigner>(ctx =>
Expand All @@ -185,6 +195,7 @@ protected override ContainerBuilder ConfigureContainer(ContainerBuilder builder,
})
.AddSingleton((_) => BlockProducer)
//.AddSingleton((_) => BlockProducerRunner)
.AddSingleton<IRewardCalculator, NullRewardCalculator>()
.AddSingleton<IBlockProducerRunner, XdcHotStuff>()
.AddSingleton<IProcessExitSource>(new ProcessExitSource(TestContext.CurrentContext.CancellationToken))

Expand Down Expand Up @@ -218,6 +229,7 @@ private IXdcReleaseSpec WrapReleaseSpec(IReleaseSpec spec)
xdcSpec.LimitPenaltyEpoch = 2;
xdcSpec.MinimumSigningTx = 1;
xdcSpec.GasLimitBoundDivisor = 1024;
xdcSpec.BlockSignerContract = new Address("0x0000000000000000000000000000000000000089");

V2ConfigParams[] v2ConfigParams = [
new V2ConfigParams {
Expand Down Expand Up @@ -263,6 +275,8 @@ private IXdcReleaseSpec WrapReleaseSpec(IReleaseSpec spec)
];

xdcSpec.V2Configs = v2ConfigParams.ToList();
xdcSpec.ValidateChainId = false;
xdcSpec.Reward = 5000;
return xdcSpec;
}

Expand Down Expand Up @@ -302,7 +316,8 @@ private class XdcTestGenesisBuilder(
IWorldState state,
ISnapshotManager snapshotManager,
IGenesisPostProcessor[] postProcessors,
Configuration testConfiguration
Configuration testConfiguration,
List<PrivateKey> masterNodeCandidates
) : IGenesisBuilder
{
public Block Build()
Expand All @@ -311,6 +326,11 @@ public Block Build()
state.CreateAccount(TestItem.AddressB, testConfiguration.AccountInitialValue);
state.CreateAccount(TestItem.AddressC, testConfiguration.AccountInitialValue);

foreach (PrivateKey candidate in masterNodeCandidates)
{
state.CreateAccount(candidate.Address, testConfiguration.AccountInitialValue);
}

IXdcReleaseSpec? finalSpec = (IXdcReleaseSpec)specProvider.GetFinalSpec();

XdcBlockHeaderBuilder xdcBlockHeaderBuilder = new();
Expand All @@ -335,6 +355,11 @@ public Block Build()
}
}

private class NullRewardCalculator : IRewardCalculator
{
public BlockReward[] CalculateRewards(Block block) => Array.Empty<BlockReward>();
}

public void ChangeReleaseSpec(Action<XdcReleaseSpec> reconfigure)
{
reconfigure((XdcReleaseSpec)SpecProvider.GetXdcSpec((XdcBlockHeader)BlockTree.Head!.Header));
Expand Down
235 changes: 235 additions & 0 deletions src/Nethermind/Nethermind.Xdc.Test/ModuleTests/RewardTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
// SPDX-FileCopyrightText: 2025 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System.Linq;
using System.Threading.Tasks;
using FluentAssertions;
using Nethermind.Consensus.Rewards;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Test.Builders;
using Nethermind.Crypto;
using Nethermind.Int256;
using Nethermind.Xdc.Contracts;
using Nethermind.Xdc.Spec;
using Nethermind.Xdc.Test.Helpers;
using Nethermind.Xdc.Types;
using NSubstitute;
using NUnit.Framework;

namespace Nethermind.Xdc.Test.ModuleTests;

public class RewardTests
{
[Test]
public async Task TestHookRewardV2()
{
var chain = await XdcTestBlockchain.Create();
var masternodeVotingContract = Substitute.For<IMasternodeVotingContract>();
var rc = new XdcRewardCalculator(
chain.EpochSwitchManager,
chain.SpecProvider,
chain.BlockTree,
masternodeVotingContract
);
var head = (XdcBlockHeader)chain.BlockTree.Head!.Header;
IXdcReleaseSpec spec = chain.SpecProvider.GetXdcSpec(head, chain.XdcContext.CurrentRound);
var epochLength = spec.EpochLength;

// Add blocks up to epochLength (E) + 15 and create a signing tx that will be inserted in the next block
await chain.AddBlocks(epochLength + 15 - 3);
var header915 = chain.BlockTree.Head!.Header as XdcBlockHeader;
Assert.That(header915, Is.Not.Null);
PrivateKey signer915 = GetSignerFromMasternodes(chain, header915, spec);
Address owner = signer915.Address;
masternodeVotingContract.GetCandidateOwner(Arg.Any<BlockHeader>(), signer915.Address).Returns(owner);
await chain.AddBlock(BuildSigningTx(
spec,
header915.Number,
header915.Hash ?? header915.CalculateHash().ToHash256(),
signer915));

// Add blocks up until 3E and evaluate rewards at this checkpoint
// Save block 3E - 15 for second part of the test
await chain.AddBlocks(2 * epochLength - 31);
var header2685 = chain.BlockTree.Head!.Header as XdcBlockHeader;
Assert.That(header2685, Is.Not.Null);
PrivateKey signer2685 = GetSignerFromMasternodes(chain, header2685, spec);
Address owner2 = signer2685.Address;
masternodeVotingContract.GetCandidateOwner(Arg.Any<BlockHeader>(), signer2685.Address).Returns(owner2);
// Continue adding blocks up until 3E
await chain.AddBlocks(15);
Block block2700 = chain.BlockTree.Head!;
var header2700 = block2700.Header as XdcBlockHeader;
Assert.That(header2700, Is.Not.Null);

BlockReward[] rewardsAt2700 = rc.CalculateRewards(block2700);

// Expect exactly 2 entries: one for the masternode owner and one for foundation
Address foundation = spec.FoundationWallet;
foundation.Should().NotBe(Address.Zero);

Assert.That(rewardsAt2700, Has.Length.EqualTo(2));
rewardsAt2700.Length.Should().Be(2);

UInt256 total = rewardsAt2700.Aggregate(UInt256.Zero, (acc, r) => acc + r.Value);
UInt256 ownerReward = rewardsAt2700.Single(r => r.Address == owner).Value;
UInt256 foundationReward = rewardsAt2700.Single(r => r.Address == foundation).Value;

// Check 90/10 split
Assert.That(foundationReward, Is.EqualTo(total / 10));
Assert.That(ownerReward, Is.EqualTo(total * 90 / 100));

// === Second part of the test: signing hash in a different epoch still counts ===

Transaction signingTx2 = BuildSigningTx(
spec,
header2685.Number,
header2685.Hash!,
signer2685);

// Place signingTx2 in block 3E + 16 (different epoch than the signed block)
await chain.AddBlocks(15);
await chain.AddBlock(signingTx2);

// Add blocks up until 4E and check rewards
await chain.AddBlocks(epochLength - 16);
Block block3600 = chain.BlockTree.Head!;
var header3600 = block3600.Header as XdcBlockHeader;
Assert.That(header3600, Is.Not.Null);
BlockReward[] rewardsAt3600 = rc.CalculateRewards(block3600);

// Same expectations: exactly two outputs with 90/10 split
// Since this only counts signing txs from 2E to 3E, only signingTx2 should get counted
Assert.That(rewardsAt3600, Has.Length.EqualTo(2));
UInt256 total2 = rewardsAt3600.Aggregate(UInt256.Zero, (acc, r) => acc + r.Value);
UInt256 ownerReward2 = rewardsAt3600.Single(r => r.Address == owner2).Value;
UInt256 foundationReward2 = rewardsAt3600.Single(r => r.Address == foundation).Value;

Assert.That(foundationReward2, Is.EqualTo(total2 / 10));
Assert.That(ownerReward2, Is.EqualTo(total2 * 90 / 100));

// === Third part of the test: if no signing tx, reward should be empty ===

// Add blocks up to 5E and check rewards
await chain.AddBlocks(epochLength);
Block block4500 = chain.BlockTree.Head!;
BlockReward[] rewardsAt4500 = rc.CalculateRewards(block4500);
// If no valid signing txs were counted for that epoch, expect no rewards.
rewardsAt4500.Should().BeEmpty();
}

[Test]
public async Task TestHookRewardV2SplitReward()
{
var chain = await XdcTestBlockchain.Create();
var masternodeVotingContract = Substitute.For<IMasternodeVotingContract>();
var rc = new XdcRewardCalculator(
chain.EpochSwitchManager,
chain.SpecProvider,
chain.BlockTree,
masternodeVotingContract
);

var head = (XdcBlockHeader)chain.BlockTree.Head!.Header;
IXdcReleaseSpec spec = chain.SpecProvider.GetXdcSpec(head, chain.XdcContext.CurrentRound);
var epochLength = spec.EpochLength;

// === Layout (mirrors Go test intent) ===
// - Insert 1 signing tx for header (E + 15) signed by signerA into block (E + 16)
// - Insert 2 signing txs (one for header (E + 15), one for header (2E - 15)) signed by signerB into block (2E - 1)
// - Verify: rewards at (3E) split 1:2 between A:B with 90/10 owner/foundation

// Move to block (E + 15)
await chain.AddBlocks(epochLength + 15 - 3);
var header915 = (XdcBlockHeader)chain.BlockTree.Head!.Header;

EpochSwitchInfo? epochInfo = chain.EpochSwitchManager.GetEpochSwitchInfo(header915);
Assert.That(epochInfo, Is.Not.Null);
PrivateKey[] masternodes = chain.TakeRandomMasterNodes(spec, epochInfo);
PrivateKey signerA = masternodes.First();
PrivateKey signerB = masternodes.Last();
Address ownerA = signerA.Address;
Address ownerB = signerB.Address;
masternodeVotingContract.GetCandidateOwner(Arg.Any<BlockHeader>(), signerA.Address).Returns(ownerA);
masternodeVotingContract.GetCandidateOwner(Arg.Any<BlockHeader>(), signerB.Address).Returns(ownerB);

// Insert 1 signing tx for header (E + 15) in block (E + 16)
Transaction txA = BuildSigningTx(
spec,
header915.Number,
header915.Hash ?? header915.CalculateHash().ToHash256(),
signerA);
await chain.AddBlock(txA); // advances to (E + 16)

// Move to block (2E - 15) to capture that header as well
var currentNumber = chain.BlockTree.Head!.Number;
await chain.AddBlocks(epochLength - 31);
var header1785 = (XdcBlockHeader)chain.BlockTree.Head!.Header;

// Prepare two signing txs signed by signerB:
// - for header (E + 15)
// - for header (2E - 15)
Transaction txB1 = BuildSigningTx(
spec,
header915.Number,
header915.Hash!,
signerB);

Transaction txB2 = BuildSigningTx(
spec,
header1785.Number,
header1785.Hash!,
signerB,
1);

// Advance to (2E - 2), then add a block with both signerB txs to be at (2E - 1)
await chain.AddBlocks(13);
await chain.AddBlock(txB1, txB2); // now at (2E - 1)

// Rewards at (3E) should exist with split 1:2 across A:B and 90/10 owner/foundation
await chain.AddBlocks(epochLength + 1); // from (2E - 1) to (3E)
Block block2700 = chain.BlockTree.Head!;
BlockReward[] rewards = rc.CalculateRewards(block2700);

Address foundation = spec.FoundationWallet;

// Expect exactly 3 entries: ownerA, ownerB, foundation.
Assert.That(rewards.Length, Is.EqualTo(3));

// Calculate exact rewards for each address
UInt256 totalRewards = (UInt256)spec.Reward * Unit.Ether;
UInt256 signerAReward = totalRewards / 3;
UInt256 ownerAReward = signerAReward * 90 / 100;
UInt256 foundationReward = signerAReward / 10;
UInt256 signerBReward = totalRewards * 2 / 3;
UInt256 ownerBReward = signerBReward * 90 / 100;
foundationReward += signerBReward / 10;

foreach (BlockReward reward in rewards)
{
if (reward.Address == ownerA) Assert.That(reward.Value, Is.EqualTo(ownerAReward));
if (reward.Address == ownerB) Assert.That(reward.Value, Is.EqualTo(ownerBReward));
if (reward.Address == foundation) Assert.That(reward.Value, Is.EqualTo(foundationReward));
}
}

private static Transaction BuildSigningTx(IXdcReleaseSpec spec, long blockNumber, Hash256 blockHash, PrivateKey signer, long nonce = 0)
{
return Build.A.Transaction
.WithChainId(0)
.WithNonce((UInt256)nonce)
.WithGasLimit(200000)
.WithXdcSigningData(blockNumber, blockHash)
.ToBlockSignerContract(spec)
.SignedAndResolved(signer)
.TestObject;
}

private static PrivateKey GetSignerFromMasternodes(XdcTestBlockchain chain, XdcBlockHeader header, IXdcReleaseSpec spec)
{
EpochSwitchInfo? epochInfo = chain.EpochSwitchManager.GetEpochSwitchInfo(header);
Assert.That(epochInfo, Is.Not.Null);
return chain.TakeRandomMasterNodes(spec, epochInfo).First();
}
}
Loading