Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2ecf266
feat(engine): implement Engine API v2 REST+SSZ surface per spec
Dyslex7c Jun 3, 2026
78c78d7
fix build errors
Dyslex7c Jun 3, 2026
aba2a6a
fix benchmarks build error
Dyslex7c Jun 3, 2026
7e93834
fix stateless build and AuRa test
Dyslex7c Jun 3, 2026
0eadda1
address claude review
Dyslex7c Jun 3, 2026
73c3019
address deep review comments
Dyslex7c Jun 3, 2026
60915b3
Merge branch 'master' into update-ssz-spec
LukaszRozmej Jun 3, 2026
43309fe
fix consensus test
Dyslex7c Jun 3, 2026
2447394
centralize fork names
Dyslex7c Jun 3, 2026
4b93d02
address other review comments by @flcl42
Dyslex7c Jun 3, 2026
00b4b6d
address claude review comments
Dyslex7c Jun 3, 2026
7610306
clean fcu handler and remove `custodyColumns`, add v2 to urls
Dyslex7c Jun 4, 2026
ac868b2
remove `TargetGasLimit` from `PayloadAttributes`
Dyslex7c Jun 10, 2026
a8e5611
Merge remote-tracking branch 'upstream/master' into update-ssz-spec
Dyslex7c Jun 10, 2026
0f80d56
fix AuRa merge tests
Dyslex7c Jun 11, 2026
3894b82
use modern transport during startup
Dyslex7c Jun 11, 2026
efbb6da
address review: simplifications, pooling, no-alloc
LukaszRozmej Jun 12, 2026
486fbc0
SszRestPaths: ForkVersionKey record + cached span lookup
LukaszRozmej Jun 12, 2026
3ca5b97
address review: cache capabilities, inline-array cell, dedup
LukaszRozmej Jun 12, 2026
e30f3c7
refactor: implement memory pooling for `engine_getBlobs` REST/RPC end…
Dyslex7c Jun 12, 2026
4439b5f
simplify FCU URL-fork match to a single spec.Name comparison
LukaszRozmej Jun 12, 2026
e4aa948
move engine-api versions onto NamedReleaseSpec
LukaszRozmej Jun 12, 2026
17908e3
GetBlobsHandlerV4: clear pool-rented arrays to avoid stale-slot corru…
LukaszRozmej Jun 12, 2026
2476760
SszRestPaths: build fork→spec map by walking from Forks.Amsterdam
LukaszRozmej Jun 12, 2026
b2935ab
EngineApiVersions: move to Nethermind.Core namespace; SszRestPaths: d…
LukaszRozmej Jun 12, 2026
8b35d68
GetBlobsV4 SSZ encode: fix cell length + slice rented buffers
LukaszRozmej Jun 12, 2026
cfac389
SszMiddlewareTests: drop s_ prefix; use EngineApiVersions for version…
LukaszRozmej Jun 12, 2026
4aff0b2
Merge branch 'master' of github.com:NethermindEth/nethermind into wor…
LukaszRozmej Jun 12, 2026
fa17686
test: cover unscoped-endpoint extra-segment rejection in TryRoute
LukaszRozmej Jun 12, 2026
d57b6eb
Merge commit '8b35d68dca' into worktree-pr-11887-review
LukaszRozmej Jun 12, 2026
74a9d52
Merge branch 'update-ssz-spec' of github.com:Dyslex7c/nethermind into…
LukaszRozmej Jun 12, 2026
e96869b
address two-reviewer findings on SSZ-REST surface
LukaszRozmej Jun 12, 2026
d6d3e66
style: remove unused using directives flagged by IDE0005
LukaszRozmej Jun 12, 2026
7ca8a97
align SSZ-REST surface with execution-apis #793 spec
LukaszRozmej Jun 12, 2026
e0b8f48
Apply suggestion from @LukaszRozmej
LukaszRozmej Jun 12, 2026
f9d3081
fix disposal leak on SSZ-REST bodies path + align V4 wire to spec
LukaszRozmej Jun 12, 2026
0d43291
fix SszExecutionPayloadV4.BlockAccessList limit to match spec MAX_BAL…
LukaszRozmej Jun 12, 2026
dfa8e83
revert SszExecutionPayloadV4.BlockAccessList limit to 16 MiB
LukaszRozmej Jun 12, 2026
0c8cccb
Merge branch 'master' into update-ssz-spec
LukaszRozmej Jun 13, 2026
dad6790
address deep-review nits on SSZ-REST surface
LukaszRozmej Jun 13, 2026
1551536
Merge remote-tracking branch 'Dyslex7c/update-ssz-spec' into pr-11887…
LukaszRozmej Jun 13, 2026
13e7ea9
map ErrorCodes.ParseError to /engine-api/errors/parse-error
LukaszRozmej Jun 13, 2026
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
@@ -0,0 +1,84 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using Nethermind.Consensus.Producers;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Specs;
using NSubstitute;
using NUnit.Framework;

namespace Nethermind.Consensus.Test;

public class PayloadAttributesValidateTests
{
private static PayloadAttributes BuildAttrs(bool withSlotNumber, ulong timestamp = 1_000UL) => new()
{
Timestamp = timestamp,
PrevRandao = Keccak.Zero,
SuggestedFeeRecipient = Address.Zero,
Withdrawals = [],
ParentBeaconBlockRoot = Keccak.Zero,
SlotNumber = withSlotNumber ? 42UL : null,
};

private static ISpecProvider MakeSpecProvider(bool isAmsterdam)
{
ISpecProvider sp = Substitute.For<ISpecProvider>();
IReleaseSpec spec = Substitute.For<IReleaseSpec>();
spec.IsEip7843Enabled.Returns(isAmsterdam);
spec.IsEip4844Enabled.Returns(true);
spec.WithdrawalsEnabled.Returns(true);
sp.GetSpec(Arg.Any<ForkActivation>()).Returns(spec);
return sp;
}

// Each case asserts a distinct branch of PayloadAttributes.Validate against the spec.
// mustContain/mustNotContain are checked when non-null.
private static readonly object[] ValidateCases =
[
new object[] { /* isAmsterdam */ true, /* withSlot */ false, /* fcu */ PayloadAttributesVersions.V4,
PayloadAttributesValidationResult.InvalidPayloadAttributes, "must be provided", "expected" },
new object[] { /* isAmsterdam */ true, /* withSlot */ true, /* fcu */ PayloadAttributesVersions.V4,
PayloadAttributesValidationResult.Success, null!, null! },
new object[] { /* isAmsterdam */ false, /* withSlot */ true, /* fcu */ PayloadAttributesVersions.V3,
PayloadAttributesValidationResult.UnsupportedFork, null!, null! },
];

[TestCaseSource(nameof(ValidateCases))]
public void Validate_returns_expected_result(
bool isAmsterdam, bool withSlotNumber, int fcuVersion,
PayloadAttributesValidationResult expected, string errorMustContain, string errorMustNotContain)
{
ISpecProvider sp = MakeSpecProvider(isAmsterdam);
PayloadAttributes attrs = BuildAttrs(withSlotNumber);

PayloadAttributesValidationResult result = attrs.Validate(sp, fcuVersion, out string error);

Assert.That(result, Is.EqualTo(expected));
if (expected == PayloadAttributesValidationResult.Success)
{
Assert.That(error, Is.Null);
}
else
{
Assert.That(error, Is.Not.Null);
if (errorMustContain is not null) Assert.That(error, Does.Contain(errorMustContain));
if (errorMustNotContain is not null) Assert.That(error, Does.Not.Contain(errorMustNotContain));
}
}

[TestCase(false, PayloadAttributesVersions.V1)]
[TestCase(true, PayloadAttributesVersions.V4)]
public void GetVersion_infers_correct_version_from_present_fields(
bool hasSlotNumber, int expectedVersion)
{
PayloadAttributes attrs = new()
{
Timestamp = 1_000UL,
SlotNumber = hasSlotNumber ? 1UL : null,
};

Assert.That(attrs.GetVersion(), Is.EqualTo(expectedVersion));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ protected virtual int ComputePayloadIdMembersSize() =>
+ Address.Size // suggested fee recipient
+ (Withdrawals is null ? 0 : Keccak.Size) // withdrawals root hash
+ (ParentBeaconBlockRoot is null ? 0 : Keccak.Size) // parent beacon block root
+ (SlotNumber is null ? 0 : sizeof(ulong)); // slot number
+ (SlotNumber is null ? 0 : sizeof(ulong)) // slot number
;

protected static string ComputePayloadId(Span<byte> inputSpan)
{
Expand Down Expand Up @@ -158,23 +159,25 @@ private static PayloadAttributesValidationResult ValidateVersion(
string methodName,
[NotNullWhen(false)] out string? error)
{
// This FCU version doesn't support this fork at all (e.g. V3 attrs sent to FCUv2).
if (!IsSupportedFcuForkCombination(fcuVersion, actualVersion))
{
error = $"{methodName}{fcuVersion} expected";
return PayloadAttributesValidationResult.InvalidPayloadAttributes;
}

// Attributes structure doesn't match what the fork expects (e.g. V3 attrs sent to when FCUv3 not yet activated in spec).
// Attributes structure doesn't match what the fork expects (e.g. V3 attrs sent when FCUv3 not yet activated in spec).
if (actualVersion != timestampVersion)
{
error = $"{methodName}{timestampVersion} expected";
// FCU also doesn't support this fork → UnsupportedFork (post-Paris only)
return fcuVersion != timestampVersion && timestampVersion >= PayloadAttributesVersions.V2
bool unsupportedFork = timestampVersion >= PayloadAttributesVersions.V2 &&
(actualVersion > timestampVersion || fcuVersion != timestampVersion);
return unsupportedFork
? PayloadAttributesValidationResult.UnsupportedFork
: PayloadAttributesValidationResult.InvalidPayloadAttributes;
}

// This FCU version doesn't support this fork at all (e.g. V3 attrs sent to FCUv2).
if (!IsSupportedFcuForkCombination(fcuVersion, actualVersion))
{
error = $"{methodName}{fcuVersion} expected";
return PayloadAttributesValidationResult.InvalidPayloadAttributes;
}

error = null;
return PayloadAttributesValidationResult.Success;
}
Expand All @@ -185,10 +188,25 @@ public virtual PayloadAttributesValidationResult Validate(
[NotNullWhen(false)] out string? error)
{
int actualVersion = this.GetVersion();
int timestampVersion = specProvider.GetSpec(ForkActivation.TimestampOnly(Timestamp)).ExpectedPayloadAttributesVersion();

// When attrs are below the timestamp-implied version and the FCU doesn't accept this
// combination (i.e. it's not the V2-accepts-V1 backward-compat case), report the
// specific missing field rather than a generic version-mismatch.
if (actualVersion < timestampVersion && !IsSupportedFcuForkCombination(fcuVersion, actualVersion))
{
string? fieldError = ValidateFields(timestampVersion);
if (fieldError is not null)
{
error = fieldError;
return PayloadAttributesValidationResult.InvalidPayloadAttributes;
}
}

PayloadAttributesValidationResult result = ValidateVersion(
fcuVersion,
actualVersion,
timestampVersion: specProvider.GetSpec(ForkActivation.TimestampOnly(Timestamp)).ExpectedPayloadAttributesVersion(),
timestampVersion,
"PayloadAttributesV",
out error);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

namespace Nethermind.Consensus;
namespace Nethermind.Core;

/// <summary>
/// Engine API method version constants, grouped by method.
Expand Down Expand Up @@ -50,7 +50,8 @@ public static class GetBlobs
public const int V1 = 1; // Cancun
public const int V2 = 2; // Osaka
public const int V3 = 3; // Osaka (allowPartialReturn = true)
public const int Latest = V3;
public const int V4 = 4; // Osaka (cell retrieval, EIP-7594/PeerDAS)
public const int Latest = V4;
}

/// <summary>engine_getPayloadBodiesByHash method versions.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,8 @@ ReadOnlySpan<byte> proof

public static void ComputeCellProofs(ReadOnlySpan<byte> blob, Span<byte> cellProofs) =>
Ckzg.ComputeCellsAndKzgProofs(new byte[Ckzg.CellsPerExtBlob * Ckzg.BytesPerCell], cellProofs, blob, _ckzgSetup);

/// <param name="blob">The input blob data.</param>
/// <param name="cells">The output span of size <c>CELLS_PER_EXT_BLOB * BYTES_PER_CELL</c> (262144 bytes) where cells will be written.</param>
public static void ComputeCells(ReadOnlySpan<byte> blob, Span<byte> cells) => Ckzg.ComputeCells(cells, blob, _ckzgSetup);
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public void GlobalSetup()
Hash256[] blobHashes = BuildBlobVersionedHashes(Blobs);
Hash256 parentRoot = TestItem.KeccakA;

_sszBody = EncodeSszBody(payload, blobHashes, parentRoot);
_sszBody = EncodeSszBody(payload, parentRoot);
Comment thread
flcl42 marked this conversation as resolved.
_jsonBody = EncodeJsonBody(payload, blobHashes, parentRoot);
_jsonPayloadOnly = JsonSerializer.SerializeToUtf8Bytes(payload, EthereumJsonSerializer.JsonOptions);

Expand Down Expand Up @@ -187,12 +187,11 @@ private static Hash256[] BuildBlobVersionedHashes(int count)
return hashes;
}

private static byte[] EncodeSszBody(ExecutionPayloadV3 payload, Hash256[] blobs, Hash256 parentRoot)
private static byte[] EncodeSszBody(ExecutionPayloadV3 payload, Hash256 parentRoot)
{
NewPayloadV3RequestWire wire = new()
{
ExecutionPayload = new SszExecutionPayloadV3(payload),
ExpectedBlobVersionedHashes = blobs,
ParentBeaconBlockRoot = parentRoot,
};

Expand Down
55 changes: 29 additions & 26 deletions src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
using Nethermind.Logging;
using Nethermind.Merge.Plugin.Data;
using Nethermind.Merge.Plugin.Handlers;
using Nethermind.Merge.Plugin.SszRest.Handlers;
using Nethermind.Serialization.Json;
using Nethermind.Specs;
using Nethermind.Specs.ChainSpecStyle;
Expand Down Expand Up @@ -2031,7 +2032,7 @@ public async Task Should_return_ClientVersionV1()
{
using MergeTestBlockchain chain = await CreateBlockchain();
IEngineRpcModule rpcModule = chain.EngineRpcModule;
ResultWrapper<ClientVersionV1[]> result = rpcModule.engine_getClientVersionV1(new ClientVersionV1());
ResultWrapper<ClientVersionV1[]> result = rpcModule.engine_getClientVersionV1(default);
Assert.That(result.Data, Is.EqualTo([new ClientVersionV1()]));
}

Expand Down Expand Up @@ -2089,7 +2090,8 @@ public void Should_return_expected_capabilities_for_mainnet()

nameof(IEngineRpcModule.engine_getPayloadV5),
nameof(IEngineRpcModule.engine_getBlobsV2),
nameof(IEngineRpcModule.engine_getBlobsV3)
nameof(IEngineRpcModule.engine_getBlobsV3),
nameof(IEngineRpcModule.engine_getBlobsV4)
];
Assert.That(result, Is.EquivalentTo(expectedMethods));
}
Expand Down Expand Up @@ -2126,50 +2128,51 @@ public async Task Should_warn_for_missing_capabilities()

private static readonly string[] SszRestPathsParis =
[
"POST /engine/v1/payloads",
"GET /engine/v1/payloads/{payload_id}",
"POST /engine/v1/forkchoice",
"POST /engine/v1/capabilities",
"POST /engine/v1/client/version",
SszRestPaths.PostV1Payloads,
SszRestPaths.GetV1Payloads,
SszRestPaths.PostV1Forkchoice,
SszRestPaths.PostV1Capabilities,
SszRestPaths.PostV1ClientVersion,
];

private static readonly string[] SszRestPathsShanghai =
[
"POST /engine/v2/payloads",
"GET /engine/v2/payloads/{payload_id}",
"POST /engine/v2/forkchoice",
"POST /engine/v1/payloads/bodies/by-hash",
"POST /engine/v1/payloads/bodies/by-range",
SszRestPaths.PostV2Payloads,
SszRestPaths.GetV2Payloads,
SszRestPaths.PostV2Forkchoice,
SszRestPaths.PostV1PayloadBodiesByHash,
SszRestPaths.GetV1PayloadBodiesByRange,
];

private static readonly string[] SszRestPathsCancun =
[
"POST /engine/v3/payloads",
"GET /engine/v3/payloads/{payload_id}",
"POST /engine/v3/forkchoice",
"POST /engine/v1/blobs",
SszRestPaths.PostV3Payloads,
SszRestPaths.GetV3Payloads,
SszRestPaths.PostV3Forkchoice,
SszRestPaths.PostV1Blobs,
];

private static readonly string[] SszRestPathsPrague =
[
"POST /engine/v4/payloads",
"GET /engine/v4/payloads/{payload_id}",
SszRestPaths.PostV4Payloads,
SszRestPaths.GetV4Payloads,
];

private static readonly string[] SszRestPathsOsaka =
[
"GET /engine/v5/payloads/{payload_id}",
"POST /engine/v2/blobs",
"POST /engine/v3/blobs",
SszRestPaths.GetV5Payloads,
SszRestPaths.PostV2Blobs,
SszRestPaths.PostV3Blobs,
SszRestPaths.PostV4Blobs,
];

private static readonly string[] SszRestPathsAmsterdam =
[
"POST /engine/v5/payloads",
"GET /engine/v6/payloads/{payload_id}",
"POST /engine/v4/forkchoice",
"POST /engine/v2/payloads/bodies/by-hash",
"POST /engine/v2/payloads/bodies/by-range",
SszRestPaths.PostV5Payloads,
SszRestPaths.GetV6Payloads,
SszRestPaths.PostV4Forkchoice,
SszRestPaths.PostV2PayloadBodiesByHash,
SszRestPaths.GetV2PayloadBodiesByRange,
];

public static IEnumerable<TestCaseData> SszRestPathsAdvertisedCases()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,10 @@ public async Task<string> NewPayloadV3_should_verify_blob_versioned_hashes_again
Substitute.For<IHandler<IReadOnlyList<Hash256>, IReadOnlyList<ExecutionPayloadBodyV1Result?>>>(),
Substitute.For<IGetPayloadBodiesByRangeV1Handler>(),
Substitute.For<IHandler<TransitionConfigurationV1, TransitionConfigurationV1>>(),
Substitute.For<IHandler<IEnumerable<string>, IReadOnlyList<string>>>(),
Substitute.For<IHandler<HashSet<string>, IReadOnlyList<string>>>(),
Substitute.For<IAsyncHandler<byte[][], IReadOnlyList<BlobAndProofV1?>>>(),
Substitute.For<IAsyncHandler<GetBlobsHandlerV2Request, IReadOnlyList<BlobAndProofV2?>?>>(),
Substitute.For<IAsyncHandler<GetBlobsHandlerV4Request, IReadOnlyList<BlobCellsAndProofs?>?>>(),
Substitute.For<IHandler<IReadOnlyList<Hash256>, IReadOnlyList<ExecutionPayloadBodyV2Result?>>>(),
Substitute.For<IGetPayloadBodiesByRangeV2Handler>(),
Substitute.For<IEngineRequestsTracker>(),
Expand Down Expand Up @@ -855,7 +856,7 @@ public static IEnumerable<TestCaseData> ForkchoiceUpdatedV3DeclinedTestCaseSourc
yield return new TestCaseData(Shanghai.Instance, nameof(IEngineRpcModule.engine_forkchoiceUpdatedV2), true)
{
TestName = "ForkchoiceUpdatedV2 To Request Shanghai Payload, Zero Beacon Root",
ExpectedResult = MergeErrorCodes.InvalidPayloadAttributes,
ExpectedResult = MergeErrorCodes.UnsupportedFork,
};
yield return new TestCaseData(Cancun.Instance, nameof(IEngineRpcModule.engine_forkchoiceUpdatedV2), true)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ public virtual async Task GetPayloadV6_builds_block_with_BAL(string? customWithd
SuggestedFeeRecipient = Address.Zero,
ParentBeaconBlockRoot = Keccak.Zero,
Withdrawals = [],
SlotNumber = 1
SlotNumber = 1,
};

Transaction tx = Build.A.Transaction
Expand Down Expand Up @@ -510,7 +510,7 @@ private async Task<ExecutionPayloadV4> AddNewBlockV6(IEngineRpcModule rpcModule,
SuggestedFeeRecipient = TestItem.AddressF,
Withdrawals = [],
ParentBeaconBlockRoot = TestItem.KeccakE,
SlotNumber = chain.BlockTree.Head!.SlotNumber + 1
SlotNumber = chain.BlockTree.Head!.SlotNumber + 1,
};
Hash256 currentHeadHash = chain.BlockTree.HeadHash;
ForkchoiceStateV1 forkchoiceState = new(currentHeadHash, currentHeadHash, currentHeadHash);
Expand Down Expand Up @@ -849,7 +849,7 @@ private static (Transaction tx, Transaction tx2, Transaction tx3, Withdrawal wit
SuggestedFeeRecipient = TestItem.AddressE,
ParentBeaconBlockRoot = Keccak.Zero,
Withdrawals = [withdrawal],
SlotNumber = slotNumber
SlotNumber = slotNumber,
};

ForkchoiceStateV1 fcuState = new(parentHash, parentHash, parentHash);
Expand Down
Loading
Loading