From e02e8fca6679832df0407a92ae243649d346a981 Mon Sep 17 00:00:00 2001
From: marioevz <marioevz@gmail.com>
Date: Mon, 25 Apr 2022 16:21:54 +0000
Subject: [PATCH] simulators/engine: reorg to missing invalid payload chain
 tests

---
 simulators/ethereum/engine/enginetests.go | 110 ++++++++++++++++++++++
 1 file changed, 110 insertions(+)

diff --git a/simulators/ethereum/engine/enginetests.go b/simulators/ethereum/engine/enginetests.go
index b957fa5f64..0d612c8048 100644
--- a/simulators/ethereum/engine/enginetests.go
+++ b/simulators/ethereum/engine/enginetests.go
@@ -132,6 +132,16 @@ var engineTests = []TestSpec{
 		Run:  invalidPayloadTestCaseGen("Transaction/Value"),
 	},
 
+	// Invalid Payload Re-Org Tests
+	{
+		Name: "Re-Org to Chain Missing Invalid Parent (Invalid after common ancestor)",
+		Run:  reOrgToChainMissingInvalidParentGen(1),
+	},
+	{
+		Name: "Re-Org to Chain Missing Invalid Parent (Invalid two payloads after common ancestor)",
+		Run:  reOrgToChainMissingInvalidParentGen(3),
+	},
+
 	// Eth RPC Status on ForkchoiceUpdated Events
 	{
 		Name: "Latest Block after NewPayload",
@@ -756,6 +766,106 @@ func invalidPayloadTestCaseGen(payloadField string) func(*TestEnv) {
 	}
 }
 
+// Attempt to re-org to a chain which at some point contains an unknown payload which is also invalid.
+// Then reveal the invalid payload and expect that the client rejects it and rejects forkchoice updated calls to this chain.
+// The invalid_index parameter determines how many payloads apart is the common ancestor from the block that invalidates the chain,
+// with a value of 1 meaning that the immediate payload after the common ancestor will be invalid.
+func reOrgToChainMissingInvalidParentGen(invalid_index int) func(*TestEnv) {
+	return func(t *TestEnv) {
+		// Wait until TTD is reached by this client
+		t.CLMock.waitForTTD()
+
+		// Produce blocks before starting the test
+		t.CLMock.produceBlocks(5, BlockProcessCallbacks{})
+
+		// Save the common ancestor
+		cA := t.CLMock.LatestPayloadBuilt
+
+		// Amount of blocks to deviate starting from the common ancestor
+		n := 10
+
+		// Slice to save the alternate B chain
+		altChainPayloads := make([]*ExecutableDataV1, 0)
+
+		// Append the common ancestor
+		altChainPayloads = append(altChainPayloads, &cA)
+
+		// Produce blocks but at the same time create an alternate chain which contains an invalid parent (INV_P1)
+		// CommonAncestor◄─▲── P1 ◄──── P2  ◄─ P3  ◄─ ... ◄─ Pn
+		//                 │
+		//                 └──INV_P1 ◄─ P2' ◄─ P3' ◄─ ... ◄─ Pn'
+		t.CLMock.produceBlocks(n, BlockProcessCallbacks{
+			OnGetPayload: func() {
+				var (
+					alternatePayload *ExecutableDataV1
+					err              error
+				)
+				// Create another prevRandao to ensure we deviate from the main payload
+				prevRandao := common.Hash{}
+				rand.Read(prevRandao[:])
+				if len(altChainPayloads) == invalid_index {
+					// This is the payload we are looking to invalidate, this must be invalid
+					invStateRoot := common.Hash{}
+					rand.Read(invStateRoot[:])
+					alternatePayload, err = customizePayload(&t.CLMock.LatestPayloadBuilt, &CustomPayloadData{
+						StateRoot:  &invStateRoot,
+						ParentHash: &altChainPayloads[len(altChainPayloads)-1].BlockHash,
+						PrevRandao: &prevRandao,
+					})
+				} else {
+					// Modify the created payload only to reference the previous parent in the B chain
+					alternatePayload, err = customizePayload(&t.CLMock.LatestPayloadBuilt, &CustomPayloadData{
+						ParentHash: &altChainPayloads[len(altChainPayloads)-1].BlockHash,
+						PrevRandao: &prevRandao,
+					})
+				}
+				if err != nil {
+					t.Fatalf("FAIL (%s): Unable to customize payload: %v", t.TestName, err)
+				}
+				altChainPayloads = append(altChainPayloads, alternatePayload)
+			},
+		})
+
+		// Now let's send the alternate chain to the client using newPayload in reverse order
+		for i := n - 1; i >= 0; i-- {
+			// Send the payload
+			r := t.TestEngine.TestEngineNewPayloadV1(altChainPayloads[i])
+			if i == 1 {
+				if i == invalid_index {
+					// If this is the first payload after the common ancestor, and this is the payload we invalidated,
+					// then we have all the information to determine that this payload is invalid.
+					r.ExpectStatus(Invalid)
+					r.ExpectLatestValidHash(&cA.BlockHash)
+				} else {
+					// This is the first payload, but we did not invalidated the chain until after this one,
+					// therefore the expected status is valid
+					r.ExpectStatus(Valid)
+				}
+
+			} else if i > 1 {
+				r.ExpectStatus(Accepted)
+				r.ExpectLatestValidHash(nil)
+			}
+
+			if i == n-1 {
+				// Send the fcU, this is before we send all the payloads from the alternative chain.
+				// At this point is impossible to give a latestValidHash, since we don't know the required information.
+				s := t.TestEngine.TestEngineForkchoiceUpdatedV1(&ForkchoiceStateV1{
+					HeadBlockHash:      altChainPayloads[i].BlockHash,
+					SafeBlockHash:      cA.BlockHash,
+					FinalizedBlockHash: cA.BlockHash,
+				}, nil)
+				s.ExpectPayloadStatus(Syncing)
+				s.ExpectLatestValidHash(nil)
+			}
+		}
+
+		// Resend the latest correct fcU
+		r := t.TestEngine.TestEngineForkchoiceUpdatedV1(&t.CLMock.LatestForkchoice, nil)
+		r.ExpectNoError()
+	}
+}
+
 // Test to verify Block information available at the Eth RPC after NewPayload
 func blockStatusExecPayload(t *TestEnv) {
 	// Wait until this client catches up with latest PoS Block