-
Notifications
You must be signed in to change notification settings - Fork 720
feat(xdc): read XDPoS rewards as XDC amounts #12852
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
173b2a4
84b8591
3a2dde6
f6ae705
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited | ||
| // SPDX-License-Identifier: LGPL-3.0-only | ||
|
|
||
| using System.IO; | ||
| using System.Text; | ||
| using Nethermind.Int256; | ||
| using Nethermind.Logging; | ||
| using Nethermind.Serialization.Json; | ||
| using Nethermind.Specs.ChainSpecStyle; | ||
| using Nethermind.Xdc.RPC; | ||
| using Nethermind.Xdc.Spec; | ||
| using NUnit.Framework; | ||
|
|
||
| namespace Nethermind.Xdc.Test; | ||
|
|
||
| /// <summary> | ||
| /// Covers reading XDPoS rewards, which the chainspec states in XDC, and the boundary that keeps | ||
| /// that unit convention inside <c>engine.XDPoS.params</c>. | ||
| /// </summary> | ||
| [TestFixture, Parallelizable(ParallelScope.All)] | ||
| public class XdcChainSpecRewardTests | ||
| { | ||
| /// <summary>The only Apothem v2 config that sets rewards.</summary> | ||
| private const ulong ApothemRewardSwitchRound = 27_360_000; | ||
|
|
||
| private const string RewardPlaceholder = "$REWARD$"; | ||
| private const string CoreParamsPlaceholder = "$CORE_PARAMS$"; | ||
| private const string EnginePeriodPlaceholder = "$PERIOD$"; | ||
|
|
||
| private const string ChainSpecTemplate = $$""" | ||
| { | ||
| "name": "xdc-reward-test", | ||
| "engine": { | ||
| "XDPoS": { | ||
| "params": { | ||
| "period": {{EnginePeriodPlaceholder}}, | ||
| "epoch": 900, | ||
| "masternodeReward": {{RewardPlaceholder}}, | ||
| "v2Configs": [ | ||
| { | ||
| "SwitchRound": 0, | ||
| "MasternodeReward": {{RewardPlaceholder}} | ||
| } | ||
| ] | ||
| } | ||
| } | ||
| }, | ||
| "params": { "chainId": 50{{CoreParamsPlaceholder}} } | ||
| } | ||
| """; | ||
|
|
||
| /// <remarks> | ||
| /// The three Apothem amounts are the ones that pin the conversion: the reference scales the | ||
| /// float64 through a 64-bit significand and truncates, so 63.42 XDC is 63420000000000001704 wei | ||
| /// rather than a round 6.342e19. Getting this wrong changes what validators are paid. | ||
| /// </remarks> | ||
| [TestCase("63.42", "63420000000000001704", TestName = "Masternode reward")] | ||
| [TestCase("50.27", "50270000000000003128", TestName = "Protector reward")] | ||
| [TestCase("25.13", "25129999999999999006", TestName = "Observer reward")] | ||
| [TestCase("6.342e1", "63420000000000001704", TestName = "Exponent notation is the same amount")] | ||
| [TestCase("1", "1000000000000000000", TestName = "Whole XDC")] | ||
| [TestCase("2.5", "2500000000000000000", TestName = "Exactly representable fraction")] | ||
| [TestCase("0.5", "500000000000000000", TestName = "Fraction below one")] | ||
| [TestCase("1e-18", "1", TestName = "One wei")] | ||
| [TestCase("1e-19", "0", TestName = "Below one wei truncates to zero")] | ||
| [TestCase("0", "0", TestName = "Zero")] | ||
| [TestCase("0.0", "0", TestName = "Zero with a fraction")] | ||
| public void Reward_stated_in_xdc_is_converted_to_wei(string literal, string expectedWei) | ||
| { | ||
| XdcChainSpecEngineParameters parameters = LoadEngineParameters(literal); | ||
|
|
||
| UInt256 wei = UInt256.Parse(expectedWei); | ||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(parameters.MasternodeReward, Is.EqualTo(wei)); | ||
| Assert.That(parameters.V2Configs[0].MasternodeReward, Is.EqualTo(wei)); | ||
| }); | ||
| } | ||
|
|
||
| [TestCase("-1", TestName = "Negative")] | ||
| [TestCase("-0.0", TestName = "Negative zero")] | ||
| [TestCase("1e60", TestName = "Beyond UInt256 once scaled")] | ||
| [TestCase("\"0x37f0e6c9e9dd0e0000\"", TestName = "Hex string, which would have to mean wei")] | ||
| [TestCase("\"63.42\"", TestName = "Quoted amount")] | ||
| public void Reward_that_is_not_an_xdc_amount_is_rejected(string literal) => | ||
| Assert.That(() => LoadEngineParameters(literal), Throws.TypeOf<InvalidDataException>()); | ||
|
|
||
| /// <summary> | ||
| /// The conversion is opt-in per property, so an XDPoS field that was not annotated keeps the | ||
| /// shared converter's behaviour. | ||
| /// </summary> | ||
| [Test] | ||
| public void Fractional_value_in_an_unannotated_engine_field_is_rejected() => | ||
| Assert.That(() => LoadEngineParameters("1", period: "2.0"), Throws.TypeOf<InvalidDataException>()); | ||
|
|
||
| /// <summary> | ||
| /// The converter is attached to properties owned by <c>Nethermind.Xdc</c>, so nothing outside the | ||
| /// XDPoS engine section gains either the XDC unit or tolerance for a fractional number. | ||
| /// </summary> | ||
| [TestCase(", \"eip150Transition\": 2.0", TestName = "Transition block")] | ||
| [TestCase(", \"terminalTotalDifficulty\": 1e18", TestName = "Terminal total difficulty")] | ||
| public void Fractional_value_outside_the_engine_section_is_rejected(string coreParams) => | ||
| Assert.That(() => LoadEngineParameters("1", coreParams: coreParams), Throws.TypeOf<InvalidDataException>()); | ||
|
|
||
| /// <summary> | ||
| /// Pins the shipped Apothem chainspec to the wei amounts it resolved to before the rewards were | ||
| /// restated in XDC. | ||
| /// </summary> | ||
| [Test] | ||
| public void Shipped_testnet_rewards_are_unchanged() | ||
| { | ||
| V2ConfigParams config = LoadShippedTestnetRewardConfig(); | ||
|
|
||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(config.MasternodeReward, Is.EqualTo(UInt256.Parse("63420000000000001704"))); | ||
| Assert.That(config.ProtectorReward, Is.EqualTo(UInt256.Parse("50270000000000003128"))); | ||
| Assert.That(config.ObserverReward, Is.EqualTo(UInt256.Parse("25129999999999999006"))); | ||
| }); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// <see cref="V2ConfigParams"/> doubles as the <c>XDPoS_networkInformation</c> response DTO, so | ||
| /// the reward properties are serialized as well as read. The response reports wei, unchanged by | ||
| /// the chainspec moving to XDC. | ||
| /// </summary> | ||
| [Test] | ||
| public void Network_information_response_reports_rewards_in_wei() | ||
| { | ||
| NetworkInformation response = new() | ||
| { | ||
| ConsensusConfigs = new XDPoSConfig { V2Configs = [LoadShippedTestnetRewardConfig()] } | ||
| }; | ||
|
|
||
| string json = new EthereumJsonSerializer().Serialize(response); | ||
|
|
||
| // The three rewards as the QUANTITY the endpoint has always emitted. | ||
| Assert.Multiple(() => | ||
| { | ||
| Assert.That(json, Does.Contain("0x3702119fc874606a8")); | ||
| Assert.That(json, Does.Contain("0x2b9a2eaa87ae30c38")); | ||
| Assert.That(json, Does.Contain("0x15cbfb1db0590fc1e")); | ||
| }); | ||
| } | ||
|
|
||
| private static V2ConfigParams LoadShippedTestnetRewardConfig() | ||
| { | ||
| ChainSpec chainSpec = new ChainSpecFileLoader(new EthereumJsonSerializer(), LimboLogs.Instance) | ||
| .LoadEmbeddedOrFromFile("chainspec/xdc-testnet.json"); | ||
|
|
||
| XdcChainSpecEngineParameters parameters = chainSpec.EngineChainSpecParametersProvider | ||
| .GetChainSpecParameters<XdcChainSpecEngineParameters>(); | ||
|
|
||
| foreach (V2ConfigParams config in parameters.V2Configs) | ||
| { | ||
| if (config.SwitchRound == ApothemRewardSwitchRound) return config; | ||
| } | ||
|
|
||
| throw new AssertionException($"No v2 config at switch round {ApothemRewardSwitchRound}"); | ||
| } | ||
|
|
||
| private static XdcChainSpecEngineParameters LoadEngineParameters(string reward, string coreParams = "", string period = "2") | ||
| { | ||
| string json = ChainSpecTemplate | ||
| .Replace(RewardPlaceholder, reward) | ||
| .Replace(CoreParamsPlaceholder, coreParams) | ||
| .Replace(EnginePeriodPlaceholder, period); | ||
|
|
||
| using MemoryStream stream = new(Encoding.UTF8.GetBytes(json)); | ||
| ChainSpec chainSpec = new ChainSpecLoader(new EthereumJsonSerializer(), LimboLogs.Instance).Load(stream); | ||
|
|
||
| return chainSpec.EngineChainSpecParametersProvider.GetChainSpecParameters<XdcChainSpecEngineParameters>(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |
| using System.Collections.Generic; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Runtime.InteropServices; | ||
| using System.Text.Json.Serialization; | ||
| using Nethermind.Core; | ||
| using Nethermind.Int256; | ||
| using Nethermind.Specs; | ||
|
|
@@ -68,8 +69,11 @@ public List<V2ConfigParams> V2Configs | |
|
|
||
| public ulong? TipUpgradePenalty { get; set; } | ||
| public ulong? TipUpgradeReward { get; set; } | ||
| [JsonConverter(typeof(XdcToWeiConverter))] | ||
| public UInt256 MasternodeReward { get; set; } | ||
| [JsonConverter(typeof(XdcToWeiConverter))] | ||
| public UInt256 ProtectorReward { get; set; } | ||
| [JsonConverter(typeof(XdcToWeiConverter))] | ||
| public UInt256 ObserverReward { get; set; } | ||
| public ulong MergeSignRange { get; set; } | ||
| public Address[] BlackListedAddresses { get; set; } | ||
|
|
@@ -122,8 +126,11 @@ public sealed class V2ConfigParams | |
| public int TimeoutSyncThreshold { get; init; } | ||
| public int TimeoutPeriod { get; init; } | ||
| public ulong MinePeriod { get; init; } | ||
| [JsonConverter(typeof(XdcToWeiConverter))] | ||
| public UInt256 MasternodeReward { get; init; } | ||
| [JsonConverter(typeof(XdcToWeiConverter))] | ||
| public UInt256 ProtectorReward { get; init; } | ||
| [JsonConverter(typeof(XdcToWeiConverter))] | ||
| public UInt256 ObserverReward { get; init; } | ||
|
Comment on lines
+129
to
134
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical — this breaks the
A property-level The PR description states "Nothing serializes engine parameters"; that is the assumption that doesn't hold here. Before this PR the endpoint emitted these three fields through the default
Either way this needs a regression test — |
||
| public ulong MinimumMinerBlockPerEpoch { get; init; } | ||
| public ulong LimitPenaltyEpoch { get; init; } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,147 @@ | ||||||||||||||||||||||
| // SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited | ||||||||||||||||||||||
| // SPDX-License-Identifier: LGPL-3.0-only | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| using System; | ||||||||||||||||||||||
| using System.Buffers; | ||||||||||||||||||||||
| using System.Diagnostics; | ||||||||||||||||||||||
| using System.Diagnostics.CodeAnalysis; | ||||||||||||||||||||||
| using System.Numerics; | ||||||||||||||||||||||
| using System.Text; | ||||||||||||||||||||||
| using System.Text.Json; | ||||||||||||||||||||||
| using System.Text.Json.Serialization; | ||||||||||||||||||||||
| using Nethermind.Int256; | ||||||||||||||||||||||
| using Nethermind.Serialization.Json; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| namespace Nethermind.Xdc.Spec; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary> | ||||||||||||||||||||||
| /// Reads a reward stated in XDC — <c>63.42</c>, <c>1</c>, <c>2.5e1</c> — and yields the amount in wei. | ||||||||||||||||||||||
| /// </summary> | ||||||||||||||||||||||
| /// <remarks> | ||||||||||||||||||||||
| /// The reference client decodes these fields into a float64 and scales them by 10^18 through a | ||||||||||||||||||||||
| /// <c>big.Float</c> whose precision defaults to 64 bits, then truncates toward zero. Both steps are | ||||||||||||||||||||||
| /// load-bearing: <c>63.42</c> has to produce 63420000000000001704 — the value Apothem's genesis was | ||||||||||||||||||||||
| /// generated with — and neither <see cref="double"/> nor <see cref="decimal"/> arithmetic lands | ||||||||||||||||||||||
| /// there. So the exact product is formed in <see cref="BigInteger"/> and rounded explicitly. | ||||||||||||||||||||||
| /// <para> | ||||||||||||||||||||||
| /// The 64 comes from Go raising a zero-precision <c>big.Float</c> to that width, which only survives | ||||||||||||||||||||||
| /// into the product while the receiver's precision is unset — the detail that would silently change | ||||||||||||||||||||||
| /// the result if the reference restructured that expression. <c>Reward</c>, the pre-<c>TIPUpgradeReward</c> | ||||||||||||||||||||||
| /// equivalent, is likewise a whole-XDC value scaled by <c>Unit.Ether</c> in <see cref="XdcRewardCalculator"/>. | ||||||||||||||||||||||
| /// </para> | ||||||||||||||||||||||
| /// <para> | ||||||||||||||||||||||
| /// Only a JSON number is accepted. A hex QUANTITY would have to mean wei, and carrying two units on | ||||||||||||||||||||||
| /// one field is how a reward ends up wrong by a factor of 10^18. | ||||||||||||||||||||||
| /// </para> | ||||||||||||||||||||||
| /// <para> | ||||||||||||||||||||||
| /// Applying this per property keeps the XDC unit convention inside <c>engine.XDPoS.params</c>. The | ||||||||||||||||||||||
| /// converters registered on <see cref="EthereumJsonSerializer"/> are untouched, so the rest of the | ||||||||||||||||||||||
| /// chainspec — and JSON-RPC, where EIP-1474 requires a QUANTITY — still rejects a fractional number. | ||||||||||||||||||||||
| /// </para> | ||||||||||||||||||||||
| /// </remarks> | ||||||||||||||||||||||
|
Comment on lines
+20
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The derivation checks out — I reproduced all three Apothem amounts by hand. Recording the working here since the PR asks for a second pair of eyes on it, and since it's the part no reviewer can check by reading the reference format alone. For each literal: take the exact
All three land exactly on the values Apothem's genesis carries. The two nearby hypotheses both fail, so the rule is pinned rather than merely consistent:
Note the sign structure is what makes this convincing: two amounts round up past the round value and one rounds down, and the offsets ( Two small things on the comment itself:
|
||||||||||||||||||||||
| public sealed class XdcToWeiConverter : JsonConverter<UInt256> | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| private const int WeiPerXdcExponent = 18; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary>Significand width the reference conversion rounds to.</summary> | ||||||||||||||||||||||
| private const int ReferencePrecisionBits = 64; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private const int UInt256ByteCount = 32; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static readonly UInt256Converter WeiConverter = new(); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| public override UInt256 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| if (reader.TokenType != JsonTokenType.Number) ThrowNotANumber(reader.TokenType); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| if (!reader.TryGetDouble(out double amount) || !double.IsFinite(amount) || double.IsNegative(amount)) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| ThrowNotAnAmount(in reader); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return ToWei(amount, in reader); | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [HIGH] Legacy wei-denominated XDC chainspecs are silently rescaled Before this PR, these UInt256 fields and the module documentation treated numeric values as wei. An existing custom XDPoS or subnet chainspec can therefore contain
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no production configs out there, so this is not a problem |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <remarks> | ||||||||||||||||||||||
| /// Writes wei rather than XDC. <see cref="V2ConfigParams"/> doubles as the | ||||||||||||||||||||||
| /// <c>XDPoS_networkInformation</c> response DTO, whose wire format must not change, and reporting | ||||||||||||||||||||||
| /// wei over JSON-RPC while configuring XDC in the chainspec is the split used everywhere else. | ||||||||||||||||||||||
| /// The consequence is that a <see cref="V2ConfigParams"/> does not survive a JSON round trip; | ||||||||||||||||||||||
| /// nothing performs one. | ||||||||||||||||||||||
| /// </remarks> | ||||||||||||||||||||||
| public override void Write(Utf8JsonWriter writer, UInt256 value, JsonSerializerOptions options) => | ||||||||||||||||||||||
| WeiConverter.Write(writer, value, options); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static UInt256 ToWei(double amount, in Utf8JsonReader reader) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| if (amount == 0) return default; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| (BigInteger significand, int exponent) = Decompose(amount); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // The amount is `significand * 2^exponent`, so its wei value is `numerator / 2^shift` exactly. | ||||||||||||||||||||||
| BigInteger numerator = significand * BigInteger.Pow(10, WeiPerXdcExponent); | ||||||||||||||||||||||
| int shift = -exponent; | ||||||||||||||||||||||
| if (shift < 0) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| numerator <<= -shift; | ||||||||||||||||||||||
| shift = 0; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // A value in [2^(e-1), 2^e) keeps bits down to 2^(e-64); `numerator`'s bit length minus the | ||||||||||||||||||||||
| // shift is that e, because the divisor is a power of two. | ||||||||||||||||||||||
| int ulpExponent = (int)numerator.GetBitLength() - shift - ReferencePrecisionBits; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| BigInteger rounded = ShiftRoundHalfEven(numerator, shift + ulpExponent); | ||||||||||||||||||||||
| BigInteger wei = ulpExponent >= 0 ? rounded << ulpExponent : rounded >> -ulpExponent; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| int byteCount = wei.GetByteCount(isUnsigned: true); | ||||||||||||||||||||||
| if (byteCount > UInt256ByteCount) ThrowTooLarge(in reader); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| Span<byte> bytes = stackalloc byte[UInt256ByteCount]; | ||||||||||||||||||||||
| wei.TryWriteBytes(bytes[(UInt256ByteCount - byteCount)..], out _, isUnsigned: true, isBigEndian: true); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| ReadOnlySpan<byte> bigEndian = bytes; | ||||||||||||||||||||||
| return new UInt256(in bigEndian, isBigEndian: true); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary>Splits a positive finite <paramref name="value"/> into <c>significand * 2^exponent</c>.</summary> | ||||||||||||||||||||||
| private static (BigInteger Significand, int Exponent) Decompose(double value) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| long bits = BitConverter.DoubleToInt64Bits(value); | ||||||||||||||||||||||
| int biasedExponent = (int)((bits >> 52) & 0x7FF); | ||||||||||||||||||||||
| long significand = bits & 0xF_FFFF_FFFF_FFFF; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return biasedExponent == 0 | ||||||||||||||||||||||
| ? (significand, -1074) | ||||||||||||||||||||||
| : (significand | (1L << 52), biasedExponent - 1075); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /// <summary>Divides by <c>2^shift</c>, rounding half to even; a negative shift multiplies instead.</summary> | ||||||||||||||||||||||
| private static BigInteger ShiftRoundHalfEven(BigInteger value, int shift) | ||||||||||||||||||||||
| { | ||||||||||||||||||||||
| if (shift <= 0) return value << -shift; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| BigInteger quotient = value >> shift; | ||||||||||||||||||||||
| BigInteger remainder = value - (quotient << shift); | ||||||||||||||||||||||
| int comparedToHalf = remainder.CompareTo(BigInteger.One << (shift - 1)); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return comparedToHalf > 0 || (comparedToHalf == 0 && !quotient.IsEven) | ||||||||||||||||||||||
| ? quotient + BigInteger.One | ||||||||||||||||||||||
| : quotient; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| private static string Literal(in Utf8JsonReader reader) => | ||||||||||||||||||||||
| Encoding.UTF8.GetString(reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| [DoesNotReturn, StackTraceHidden] | ||||||||||||||||||||||
| private static void ThrowNotANumber(JsonTokenType tokenType) => | ||||||||||||||||||||||
| throw new JsonException($"An XDC reward must be a JSON number, found {tokenType}"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| [DoesNotReturn, StackTraceHidden] | ||||||||||||||||||||||
| private static void ThrowNotAnAmount(in Utf8JsonReader reader) => | ||||||||||||||||||||||
| throw new JsonException($"'{Literal(in reader)}' is not an XDC amount"); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| [DoesNotReturn, StackTraceHidden] | ||||||||||||||||||||||
| private static void ThrowTooLarge(in Utf8JsonReader reader) => | ||||||||||||||||||||||
| throw new JsonException($"'{Literal(in reader)}' XDC exceeds the largest amount representable in wei"); | ||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Low — the message names the wrong ceiling, and nothing pins it.
Related: the only case exercising this guard is |
||||||||||||||||||||||
| } | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Medium — the rejection cases don't cover the one input the migration actually produces.
1e60only proves theUInt256ceiling works. The realistic bad input is an operator chainspec that still states the reward in wei — literally the value this PR deletes fromxdc-testnet.json:Today that case would fail, because
6.342e37wei fits in aUInt256and loads silently (see the comment onXdcToWeiConverter.cs). It's worth adding alongside whatever bound you settle on, so the test documents the migration boundary rather than just the arithmetic one.Two more gaps worth closing while you're here:
XDPoS_networkInformationbreakage got through. A test that serializes aV2ConfigParams(or the wholeNetworkInformationresponse) would have caught it.Shipped_testnet_rewards_are_unchangedreadsV2Configs[^1]with the comment "the newest config … sorts last by switch round". That's true, but the assertion is only meaningful because that entry happens to be the one carrying rewards. If a later config is appended without reward fields, this silently starts asserting three zeros against three non-zero expectations — it'd fail, so it's safe, just confusingly. Selecting bySwitchRoundwould say what the test means.