Enforce strict leading-zero validation for QUANTITY hex in BlockOverrides#12103
Enforce strict leading-zero validation for QUANTITY hex in BlockOverrides#12103manusw7 wants to merge 3 commits into
Conversation
EIP-1474 requires that integer hex strings (QUANTITY type) have no leading zero digits. `BlockOverrides` fields (`number`, `time`, `gasLimit`, `baseFeePerGas`, `blobBaseFee`) silently accepted values like `"0x0b"` instead of rejecting with -32602. - Add `JsonRpcQuantityFormat.StrictMode` (Nethermind.Core) — shared flag kept in sync with `EthereumJsonSerializer.StrictHexFormat`. - Add `NullableQuantityUInt256Converter` (Nethermind.Core.JsonConverters) — QUANTITY-aware UInt256? converter that rejects leading zeros in strict mode. Annotated on `BaseFeePerGas` and `BlobBaseFee` in `BlockOverride`. - Extend `NumericConverterHelper.Parse<T>` with the same leading-zero check for `ulong`/`long` QUANTITY fields (`number`, `time`, `gasLimit`). - Fix pre-existing test that passed `"0x02"` for `blobBaseFee` (corrected to `"0x2"`).
|
Claude finished @manusw7's task in 6m 7s —— View job PR Review
SummaryThe overall approach is sound: One High and two Medium issues need to be addressed before merge. FindingsHigh (1)Buffer underflow in The Medium (2)
The window between process start and Low (2)
Missing comment on the |
|
|
||
| int length = reader.HasValueSequence ? (int)reader.ValueSequence.Length : reader.ValueSpan.Length; | ||
| if (length is 0 or > 78) | ||
| { |
There was a problem hiding this comment.
High — buffer underflow for oversized hex inputs
The > 78 guard is calibrated for the max decimal representation of UInt256 (78 chars), but it is too loose for the hex path. After stripping the "0x" prefix the remaining hex body can be up to 76 characters, while a UInt256 only fits 64. For hex.Length of 65–76:
length = (hex.Length >> 1) + hex.Length % 2 // e.g. 33 for 65 chars
bytes[(32 - length)..] // 32 - 33 = -1 → ArgumentOutOfRangeException
This throws an unhandled ArgumentOutOfRangeException (caught by the outer RPC try-catch and reported as -32602 Invalid params) rather than the descriptive SafePublicMessageFormatException. Any caller sending {"baseFeePerGas":"0x1000000000000000000000000000000000000000000000000000000000000001"} (65 hex chars after "0x") triggers this.
Fix: add a post-strip guard in ReadHex before the buffer operations:
if (hex.StartsWith("0x"u8))
{
hex = hex[2..];
if (hex.Length > 64)
ThrowJsonException(); // UInt256 is 32 bytes = 64 hex digits max
if (JsonRpcQuantityFormat.StrictMode && hex.Length > 1 && hex[0] == (byte)'0')
ThrowLeadingZero();
}Also worth adding a comment on line 39 to clarify: > 78 covers the 78-digit decimal max; the hex max (66 total) is enforced separately inside ReadHex.
| } | ||
|
|
||
| // Delegate to the standard UInt256 writer via the registered converter. | ||
| JsonSerializer.Serialize(writer, value.Value, options); |
There was a problem hiding this comment.
Medium — Write will produce incorrect output outside EthereumJsonSerializer
JsonSerializer.Serialize(writer, value.Value, options);This relies on options having a UInt256Converter registered. The test fixture uses new JsonSerializerOptions { Converters = { _converter } } which does not include UInt256Converter, so serializing via those options would produce a JSON object (all UInt256 public properties) instead of a hex string. In production this works only because EthereumJsonSerializer.CreateOptions() always registers UInt256Converter.
Consider caching a UInt256Converter instance and calling it directly:
private static readonly UInt256Converter _uint256Converter = new();
public override void Write(Utf8JsonWriter writer, UInt256? value, JsonSerializerOptions options)
{
if (value is null) { writer.WriteNullValue(); return; }
_uint256Converter.Write(writer, value.Value, options);
}This removes the dependency on options containing the right converter and avoids the overhead of re-entering the serializer metadata resolver.
| /// When <see langword="true"/>, hex QUANTITY strings with leading zero digits are rejected. | ||
| /// Mirrors <c>EthereumJsonSerializer.StrictHexFormat</c>. | ||
| /// </summary> | ||
| public static volatile bool StrictMode; |
There was a problem hiding this comment.
Medium — global mutable static creates invisible coupling
StrictMode is a process-wide flag that changes the behavior of every NullableQuantityUInt256Converter instance regardless of which JsonSerializerOptions it belongs to. This means:
- The converter behaves differently depending on whether
EthereumJsonSerializer.StrictHexFormatwas set before the first deserialization — e.g. if another test or plugin component uses the converter before startup wires the flag. - The two flags (
EthereumJsonSerializer._strictHexFormatandJsonRpcQuantityFormat.StrictMode) must always be in sync. Right now they are kept in sync by the setter, but the_strictHexFormatbacking field is directly accessible viaEthereumJsonSerializer.StrictHexFormatread — which reads_strictHexFormat, notStrictMode. They diverge if one is ever set without going through the setter.
A small improvement: consider initialising StrictMode to the IJsonRpcConfig.StrictHexFormat default (true) so that the converter is strict-by-default even before the startup hook fires:
public static volatile bool StrictMode = true; // mirrors JsonRpcConfig defaultThat makes the field's default safe without changing any existing behaviour (startup sets it explicitly anyway).
| namespace Nethermind.Core.Test.Json; | ||
|
|
||
| [TestFixture] | ||
| [NonParallelizable] |
There was a problem hiding this comment.
Low — [NonParallelizable] scope is too broad
Marking the entire fixture [NonParallelizable] serialises all tests in the class, including those that don't touch global state (Accepts_null, Can_read_value_lenient_mode without strict-mode mutations, Lenient_mode_accepts_leading_zeros). NullableULongConverterTests correctly scopes [NonParallelizable] to the individual methods that mutate global state. Prefer the same pattern here:
[TestFixture]
public class NullableQuantityUInt256ConverterTests // remove [NonParallelizable] from class
{
[NonParallelizable]
[TestCase("\"0x0b\"")]
...
public void Strict_rejects_leading_zero(string json) { ... }
[NonParallelizable]
[TestCase("\"0x0\"", 0UL)]
...
public void Strict_accepts_valid_quantity(string json, ulong expected) { ... }
}This also removes the need for the SetUp/TearDown save-restore scaffold (which could silently break if the save is taken while another thread has already mutated the flag).
- Add hex-body length guard (> 64 chars) in ReadHex to prevent buffer underflow for inputs with 65-76 hex digits after the 0x prefix - Implement Write self-contained using ToBigEndian + Convert.ToHexStringLower, removing the fragile dependency on options containing UInt256Converter - Initialise JsonRpcQuantityFormat.StrictMode to true to match the JsonRpcConfig.StrictHexFormat production default and close the startup window where requests could be accepted leniently - Scope [NonParallelizable] to the methods that mutate StrictMode instead of the entire fixture; use per-method try/finally save/restore - Add Write round-trip tests and the > 64-char rejection test
|
@claude review again |
|
Claude finished @manusw7's task in 6m 25s —— View job PR Review
SummaryAll five findings from the previous review have been addressed. The PR is clean and the implementation is correct. Previous Findings — Status
Fresh Review FindingsLow (3)
The outer
string hex = Convert.ToHexStringLower(bytes[start..]);
writer.WriteStringValue(hex[0] == '0' ? "0x" + hex[1..] : "0x" + hex);Two string allocations per serialisation call. Positive Notes
All previously blocking findings have been resolved. The three remaining findings are Low severity and do not block merge. |
EVM Opcode Benchmark DiffAggregated runs: base=3, pr=3 Improvements (1)
|
|
Need to fix the failing tests |
Fixes #12102
Changes
"0x0b"→-32602) inBlockOverridesfields (number,time,gasLimit,baseFeePerGas,blobBaseFee) per EIP-1474NullableQuantityUInt256ConverterinNethermind.Core.JsonConvertersfor strict QUANTITY validation onUInt256?fieldsNumericConverterHelperto detect leading zeros forulong/longQUANTITY fields when strict mode is enabledJsonRpcQuantityFormat.StrictMode(new shared flag inNethermind.Core) toEthereumJsonSerializer.StrictHexFormatsetter soNethermind.Evmconverters don't need to referenceNethermind.Serialization.JsonTypes of changes
Testing
Requires testing
If yes, did you write tests?
Notes on testing
Unit tests for
NullableQuantityUInt256Converter(strict/lenient, null, accept/reject cases) andNumericConverterHelperulong strict-mode cases. Integration tests inEthRpcModuleTests.EthCallcovering all fiveBlockOverridesQUANTITY fields — both rejection of leading-zero inputs and acceptance of valid quantities.Documentation
Requires documentation update
Requires explanation in Release Notes