From 389338a6a2120d8453da8b9a205aa0b92131268d Mon Sep 17 00:00:00 2001 From: marioevz Date: Mon, 16 May 2022 23:47:40 +0000 Subject: [PATCH] simulators/ethereum/engine: add transition block checks --- simulators/ethereum/engine/enginetests.go | 185 +++++++++++++++------- simulators/ethereum/engine/testenv.go | 28 ++++ simulators/ethereum/engine/teststruct.go | 37 +++++ 3 files changed, 192 insertions(+), 58 deletions(-) diff --git a/simulators/ethereum/engine/enginetests.go b/simulators/ethereum/engine/enginetests.go index 2f66c5185e..9d3b30a57b 100644 --- a/simulators/ethereum/engine/enginetests.go +++ b/simulators/ethereum/engine/enginetests.go @@ -296,19 +296,39 @@ var engineTests = []TestSpec{ // Eth RPC Status on ForkchoiceUpdated Events { Name: "Latest Block after NewPayload", - Run: blockStatusExecPayload, + Run: blockStatusExecPayloadGen(false), + }, + { + Name: "Latest Block after NewPayload (Transition Block)", + Run: blockStatusExecPayloadGen(true), + TTD: 5, }, { Name: "Latest Block after New HeadBlock", - Run: blockStatusHeadBlock, + Run: blockStatusHeadBlockGen(false), + }, + { + Name: "Latest Block after New HeadBlock (Transition Block)", + Run: blockStatusHeadBlockGen(true), + TTD: 5, }, { Name: "Latest Block after New SafeBlock", - Run: blockStatusSafeBlock, + Run: blockStatusSafeBlockGen(false), + }, + { + Name: "Latest Block after New SafeBlock (Transition Block)", + Run: blockStatusSafeBlockGen(true), + TTD: 5, }, { Name: "Latest Block after New FinalizedBlock", - Run: blockStatusFinalizedBlock, + Run: blockStatusFinalizedBlockGen(false), + }, + { + Name: "Latest Block after New FinalizedBlock (Transition Block)", + Run: blockStatusFinalizedBlockGen(true), + TTD: 5, }, { Name: "Latest Block after Reorg", @@ -389,14 +409,20 @@ func invalidTerminalBlockForkchoiceUpdated(t *TestEnv) { r.ExpectPayloadStatus(InvalidTerminalBlock) r.ExpectLatestValidHash(nil) // ValidationError is not validated since it can be either null or a string message + + // Check that PoW chain progresses + t.verifyPoWProgress(gblock.Hash()) } // Invalid GetPayload Under PoW: Client must reject GetPayload directives under PoW. func invalidGetPayloadUnderPoW(t *TestEnv) { + gblock := loadGenesisBlock(t.ClientFiles["/genesis.json"]) // We start in PoW and try to get an invalid Payload, which should produce an error but nothing should be disrupted. r := t.TestEngine.TestEngineGetPayloadV1(&PayloadID{1, 2, 3, 4, 5, 6, 7, 8}) r.ExpectError() + // Check that PoW chain progresses + t.verifyPoWProgress(gblock.Hash()) } // Invalid Terminal Block in NewPayload: Client must reject NewPayload directives if the referenced ParentHash does not meet the TTD requirement. @@ -432,6 +458,9 @@ func invalidTerminalBlockNewPayload(t *TestEnv) { r.ExpectStatus(InvalidTerminalBlock) r.ExpectLatestValidHash(nil) // ValidationError is not validated since it can be either null or a string message + + // Check that PoW chain progresses + t.verifyPoWProgress(gblock.Hash()) } // Verify that a forkchoiceUpdated with a valid HeadBlock (previously sent using NewPayload) and unknown SafeBlock @@ -1207,82 +1236,122 @@ func invalidMissingAncestorReOrgGen(invalid_index int, payloadField InvalidPaylo } // 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 - t.CLMock.waitForTTD() - - // Produce blocks before starting the test - t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) +func blockStatusExecPayloadGen(transitionBlock bool) func(t *TestEnv) { + return func(t *TestEnv) { + // Wait until this client catches up with latest PoS Block + t.CLMock.waitForTTD() - // TODO: We can send a transaction and see if we get the transaction receipt after newPayload (we should not) - t.CLMock.produceSingleBlock(BlockProcessCallbacks{ - // Run test after the new payload has been broadcasted - OnNewPayloadBroadcast: func() { - r := t.TestEth.TestHeaderByNumber(nil) - r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) + // Produce blocks before starting the test, only if we are not testing the transition block + if !transitionBlock { + t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) + } - s := t.TestEth.TestBlockNumber() - s.ExpectNumber(t.CLMock.LatestFinalizedNumber.Uint64()) + var tx *types.Transaction + t.CLMock.produceSingleBlock(BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + tx = t.sendNextTransaction(t.TestEngine.Engine, (common.Address{}), big1, nil) + }, + // Run test after the new payload has been broadcasted + OnNewPayloadBroadcast: func() { + r := t.TestEth.TestHeaderByNumber(nil) + r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) - p := t.TestEth.TestBlockByNumber(nil) - p.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) + s := t.TestEth.TestBlockNumber() + s.ExpectNumber(t.CLMock.LatestFinalizedNumber.Uint64()) - }, - }) + p := t.TestEth.TestBlockByNumber(nil) + p.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) + // Check that the receipt for the transaction we just sent is still not available + q := t.TestEth.TestTransactionReceipt(tx.Hash()) + q.ExpectError() + }, + }) + } } // Test to verify Block information available at the Eth RPC after new HeadBlock ForkchoiceUpdated -func blockStatusHeadBlock(t *TestEnv) { - // Wait until this client catches up with latest PoS Block - t.CLMock.waitForTTD() +func blockStatusHeadBlockGen(transitionBlock bool) func(t *TestEnv) { + return func(t *TestEnv) { + // Wait until this client catches up with latest PoS Block + t.CLMock.waitForTTD() - // Produce blocks before starting the test - t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) + // Produce blocks before starting the test, only if we are not testing the transition block + if !transitionBlock { + t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) + } - t.CLMock.produceSingleBlock(BlockProcessCallbacks{ - // Run test after a forkchoice with new HeadBlockHash has been broadcasted - OnHeadBlockForkchoiceBroadcast: func() { - r := t.TestEth.TestHeaderByNumber(nil) - r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) + var tx *types.Transaction + t.CLMock.produceSingleBlock(BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + tx = t.sendNextTransaction(t.TestEngine.Engine, (common.Address{}), big1, nil) + }, + // Run test after a forkchoice with new HeadBlockHash has been broadcasted + OnHeadBlockForkchoiceBroadcast: func() { + r := t.TestEth.TestHeaderByNumber(nil) + r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) - }, - }) + s := t.TestEth.TestTransactionReceipt(tx.Hash()) + s.ExpectTransactionHash(tx.Hash()) + }, + }) + } } // Test to verify Block information available at the Eth RPC after new SafeBlock ForkchoiceUpdated -func blockStatusSafeBlock(t *TestEnv) { - // Wait until this client catches up with latest PoS Block - t.CLMock.waitForTTD() +func blockStatusSafeBlockGen(transitionBlock bool) func(t *TestEnv) { + return func(t *TestEnv) { + // Wait until this client catches up with latest PoS Block + t.CLMock.waitForTTD() - // Produce blocks before starting the test - t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) + // Produce blocks before starting the test, only if we are not testing the transition block + if !transitionBlock { + t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) + } - t.CLMock.produceSingleBlock(BlockProcessCallbacks{ - // Run test after a forkchoice with new SafeBlockHash has been broadcasted - OnSafeBlockForkchoiceBroadcast: func() { - r := t.TestEth.TestHeaderByNumber(nil) - r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) - }, - }) + var tx *types.Transaction + t.CLMock.produceSingleBlock(BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + tx = t.sendNextTransaction(t.TestEngine.Engine, (common.Address{}), big1, nil) + }, + // Run test after a forkchoice with new SafeBlockHash has been broadcasted + OnSafeBlockForkchoiceBroadcast: func() { + r := t.TestEth.TestHeaderByNumber(nil) + r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) + + s := t.TestEth.TestTransactionReceipt(tx.Hash()) + s.ExpectTransactionHash(tx.Hash()) + }, + }) + } } // Test to verify Block information available at the Eth RPC after new FinalizedBlock ForkchoiceUpdated -func blockStatusFinalizedBlock(t *TestEnv) { - // Wait until this client catches up with latest PoS Block - t.CLMock.waitForTTD() +func blockStatusFinalizedBlockGen(transitionBlock bool) func(t *TestEnv) { + return func(t *TestEnv) { + // Wait until this client catches up with latest PoS Block + t.CLMock.waitForTTD() - // Produce blocks before starting the test - t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) + // Produce blocks before starting the test, only if we are not testing the transition block + if !transitionBlock { + t.CLMock.produceBlocks(5, BlockProcessCallbacks{}) + } - t.CLMock.produceSingleBlock(BlockProcessCallbacks{ - // Run test after a forkchoice with new FinalizedBlockHash has been broadcasted - OnFinalizedBlockForkchoiceBroadcast: func() { - r := t.TestEth.TestHeaderByNumber(nil) - r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) + var tx *types.Transaction + t.CLMock.produceSingleBlock(BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + tx = t.sendNextTransaction(t.TestEngine.Engine, (common.Address{}), big1, nil) + }, + // Run test after a forkchoice with new FinalizedBlockHash has been broadcasted + OnFinalizedBlockForkchoiceBroadcast: func() { + r := t.TestEth.TestHeaderByNumber(nil) + r.ExpectHash(t.CLMock.LatestForkchoice.HeadBlockHash) - }, - }) + s := t.TestEth.TestTransactionReceipt(tx.Hash()) + s.ExpectTransactionHash(tx.Hash()) + }, + }) + } } diff --git a/simulators/ethereum/engine/testenv.go b/simulators/ethereum/engine/testenv.go index cff91bc688..86db4bc017 100644 --- a/simulators/ethereum/engine/testenv.go +++ b/simulators/ethereum/engine/testenv.go @@ -219,6 +219,34 @@ func (t *TestEnv) sendNextBigContractTransaction(sender *EngineClient, gasLimit } } +// Verify that the client progresses after a certain PoW block still in PoW mode +func (t *TestEnv) verifyPoWProgress(lastBlockHash common.Hash) { + // Get the block number first + lb, err := t.Eth.BlockByHash(t.Ctx(), lastBlockHash) + if err != nil { + t.Fatalf("FAIL (%s): Unable to fetch block: %v", t.TestName, err) + } + nextNum := lb.Number().Int64() + 1 + for { + nh, err := t.Eth.HeaderByNumber(t.Ctx(), big.NewInt(nextNum)) + if err == nil { + // Chain has progressed, check that the next block is also PoW + // Difficulty must NOT be zero + if nh.Difficulty.Cmp(big0) == 0 { + t.Fatalf("FAIL (%s): Expected PoW chain to progress in PoW mode, but following block difficulty==%v", t.TestName, nh.Difficulty) + } + // Chain is still PoW/Clique + return + } + t.Logf("INFO (%s): Error getting block, will try again: %v", t.TestName, err) + select { + case <-t.Timeout: + t.Fatalf("FAIL (%s): Timeout while waiting for PoW chain to progress", t.TestName) + case <-time.After(time.Second): + } + } +} + // CallContext is a helper method that forwards a raw RPC request to // the underlying RPC client. This can be used to call RPC methods // that are not supported by the ethclient.Client. diff --git a/simulators/ethereum/engine/teststruct.go b/simulators/ethereum/engine/teststruct.go index 6905733c9a..ee12dabb52 100644 --- a/simulators/ethereum/engine/teststruct.go +++ b/simulators/ethereum/engine/teststruct.go @@ -428,6 +428,43 @@ func (exp *StorageResponseExpectObject) ExpectStorageEqual(expStorage common.Has } } +// Transaction Receipt +type TransactionReceiptExpectObject struct { + *TestEnv + Call string + Receipt *types.Receipt + Error error +} + +func (teth *TestEthClient) TestTransactionReceipt(txHash common.Hash) *TransactionReceiptExpectObject { + receipt, err := teth.TransactionReceipt(teth.Ctx(), txHash) + return &TransactionReceiptExpectObject{ + TestEnv: teth.TestEnv, + Call: "TransactionReceipt", + Receipt: receipt, + Error: err, + } +} + +func (exp *TransactionReceiptExpectObject) ExpectError() { + if exp.Error == nil { + exp.Fatalf("FAIL (%s): Expected error on %s: block=%v", exp.TestName, exp.Call, exp.Receipt) + } +} + +func (exp *TransactionReceiptExpectObject) ExpectNoError() { + if exp.Error != nil { + exp.Fatalf("FAIL (%s): Unexpected error on %s: %v, expected=", exp.TestName, exp.Call, exp.Error) + } +} + +func (exp *TransactionReceiptExpectObject) ExpectTransactionHash(expectedHash common.Hash) { + exp.ExpectNoError() + if exp.Receipt.TxHash != expectedHash { + exp.Fatalf("FAIL (%s): Unexpected transaction hash on %s: %v, expected=%v", exp.TestName, exp.Call, exp.Receipt.TxHash, expectedHash) + } +} + // Ctx returns a context with the default timeout. // For subsequent calls to Ctx, it also cancels the previous context. func (t *TestEthClient) Ctx() context.Context {