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
5 changes: 3 additions & 2 deletions src/Nethermind/Ethereum.Blockchain.Pyspec.Test/Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ public class PragueBlockchainTests : PyspecBlockchainTestFixture<PragueBlockchai

public class OsakaBlockchainTests : PyspecBlockchainTestFixture<OsakaBlockchainTests>;

// Engine blockchain tests — only forks with meaningful Engine API differences
// (e.g. blobs, execution requests, BAL). Regular BlockchainTests cover earlier forks.
// Engine blockchain tests - post-merge forks with Engine API-specific coverage.
// Directory derived from class name by convention (strip "EngineBlockchainTests", lowercase)

public class ParisEngineBlockchainTests : PyspecEngineBlockchainTestFixture<ParisEngineBlockchainTests>;

public class CancunEngineBlockchainTests : PyspecEngineBlockchainTestFixture<CancunEngineBlockchainTests>;

public class PragueEngineBlockchainTests : PyspecEngineBlockchainTestFixture<PragueEngineBlockchainTests>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Nethermind.Blockchain
public interface IBlockFinalizationManager : IDisposable
{
/// <summary>
/// Last level that was finalize while processing blocks. This level will not be reorganised.
/// Current finalized level tracked by the active finalization manager.
/// </summary>
long LastFinalizedBlockLevel { get; }
event EventHandler<FinalizeEventArgs> BlocksFinalized;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class ManualBlockFinalizationManager : IManualBlockFinalizationManager
public void MarkFinalized(BlockHeader finalizingBlock, BlockHeader finalizedBlock)
{
LastFinalizedHash = finalizedBlock.Hash!;
LastFinalizedBlockLevel = Math.Max(LastFinalizedBlockLevel, finalizedBlock.Number);
LastFinalizedBlockLevel = finalizedBlock.Number;
BlocksFinalized?.Invoke(this, new FinalizeEventArgs(finalizingBlock, finalizedBlock));
}

Expand Down
56 changes: 46 additions & 10 deletions src/Nethermind/Nethermind.Merge.Plugin.Test/EngineModuleTests.V1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1003,8 +1003,8 @@ async Task CanReorganizeToLastBlock(MergeTestBlockchain testChain,
}

IReadOnlyList<ExecutionPayload> branch1 = await ProduceBranchV1(rpc, chain, 10, CreateParentBlockRequestOnHead(chain.BlockTree), true);
// setHead: false sibling production with head=safe=finalized=block per slot would
// regress finalized below branch1[9] (Casper FFG monotonicity, enforced by the handler).
// setHead: false - sibling production here only builds alternative payloads; the reorg
// assertions below exercise forkchoice updates explicitly.
IReadOnlyList<ExecutionPayload> branch2 = await ProduceBranchV1(rpc, chain, 6, branch1[3], setHead: false, TestItem.KeccakC);

await CanReorganizeToLastBlock(chain, branch1, branch2);
Expand Down Expand Up @@ -1512,26 +1512,62 @@ public async Task forkchoiceUpdated_safe_block_that_is_real_ancestor_of_current_
}

[Test]
public async Task forkchoiceUpdated_rejects_spec_ordering_violations()
public async Task forkchoiceUpdated_accepts_lower_finalized_than_previous_but_rejects_safe_before_finalized()
{
using MergeTestBlockchain chain =
await CreateBlockchain(null, new MergeConfig() { TerminalTotalDifficulty = "0" });
IEngineRpcModule rpc = chain.EngineRpcModule;

IReadOnlyList<ExecutionPayload> blocks = await ProduceBranchV1(rpc, chain, 4, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: false);
IReadOnlyList<ExecutionPayload> blocks = await ProduceBranchV1(rpc, chain, 4, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: true);

ForkchoiceStateV1 setup = new(headBlockHash: blocks[2].BlockHash, finalizedBlockHash: blocks[2].BlockHash, safeBlockHash: blocks[2].BlockHash);
(await rpc.engine_forkchoiceUpdatedV1(setup)).Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
ForkchoiceStateV1 higherFinalized = new(headBlockHash: blocks[3].BlockHash, finalizedBlockHash: blocks[2].BlockHash, safeBlockHash: blocks[2].BlockHash);
ResultWrapper<ForkchoiceUpdatedV1Result> higherFinalizedResult = await rpc.engine_forkchoiceUpdatedV1(higherFinalized);
higherFinalizedResult.ErrorCode.Should().Be(0);
higherFinalizedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockFinalizationManager.LastFinalizedHash.Should().Be(blocks[2].BlockHash);
chain.BlockFinalizationManager.LastFinalizedBlockLevel.Should().Be(blocks[2].BlockNumber);

// Casper FFG monotonicity: new finalized below previously-accepted finalized.
ForkchoiceStateV1 monotonicity = new(headBlockHash: blocks[3].BlockHash, finalizedBlockHash: blocks[0].BlockHash, safeBlockHash: blocks[0].BlockHash);
(await rpc.engine_forkchoiceUpdatedV1(monotonicity)).ErrorCode.Should().Be(MergeErrorCodes.InvalidForkchoiceState);
ForkchoiceStateV1 lowerFinalized = new(headBlockHash: blocks[3].BlockHash, finalizedBlockHash: blocks[1].BlockHash, safeBlockHash: blocks[2].BlockHash);
ResultWrapper<ForkchoiceUpdatedV1Result> lowerFinalizedResult = await rpc.engine_forkchoiceUpdatedV1(lowerFinalized);
lowerFinalizedResult.ErrorCode.Should().Be(0);
lowerFinalizedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
chain.BlockFinalizationManager.LastFinalizedHash.Should().Be(blocks[1].BlockHash);
chain.BlockFinalizationManager.LastFinalizedBlockLevel.Should().Be(blocks[1].BlockNumber);

// Spec ordering: safe must be at or after finalized.
// Request-local spec ordering: safe must be at or after finalized.
ForkchoiceStateV1 ordering = new(headBlockHash: blocks[3].BlockHash, finalizedBlockHash: blocks[2].BlockHash, safeBlockHash: blocks[1].BlockHash);
(await rpc.engine_forkchoiceUpdatedV1(ordering)).ErrorCode.Should().Be(MergeErrorCodes.InvalidForkchoiceState);
}
Comment thread
benaadams marked this conversation as resolved.

[Test]
public async Task forkchoiceUpdatedV1_should_allow_lower_finalized_than_previous_when_building_payload()
{
using MergeTestBlockchain chain =
await CreateBlockchain(null, new MergeConfig() { TerminalTotalDifficulty = "0" });
IEngineRpcModule rpc = chain.EngineRpcModule;

IReadOnlyList<ExecutionPayload> blocks = await ProduceBranchV1(rpc, chain, 4, CreateParentBlockRequestOnHead(chain.BlockTree), setHead: true);

PayloadAttributes payloadAttributes = new()
{
Timestamp = blocks[3].Timestamp + 1,
PrevRandao = TestItem.KeccakB,
SuggestedFeeRecipient = TestItem.AddressC,
};

ForkchoiceStateV1 higherFinalized = new(headBlockHash: blocks[3].BlockHash, finalizedBlockHash: blocks[2].BlockHash, safeBlockHash: blocks[2].BlockHash);
ResultWrapper<ForkchoiceUpdatedV1Result> higherFinalizedResult = await rpc.engine_forkchoiceUpdatedV1(higherFinalized);
higherFinalizedResult.ErrorCode.Should().Be(0);
higherFinalizedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);

ForkchoiceStateV1 repeatedHead = new(headBlockHash: blocks[3].BlockHash, finalizedBlockHash: blocks[1].BlockHash, safeBlockHash: blocks[2].BlockHash);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpc.engine_forkchoiceUpdatedV1(repeatedHead, payloadAttributes);

result.ErrorCode.Should().Be(0);
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
result.Data.PayloadId.Should().NotBeNull();
}

[TestCase(false, TestName = "forkchoiceUpdated_rejects_repeated_finalized_when_head_on_sibling_branch")]
[TestCase(true, TestName = "forkchoiceUpdated_rejects_repeated_safe_when_head_on_sibling_branch")]
public async Task forkchoiceUpdated_rejects_repeated_hash_when_head_on_sibling_branch(bool cachedSafe)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,41 @@ static void MarkAsUnprocessed(MergeTestBlockchain chain, int blockNumber)
Assert.That(res2.Data.PayloadStatus.Status, Is.EqualTo(PayloadStatus.Valid));
}

[Test]
public async Task ForkChoiceUpdatedV3_should_allow_lower_finalized_than_previous_when_building_payload()
{
using MergeTestBlockchain chain = await CreateBlockchain(releaseSpec: Cancun.Instance, mergeConfig: new MergeConfig()
{
NewPayloadBlockProcessingTimeout = 1000
});
IEngineRpcModule rpcModule = chain.EngineRpcModule;

ExecutionPayloadV3 block1 = await AddNewBlockV3(rpcModule, chain);
ExecutionPayloadV3 block2 = await AddNewBlockV3(rpcModule, chain);
ExecutionPayloadV3 block3 = await AddNewBlockV3(rpcModule, chain);

PayloadAttributes payloadAttributes = new()
{
Timestamp = block3.Timestamp + 1,
PrevRandao = TestItem.KeccakH,
SuggestedFeeRecipient = TestItem.AddressF,
Withdrawals = [],
ParentBeaconBlockRoot = TestItem.KeccakE
};

ForkchoiceStateV1 higherFinalized = new(headBlockHash: block3.BlockHash, finalizedBlockHash: block2.BlockHash, safeBlockHash: block2.BlockHash);
ResultWrapper<ForkchoiceUpdatedV1Result> higherFinalizedResult = await rpcModule.engine_forkchoiceUpdatedV3(higherFinalized, null);
higherFinalizedResult.ErrorCode.Should().Be(0);
higherFinalizedResult.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);

ForkchoiceStateV1 repeatedHead = new(headBlockHash: block3.BlockHash, finalizedBlockHash: block1.BlockHash, safeBlockHash: block2.BlockHash);
ResultWrapper<ForkchoiceUpdatedV1Result> result = await rpcModule.engine_forkchoiceUpdatedV3(repeatedHead, payloadAttributes);

result.ErrorCode.Should().Be(0);
result.Data.PayloadStatus.Status.Should().Be(PayloadStatus.Valid);
result.Data.PayloadId.Should().NotBeNull();
}

[Test]
public async Task GetBlobsV1_should_throw_if_more_than_128_requested_blobs([Values(128, 129)] int requestSize)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,12 @@ protected virtual bool IsOnMainChainBehindHead(BlockHeader newHeadHeader, Forkch
return true;
}

// Rejects a finalized/safe entry that fails the numeric spec-ordering bounds (Casper FFG
// monotonicity for finalized, safe >= finalized for safe) or the ancestry check against
// newHead. L1-derived finality models override this to relax the bounds check while keeping
// Rejects a finalized/safe entry that fails the request-local numeric bounds
// (finalized/safe cannot be above head, and safe cannot be below finalized)
// or the ancestry check against newHead. Cross-request finalized monotonicity is
// intentionally not enforced here; CLs may legitimately resend an older finalized
// marker on a later FCU as long as that FCU tuple is internally consistent.
// L1-derived finality models override this to relax the bounds check while keeping
// ancestry validation.
protected virtual ResultWrapper<ForkchoiceUpdatedV1Result>? RejectIfInconsistent(
BlockHeader? header, long lowerBound, string label, BlockHeader newHeadHeader, string requestStr)
Expand Down Expand Up @@ -248,13 +251,12 @@ protected virtual bool IsOnMainChainBehindHead(BlockHeader newHeadHeader, Forkch
_blockTree.UpdateMainChain(blocks!, true, true);
}

// Spec ordering: prevFinalized <= finalized <= safe <= head. Ancestry must be re-validated
// on every FCU - the binding is (head, finalized, safe), so a repeated finalized/safe hash
// paired with a new head on a sibling branch is still a spec violation.
long prevFinalizedLevel = _manualBlockFinalizationManager.LastFinalizedBlockLevel;
// Spec ordering within a single FCU: finalized <= safe <= head. Ancestry must be
// re-validated on every FCU - the binding is (head, finalized, safe), so a repeated
// finalized/safe hash paired with a new head on a sibling branch is still a spec violation.
long finalizedNumber = finalizedHeader?.Number ?? 0;

if (RejectIfInconsistent(finalizedHeader, prevFinalizedLevel, "finalized", newHeadHeader, requestStr) is { } finalizedError) return finalizedError;
if (RejectIfInconsistent(finalizedHeader, 0, "finalized", newHeadHeader, requestStr) is { } finalizedError) return finalizedError;
Comment thread
benaadams marked this conversation as resolved.
Comment thread
LukaszRozmej marked this conversation as resolved.
if (RejectIfInconsistent(safeBlockHeader, finalizedNumber, "safe", newHeadHeader, requestStr) is { } safeError) return safeError;

bool nonZeroFinalizedBlockHash = finalizedBlockHash != Keccak.Zero;
Expand Down
Loading