diff --git a/simulators/ethereum/engine/README.md b/simulators/ethereum/engine/README.md index aa7c48dc04..f5c5f74ba0 100644 --- a/simulators/ethereum/engine/README.md +++ b/simulators/ethereum/engine/README.md @@ -55,6 +55,11 @@ Client must reject GetPayload directives under PoW. - Invalid Terminal Block in NewPayload: Client must reject NewPayload directives if the referenced ParentHash does not meet the TTD requirement. +- Inconsistent ForkchoiceState: +Send an inconsistent ForkchoiceState with a known payload that belongs to a side chain as head, safe or finalized: +Having `A: Genesis <- P1 <- P2 <- P3`, `B: Genesis <- P1' <- P2' <- P3'`, +send `fcU(Head: P3', Safe: P2, Finalized: P1)`, `fcU(Head: P3, Safe: P2', Finalized: P1)`, and `fcU(Head: P3, Safe: P2, Finalized: P1')` + - Unknown HeadBlockHash: Perform a forkchoiceUpdated call with an unknown (random) HeadBlockHash, the client should initiate the syncing process. diff --git a/simulators/ethereum/engine/enginetests.go b/simulators/ethereum/engine/enginetests.go index b0b4ad2809..8c6261374e 100644 --- a/simulators/ethereum/engine/enginetests.go +++ b/simulators/ethereum/engine/enginetests.go @@ -39,6 +39,18 @@ var engineTests = []TestSpec{ Run: invalidTerminalBlockNewPayload, TTD: 1000000, }, + { + Name: "Inconsistent Head in ForkchoiceState", + Run: inconsistentForkchoiceStateGen("Head"), + }, + { + Name: "Inconsistent Safe in ForkchoiceState", + Run: inconsistentForkchoiceStateGen("Safe"), + }, + { + Name: "Inconsistent Finalized in ForkchoiceState", + Run: inconsistentForkchoiceStateGen("Finalized"), + }, { Name: "Unknown HeadBlockHash", Run: unknownHeadBlockHash, @@ -446,6 +458,59 @@ func unknownHeadBlockHash(t *TestEnv) { } +// Send an inconsistent ForkchoiceState with a known payload that belongs to a side chain as head, safe or finalized. +func inconsistentForkchoiceStateGen(inconsistency string) func(t *TestEnv) { + return func(t *TestEnv) { + // Wait until TTD is reached by this client + t.CLMock.waitForTTD() + + canonicalPayloads := make([]*ExecutableDataV1, 0) + alternativePayloads := make([]*ExecutableDataV1, 0) + // Produce blocks before starting the test + t.CLMock.produceBlocks(3, BlockProcessCallbacks{ + OnGetPayload: func() { + // Generate and send an alternative side chain + customData := CustomPayloadData{} + customData.ExtraData = &([]byte{0x01}) + if len(alternativePayloads) > 0 { + customData.ParentHash = &alternativePayloads[len(alternativePayloads)-1].BlockHash + } + alternativePayload, err := customizePayload(&t.CLMock.LatestPayloadBuilt, &customData) + if err != nil { + t.Fatalf("FAIL (%s): Unable to construct alternative payload: %v", t.TestName, err) + } + alternativePayloads = append(alternativePayloads, alternativePayload) + latestCanonicalPayload := t.CLMock.LatestPayloadBuilt + canonicalPayloads = append(canonicalPayloads, &latestCanonicalPayload) + + // Send the alternative payload + r := t.TestEngine.TestEngineNewPayloadV1(alternativePayload) + r.ExpectStatusEither(Valid, Accepted) + }, + }) + // Send the invalid ForkchoiceStates + inconsistentFcU := ForkchoiceStateV1{ + HeadBlockHash: canonicalPayloads[len(alternativePayloads)-1].BlockHash, + SafeBlockHash: canonicalPayloads[len(alternativePayloads)-2].BlockHash, + FinalizedBlockHash: canonicalPayloads[len(alternativePayloads)-3].BlockHash, + } + switch inconsistency { + case "Head": + inconsistentFcU.HeadBlockHash = alternativePayloads[len(alternativePayloads)-1].BlockHash + case "Safe": + inconsistentFcU.SafeBlockHash = alternativePayloads[len(canonicalPayloads)-2].BlockHash + case "Finalized": + inconsistentFcU.FinalizedBlockHash = alternativePayloads[len(canonicalPayloads)-3].BlockHash + } + r := t.TestEngine.TestEngineForkchoiceUpdatedV1(&inconsistentFcU, nil) + r.ExpectError() + + // Return to the canonical chain + r = t.TestEngine.TestEngineForkchoiceUpdatedV1(&t.CLMock.LatestForkchoice, nil) + r.ExpectPayloadStatus(Valid) + } +} + // Verify behavior on a forkchoiceUpdated with invalid payload attributes func invalidPayloadAttributesGen(syncing bool) func(*TestEnv) {