diff --git a/simulators/ethereum/engine/clmock/clmock.go b/simulators/ethereum/engine/clmock/clmock.go index 474c9b05dc..203045c925 100644 --- a/simulators/ethereum/engine/clmock/clmock.go +++ b/simulators/ethereum/engine/clmock/clmock.go @@ -40,6 +40,20 @@ func (h ExecutableDataHistory) LatestPayloadNumber() uint64 { return latest } +func (h ExecutableDataHistory) LatestWithdrawalsIndex() uint64 { + latest := uint64(0) + for _, p := range h { + if p.Withdrawals != nil { + for _, w := range p.Withdrawals { + if w.Index > latest { + latest = w.Index + } + } + } + } + return latest +} + // Consensus Layer Client Mock used to sync the Execution Clients once the TTD has been reached type CLMocker struct { *hivesim.T diff --git a/simulators/ethereum/engine/helper/payload.go b/simulators/ethereum/engine/helper/payload.go index 995871cef6..78194f89f4 100644 --- a/simulators/ethereum/engine/helper/payload.go +++ b/simulators/ethereum/engine/helper/payload.go @@ -265,7 +265,7 @@ func GenerateInvalidPayload(basePayload *api.ExecutableData, payloadField Invali InvalidTransactionChainID: if len(basePayload.Transactions) == 0 { - return nil, fmt.Errorf("No transactions available for modification") + return nil, fmt.Errorf("no transactions available for modification") } var baseTx types.Transaction if err := baseTx.UnmarshalBinary(basePayload.Transactions[0]); err != nil { @@ -327,3 +327,17 @@ func GenerateInvalidPayload(basePayload *api.ExecutableData, payloadField Invali return alteredPayload, nil } + +/* + Generates an alternative withdrawals list that contains the same + amounts and accounts, but the order in the list is different, so + stateRoot of the resulting payload should be the same. +*/ +func RandomizeWithdrawalsOrder(src types.Withdrawals) types.Withdrawals { + dest := make(types.Withdrawals, len(src)) + perm := rand.Perm(len(src)) + for i, v := range perm { + dest[v] = src[i] + } + return dest +} diff --git a/simulators/ethereum/engine/suites/withdrawals/README.md b/simulators/ethereum/engine/suites/withdrawals/README.md index fb09da7ab2..e716425e03 100644 --- a/simulators/ethereum/engine/suites/withdrawals/README.md +++ b/simulators/ethereum/engine/suites/withdrawals/README.md @@ -173,11 +173,13 @@ All test cases contain the following verifications: - Payloads produced of the following characteristics - [x] 16 Transactions, 16 Withdrawals - [x] 0 Transactions, 0 Withdrawals + - Send extra payloads `32'` and `33'` such that `31 <- 32' <- 33'` using `engine_newPayloadV2` - Make multiple requests to obtain the payload bodies from the canonical chain (see `./tests.go` for full list). - Verify that: - Payload bodies of blocks before the Shanghai fork contain `withdrawals==null` - All transactions and withdrawals are in the correct format and order. - Requested payload bodies past the highest known block are ignored and absent from the returned list + - Payloads `32'` and `33'` are ignored by all requests since they are not part of the canonical chain. - Payload Bodies By Hash - Shanghai Fork on Block 16 - 16 Withdrawal Blocks - Launch client `A` and create a canonical chain consisting of 32 blocks, where the first shanghai block is number 17 diff --git a/simulators/ethereum/engine/suites/withdrawals/tests.go b/simulators/ethereum/engine/suites/withdrawals/tests.go index 332e4c6a61..c4d02bae78 100644 --- a/simulators/ethereum/engine/suites/withdrawals/tests.go +++ b/simulators/ethereum/engine/suites/withdrawals/tests.go @@ -530,17 +530,25 @@ var Tests = []test.SpecInterface{ Count: 1, }, GetPayloadBodyRequestByRange{ - Start: 15, + Start: 16, Count: 2, }, GetPayloadBodyRequestByRange{ - Start: 16, + Start: 17, Count: 16, }, GetPayloadBodyRequestByRange{ Start: 1, Count: 32, }, + GetPayloadBodyRequestByRange{ + Start: 31, + Count: 3, + }, + GetPayloadBodyRequestByRange{ + Start: 32, + Count: 2, + }, GetPayloadBodyRequestByRange{ Start: 33, Count: 1, @@ -560,6 +568,37 @@ var Tests = []test.SpecInterface{ }, }, + &GetPayloadBodiesSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "GetPayloadBodiesByRange (Sidechain)", + About: ` + Make multiple withdrawals to 16 accounts each payload. + Retrieve many of the payloads' bodies by number range. + Create a sidechain extending beyond the canonical chain block number. + `, + TimeoutSeconds: 240, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 17, + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 1024, + }, + GenerateSidechain: true, + GetPayloadBodiesRequests: []GetPayloadBodyRequest{ + GetPayloadBodyRequestByRange{ + Start: 33, + Count: 1, + }, + GetPayloadBodyRequestByRange{ + Start: 32, + Count: 2, + }, + }, + }, + &GetPayloadBodiesSpec{ WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ Spec: test.Spec{ @@ -572,14 +611,22 @@ var Tests = []test.SpecInterface{ SlotsToSafe: big.NewInt(32), SlotsToFinalized: big.NewInt(64), }, - WithdrawalsForkHeight: 17, - WithdrawalsBlockCount: 16, + WithdrawalsForkHeight: 2, + WithdrawalsBlockCount: 1, WithdrawalsPerBlock: 0, TransactionsPerBlock: common.Big0, }, GetPayloadBodiesRequests: []GetPayloadBodyRequest{ GetPayloadBodyRequestByRange{ - Start: 16, + Start: 1, + Count: 1, + }, + GetPayloadBodyRequestByRange{ + Start: 2, + Count: 1, + }, + GetPayloadBodyRequestByRange{ + Start: 1, Count: 2, }, }, @@ -782,6 +829,11 @@ func (ws *WithdrawalsBaseSpec) GetForkConfig() test.ForkConfig { } } +// Get the start account for all withdrawals. +func (ws *WithdrawalsBaseSpec) GetWithdrawalsStartAccount() *big.Int { + return big.NewInt(0x1000) +} + // Adds bytecode that unconditionally sets an storage key to specified account range func AddUnconditionalBytecode(g *core.Genesis, start *big.Int, end *big.Int) { for ; start.Cmp(end) <= 0; start.Add(start, common.Big1) { @@ -1082,7 +1134,7 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { // Produce requested post-shanghai blocks // (At least 1 block will be produced after this procedure ends). var ( - startAccount = big.NewInt(0x1000) + startAccount = ws.GetWithdrawalsStartAccount() nextIndex = uint64(0) ) @@ -1749,12 +1801,12 @@ func (s *MaxInitcodeSizeSpec) Execute(t *test.Env) { // client needs to sync and apply the withdrawals. type GetPayloadBodiesSpec struct { *WithdrawalsBaseSpec - GetPayloadBodiesRequests []GetPayloadBodyRequest + GenerateSidechain bool } type GetPayloadBodyRequest interface { - Verify(*test.Env) + Verify(*test.TestEngineClient, clmock.ExecutableDataHistory) } type GetPayloadBodyRequestByRange struct { @@ -1762,8 +1814,8 @@ type GetPayloadBodyRequestByRange struct { Count uint64 } -func (req GetPayloadBodyRequestByRange) Verify(t *test.Env) { - r := t.TestEngine.TestEngineGetPayloadBodiesByRangeV1(req.Start, req.Count) +func (req GetPayloadBodyRequestByRange) Verify(testEngine *test.TestEngineClient, payloadHistory clmock.ExecutableDataHistory) { + r := testEngine.TestEngineGetPayloadBodiesByRangeV1(req.Start, req.Count) if req.Start < 1 || req.Count < 1 { r.ExpectationDescription = fmt.Sprintf(` Sent start (%d) or count (%d) to engine_getPayloadBodiesByRangeV1 with a @@ -1772,19 +1824,21 @@ func (req GetPayloadBodyRequestByRange) Verify(t *test.Env) { r.ExpectErrorCode(InvalidParamsError) return } - if req.Start > t.CLMock.CurrentPayloadNumber { + latestPayloadNumber := payloadHistory.LatestPayloadNumber() + if req.Start > latestPayloadNumber { r.ExpectationDescription = fmt.Sprintf(` Sent start=%d and count=%d to engine_getPayloadBodiesByRangeV1, latest known block is %d, hence an empty list is expected. - `, req.Start, req.Count, t.CLMock.LatestExecutedPayload.Number) + `, req.Start, req.Count, latestPayloadNumber) r.ExpectPayloadBodiesCount(0) } else { var count = req.Count - if req.Start+req.Count-1 > t.CLMock.CurrentPayloadNumber { - count = t.CLMock.CurrentPayloadNumber - req.Start + 1 + if req.Start+req.Count-1 > latestPayloadNumber { + count = latestPayloadNumber - req.Start + 1 } + r.ExpectationDescription = fmt.Sprintf("Sent engine_getPayloadBodiesByRange(start=%d, count=%d), latest payload number in canonical chain is %d", req.Start, req.Count, latestPayloadNumber) r.ExpectPayloadBodiesCount(count) for i := req.Start; i < req.Start+count; i++ { - p := t.CLMock.ExecutedPayloadHistory[i] + p := payloadHistory[i] r.ExpectPayloadBody(i-req.Start, &client_types.ExecutionPayloadBodyV1{ Transactions: p.Transactions, @@ -1800,13 +1854,13 @@ type GetPayloadBodyRequestByHashIndex struct { End uint64 } -func (req GetPayloadBodyRequestByHashIndex) Verify(t *test.Env) { +func (req GetPayloadBodyRequestByHashIndex) Verify(testEngine *test.TestEngineClient, payloadHistory clmock.ExecutableDataHistory) { payloads := make([]*beacon.ExecutableData, 0) hashes := make([]common.Hash, 0) if len(req.BlockNumbers) > 0 { for _, n := range req.BlockNumbers { - if p, ok := t.CLMock.ExecutedPayloadHistory[n]; ok { - payloads = append(payloads, &p) + if p, ok := payloadHistory[n]; ok { + payloads = append(payloads, p) hashes = append(hashes, p.BlockHash) } else { // signal to request an unknown hash (random) @@ -1819,8 +1873,8 @@ func (req GetPayloadBodyRequestByHashIndex) Verify(t *test.Env) { } if req.Start > 0 && req.End > 0 { for n := req.Start; n <= req.End; n++ { - if p, ok := t.CLMock.ExecutedPayloadHistory[n]; ok { - payloads = append(payloads, &p) + if p, ok := payloadHistory[n]; ok { + payloads = append(payloads, p) hashes = append(hashes, p.BlockHash) } else { // signal to request an unknown hash (random) @@ -1835,7 +1889,7 @@ func (req GetPayloadBodyRequestByHashIndex) Verify(t *test.Env) { panic("invalid test") } - r := t.TestEngine.TestEngineGetPayloadBodiesByHashV1(hashes) + r := testEngine.TestEngineGetPayloadBodiesByHashV1(hashes) r.ExpectPayloadBodiesCount(uint64(len(payloads))) for i, p := range payloads { var expectedPayloadBody *client_types.ExecutionPayloadBodyV1 @@ -1854,8 +1908,61 @@ func (ws *GetPayloadBodiesSpec) Execute(t *test.Env) { // Do the base withdrawal test first, skipping base verifications ws.WithdrawalsBaseSpec.SkipBaseVerifications = true ws.WithdrawalsBaseSpec.Execute(t) + + payloadHistory := t.CLMock.ExecutedPayloadHistory + + if ws.GenerateSidechain { + + // First generate an extra payload on top of the canonical chain + // Generate more withdrawals + nextWithdrawals, _ := ws.GenerateWithdrawalsForBlock(payloadHistory.LatestWithdrawalsIndex(), ws.GetWithdrawalsStartAccount()) + + f := t.TestEngine.TestEngineForkchoiceUpdatedV2( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: t.CLMock.LatestHeader.Hash(), + }, + &beacon.PayloadAttributes{ + Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(), + Withdrawals: nextWithdrawals, + }, + ) + f.ExpectPayloadStatus(test.Valid) + + // Wait for payload to be built + time.Sleep(time.Second) + + // Get the next canonical payload + p := t.TestEngine.TestEngineGetPayloadV2(f.Response.PayloadID) + p.ExpectNoError() + nextCanonicalPayload := &p.Payload + + // Now we have an extra payload that follows the canonical chain, + // but we need a side chain for the test. + sidechainCurrent, err := helper.CustomizePayload(&t.CLMock.LatestExecutedPayload, &helper.CustomPayloadData{ + Withdrawals: helper.RandomizeWithdrawalsOrder(t.CLMock.LatestExecutedPayload.Withdrawals), + }) + if err != nil { + t.Fatalf("FAIL (%s): Error obtaining custom sidechain payload: %v", t.TestName, err) + } + + sidechainHead, err := helper.CustomizePayload(nextCanonicalPayload, &helper.CustomPayloadData{ + ParentHash: &sidechainCurrent.BlockHash, + Withdrawals: helper.RandomizeWithdrawalsOrder(nextCanonicalPayload.Withdrawals), + }) + if err != nil { + t.Fatalf("FAIL (%s): Error obtaining custom sidechain payload: %v", t.TestName, err) + } + + // Send both sidechain payloads as engine_newPayloadV2 + n1 := t.TestEngine.TestEngineNewPayloadV2(sidechainCurrent) + n1.ExpectStatus(test.Valid) + n2 := t.TestEngine.TestEngineNewPayloadV2(sidechainHead) + n2.ExpectStatus(test.Valid) + } + + // Now send the range request, which should ignore the sidechain for _, req := range ws.GetPayloadBodiesRequests { - req.Verify(t) + req.Verify(t.TestEngine, payloadHistory) } }