diff --git a/simulators/ethereum/engine/client/engine.go b/simulators/ethereum/engine/client/engine.go index c8b353babb..502d53a79f 100644 --- a/simulators/ethereum/engine/client/engine.go +++ b/simulators/ethereum/engine/client/engine.go @@ -34,7 +34,7 @@ type Engine interface { GetPayloadV1(ctx context.Context, payloadId *api.PayloadID) (api.ExecutableData, error) GetPayloadV2(ctx context.Context, payloadId *api.PayloadID) (api.ExecutableData, *big.Int, error) - NewPayloadV1(ctx context.Context, payload *api.ExecutableData) (api.PayloadStatusV1, error) + NewPayloadV1(ctx context.Context, payload *client_types.ExecutableDataV1) (api.PayloadStatusV1, error) NewPayloadV2(ctx context.Context, payload *api.ExecutableData) (api.PayloadStatusV1, error) GetPayloadBodiesByRangeV1(ctx context.Context, start uint64, count uint64) ([]*client_types.ExecutionPayloadBodyV1, error) diff --git a/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go b/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go index cfbb1f5df0..a5d98504f0 100644 --- a/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go +++ b/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go @@ -366,7 +366,7 @@ func (ec *HiveRPCEngineClient) GetPayloadBodiesByRangeV1(ctx context.Context, st return nil, err } - err = ec.c.CallContext(ctx, &result, "engine_getPayloadBodiesByRangeV1", start, count) + err = ec.c.CallContext(ctx, &result, "engine_getPayloadBodiesByRangeV1", hexutil.Uint64(start), hexutil.Uint64(count)) return result, err } @@ -383,23 +383,25 @@ func (ec *HiveRPCEngineClient) GetPayloadBodiesByHashV1(ctx context.Context, has return result, err } -func (ec *HiveRPCEngineClient) NewPayload(ctx context.Context, version int, payload *api.ExecutableData) (api.PayloadStatusV1, error) { +func (ec *HiveRPCEngineClient) newPayload(ctx context.Context, version int, payload interface{}) (api.PayloadStatusV1, error) { var result api.PayloadStatusV1 if err := ec.PrepareDefaultAuthCallToken(); err != nil { return result, err } - ec.latestPayloadSent = payload err := ec.c.CallContext(ctx, &result, fmt.Sprintf("engine_newPayloadV%d", version), payload) ec.latestPayloadStatusReponse = &result return result, err } -func (ec *HiveRPCEngineClient) NewPayloadV1(ctx context.Context, payload *api.ExecutableData) (api.PayloadStatusV1, error) { - return ec.NewPayload(ctx, 1, payload) +func (ec *HiveRPCEngineClient) NewPayloadV1(ctx context.Context, payload *client_types.ExecutableDataV1) (api.PayloadStatusV1, error) { + ed := payload.ToExecutableData() + ec.latestPayloadSent = &ed + return ec.newPayload(ctx, 1, payload) } func (ec *HiveRPCEngineClient) NewPayloadV2(ctx context.Context, payload *api.ExecutableData) (api.PayloadStatusV1, error) { - return ec.NewPayload(ctx, 2, payload) + ec.latestPayloadSent = payload + return ec.newPayload(ctx, 2, payload) } func (ec *HiveRPCEngineClient) ExchangeTransitionConfigurationV1(ctx context.Context, tConf *api.TransitionConfigurationV1) (api.TransitionConfigurationV1, error) { diff --git a/simulators/ethereum/engine/client/node/node.go b/simulators/ethereum/engine/client/node/node.go index f08cf623c7..6f46e6fdeb 100644 --- a/simulators/ethereum/engine/client/node/node.go +++ b/simulators/ethereum/engine/client/node/node.go @@ -690,9 +690,10 @@ func (n *GethNode) SetBlock(block *types.Block, parentNumber uint64, parentRoot } // Engine API -func (n *GethNode) NewPayloadV1(ctx context.Context, pl *beacon.ExecutableData) (beacon.PayloadStatusV1, error) { - n.latestPayloadSent = pl - resp, err := n.api.NewPayloadV1(*pl) +func (n *GethNode) NewPayloadV1(ctx context.Context, pl *client_types.ExecutableDataV1) (beacon.PayloadStatusV1, error) { + ed := pl.ToExecutableData() + n.latestPayloadSent = &ed + resp, err := n.api.NewPayloadV1(ed) n.latestPayloadStatusReponse = &resp return resp, err } diff --git a/simulators/ethereum/engine/client/types/gen_edv1.go b/simulators/ethereum/engine/client/types/gen_edv1.go new file mode 100644 index 0000000000..daad47aba8 --- /dev/null +++ b/simulators/ethereum/engine/client/types/gen_edv1.go @@ -0,0 +1,139 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package types + +import ( + "encoding/json" + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +var _ = (*executableDataV1Marshaling)(nil) + +// MarshalJSON marshals as JSON. +func (e ExecutableDataV1) MarshalJSON() ([]byte, error) { + type ExecutableDataV1 struct { + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + } + var enc ExecutableDataV1 + enc.ParentHash = e.ParentHash + enc.FeeRecipient = e.FeeRecipient + enc.StateRoot = e.StateRoot + enc.ReceiptsRoot = e.ReceiptsRoot + enc.LogsBloom = e.LogsBloom + enc.Random = e.Random + enc.Number = hexutil.Uint64(e.Number) + enc.GasLimit = hexutil.Uint64(e.GasLimit) + enc.GasUsed = hexutil.Uint64(e.GasUsed) + enc.Timestamp = hexutil.Uint64(e.Timestamp) + enc.ExtraData = e.ExtraData + enc.BaseFeePerGas = (*hexutil.Big)(e.BaseFeePerGas) + enc.BlockHash = e.BlockHash + if e.Transactions != nil { + enc.Transactions = make([]hexutil.Bytes, len(e.Transactions)) + for k, v := range e.Transactions { + enc.Transactions[k] = v + } + } + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (e *ExecutableDataV1) UnmarshalJSON(input []byte) error { + type ExecutableDataV1 struct { + ParentHash *common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient *common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot *common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot *common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom *hexutil.Bytes `json:"logsBloom" gencodec:"required"` + Random *common.Hash `json:"prevRandao" gencodec:"required"` + Number *hexutil.Uint64 `json:"blockNumber" gencodec:"required"` + GasLimit *hexutil.Uint64 `json:"gasLimit" gencodec:"required"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` + ExtraData *hexutil.Bytes `json:"extraData" gencodec:"required"` + BaseFeePerGas *hexutil.Big `json:"baseFeePerGas" gencodec:"required"` + BlockHash *common.Hash `json:"blockHash" gencodec:"required"` + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + } + var dec ExecutableDataV1 + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.ParentHash == nil { + return errors.New("missing required field 'parentHash' for ExecutableDataV1") + } + e.ParentHash = *dec.ParentHash + if dec.FeeRecipient == nil { + return errors.New("missing required field 'feeRecipient' for ExecutableDataV1") + } + e.FeeRecipient = *dec.FeeRecipient + if dec.StateRoot == nil { + return errors.New("missing required field 'stateRoot' for ExecutableDataV1") + } + e.StateRoot = *dec.StateRoot + if dec.ReceiptsRoot == nil { + return errors.New("missing required field 'receiptsRoot' for ExecutableDataV1") + } + e.ReceiptsRoot = *dec.ReceiptsRoot + if dec.LogsBloom == nil { + return errors.New("missing required field 'logsBloom' for ExecutableDataV1") + } + e.LogsBloom = *dec.LogsBloom + if dec.Random == nil { + return errors.New("missing required field 'prevRandao' for ExecutableDataV1") + } + e.Random = *dec.Random + if dec.Number == nil { + return errors.New("missing required field 'blockNumber' for ExecutableDataV1") + } + e.Number = uint64(*dec.Number) + if dec.GasLimit == nil { + return errors.New("missing required field 'gasLimit' for ExecutableDataV1") + } + e.GasLimit = uint64(*dec.GasLimit) + if dec.GasUsed == nil { + return errors.New("missing required field 'gasUsed' for ExecutableDataV1") + } + e.GasUsed = uint64(*dec.GasUsed) + if dec.Timestamp == nil { + return errors.New("missing required field 'timestamp' for ExecutableDataV1") + } + e.Timestamp = uint64(*dec.Timestamp) + if dec.ExtraData == nil { + return errors.New("missing required field 'extraData' for ExecutableDataV1") + } + e.ExtraData = *dec.ExtraData + if dec.BaseFeePerGas == nil { + return errors.New("missing required field 'baseFeePerGas' for ExecutableDataV1") + } + e.BaseFeePerGas = (*big.Int)(dec.BaseFeePerGas) + if dec.BlockHash == nil { + return errors.New("missing required field 'blockHash' for ExecutableDataV1") + } + e.BlockHash = *dec.BlockHash + if dec.Transactions == nil { + return errors.New("missing required field 'transactions' for ExecutableDataV1") + } + e.Transactions = make([][]byte, len(dec.Transactions)) + for k, v := range dec.Transactions { + e.Transactions[k] = v + } + return nil +} diff --git a/simulators/ethereum/engine/client/types/types.go b/simulators/ethereum/engine/client/types/types.go index 7346f65dbd..8c3581996c 100644 --- a/simulators/ethereum/engine/client/types/types.go +++ b/simulators/ethereum/engine/client/types/types.go @@ -1,7 +1,11 @@ package types import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/go-ethereum/core/types" ) @@ -15,3 +19,74 @@ type ExecutionPayloadBodyV1 struct { type executionPayloadBodyV1Marshaling struct { Transactions []hexutil.Bytes } + +// ExecutableData is the data necessary to execute an EL payload. +// +//go:generate go run github.com/fjl/gencodec -type ExecutableDataV1 -field-override executableDataV1Marshaling -out gen_edv1.go +type ExecutableDataV1 struct { + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` +} + +// JSON type overrides for executableData. +type executableDataV1Marshaling struct { + Number hexutil.Uint64 + GasLimit hexutil.Uint64 + GasUsed hexutil.Uint64 + Timestamp hexutil.Uint64 + BaseFeePerGas *hexutil.Big + ExtraData hexutil.Bytes + LogsBloom hexutil.Bytes + Transactions []hexutil.Bytes +} + +func (edv1 *ExecutableDataV1) ToExecutableData() beacon.ExecutableData { + return beacon.ExecutableData{ + ParentHash: edv1.ParentHash, + FeeRecipient: edv1.FeeRecipient, + StateRoot: edv1.StateRoot, + ReceiptsRoot: edv1.ReceiptsRoot, + LogsBloom: edv1.LogsBloom, + Random: edv1.Random, + Number: edv1.Number, + GasLimit: edv1.GasLimit, + GasUsed: edv1.GasUsed, + Timestamp: edv1.Timestamp, + ExtraData: edv1.ExtraData, + BaseFeePerGas: edv1.BaseFeePerGas, + BlockHash: edv1.BlockHash, + Transactions: edv1.Transactions, + } +} + +func (edv1 *ExecutableDataV1) FromExecutableData(ed *beacon.ExecutableData) { + if ed.Withdrawals != nil { + panic("source executable data contains withdrawals, not supported by V1") + } + edv1.ParentHash = ed.ParentHash + edv1.FeeRecipient = ed.FeeRecipient + edv1.StateRoot = ed.StateRoot + edv1.ReceiptsRoot = ed.ReceiptsRoot + edv1.LogsBloom = ed.LogsBloom + edv1.Random = ed.Random + edv1.Number = ed.Number + edv1.GasLimit = ed.GasLimit + edv1.GasUsed = ed.GasUsed + edv1.Timestamp = ed.Timestamp + edv1.ExtraData = ed.ExtraData + edv1.BaseFeePerGas = ed.BaseFeePerGas + edv1.BlockHash = ed.BlockHash + edv1.Transactions = ed.Transactions +} diff --git a/simulators/ethereum/engine/clmock/clmock.go b/simulators/ethereum/engine/clmock/clmock.go index 7e3b82daf5..b9e96ab4ee 100644 --- a/simulators/ethereum/engine/clmock/clmock.go +++ b/simulators/ethereum/engine/clmock/clmock.go @@ -11,6 +11,7 @@ import ( api "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/hive/simulators/ethereum/engine/client" + client_types "github.com/ethereum/hive/simulators/ethereum/engine/client/types" "github.com/ethereum/hive/simulators/ethereum/engine/globals" "github.com/ethereum/hive/simulators/ethereum/engine/helper" @@ -28,6 +29,32 @@ var ( DefaultPayloadProductionClientDelay = time.Second ) +type ExecutableDataHistory map[uint64]*api.ExecutableData + +func (h ExecutableDataHistory) LatestPayloadNumber() uint64 { + latest := uint64(0) + for n := range h { + if n > latest { + latest = n + } + } + 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 @@ -53,7 +80,7 @@ type CLMocker struct { // PoS Chain History Information PrevRandaoHistory map[uint64]common.Hash - ExecutedPayloadHistory map[uint64]api.ExecutableData + ExecutedPayloadHistory ExecutableDataHistory HeadHashHistory []common.Hash // Latest broadcasted data using the PoS Engine API @@ -103,7 +130,7 @@ func NewCLMocker(t *hivesim.T, slotsToSafe, slotsToFinalized, safeSlotsToImportO T: t, EngineClients: make([]client.EngineClient, 0), PrevRandaoHistory: map[uint64]common.Hash{}, - ExecutedPayloadHistory: map[uint64]api.ExecutableData{}, + ExecutedPayloadHistory: ExecutableDataHistory{}, SlotsToSafe: slotsToSafe, SlotsToFinalized: slotsToFinalized, SafeSlotsToImportOptimistically: safeSlotsToImportOptimistically, @@ -392,7 +419,8 @@ func (cl *CLMocker) broadcastNextNewPayload() { } } cl.LatestExecutedPayload = cl.LatestPayloadBuilt - cl.ExecutedPayloadHistory[cl.LatestPayloadBuilt.Number] = cl.LatestPayloadBuilt + payload := cl.LatestPayloadBuilt + cl.ExecutedPayloadHistory[cl.LatestPayloadBuilt.Number] = &payload } func (cl *CLMocker) broadcastLatestForkchoice() { @@ -581,7 +609,9 @@ func (cl *CLMocker) BroadcastNewPayload(payload *api.ExecutableData) []ExecutePa if isShanghai(payload.Timestamp, cl.ShanghaiTimestamp) { execPayloadResp, err = ec.NewPayloadV2(ctx, payload) } else { - execPayloadResp, err = ec.NewPayloadV1(ctx, payload) + edv1 := &client_types.ExecutableDataV1{} + edv1.FromExecutableData(payload) + execPayloadResp, err = ec.NewPayloadV1(ctx, edv1) } if err != nil { cl.Errorf("CLMocker: Could not ExecutePayloadV1: %v", err) diff --git a/simulators/ethereum/engine/helper/payload.go b/simulators/ethereum/engine/helper/payload.go index 0a9a86b22f..78194f89f4 100644 --- a/simulators/ethereum/engine/helper/payload.go +++ b/simulators/ethereum/engine/helper/payload.go @@ -15,21 +15,22 @@ import ( ) type CustomPayloadData struct { - ParentHash *common.Hash - FeeRecipient *common.Address - StateRoot *common.Hash - ReceiptsRoot *common.Hash - LogsBloom *[]byte - PrevRandao *common.Hash - Number *uint64 - GasLimit *uint64 - GasUsed *uint64 - Timestamp *uint64 - ExtraData *[]byte - BaseFeePerGas *big.Int - BlockHash *common.Hash - Transactions *[][]byte - Withdrawals types.Withdrawals + ParentHash *common.Hash + FeeRecipient *common.Address + StateRoot *common.Hash + ReceiptsRoot *common.Hash + LogsBloom *[]byte + PrevRandao *common.Hash + Number *uint64 + GasLimit *uint64 + GasUsed *uint64 + Timestamp *uint64 + ExtraData *[]byte + BaseFeePerGas *big.Int + BlockHash *common.Hash + Transactions *[][]byte + Withdrawals types.Withdrawals + RemoveWithdrawals bool } // Construct a customized payload by taking an existing payload as base and mixing it CustomPayloadData @@ -100,7 +101,9 @@ func CustomizePayload(basePayload *api.ExecutableData, customData *CustomPayload if customData.BaseFeePerGas != nil { customPayloadHeader.BaseFee = customData.BaseFeePerGas } - if customData.Withdrawals != nil { + if customData.RemoveWithdrawals { + customPayloadHeader.WithdrawalsHash = nil + } else if customData.Withdrawals != nil { h := types.DeriveSha(customData.Withdrawals, trie.NewStackTrie(nil)) customPayloadHeader.WithdrawalsHash = &h } else if basePayload.Withdrawals != nil { @@ -125,7 +128,9 @@ func CustomizePayload(basePayload *api.ExecutableData, customData *CustomPayload BlockHash: customPayloadHeader.Hash(), Transactions: txs, } - if customData.Withdrawals != nil { + if customData.RemoveWithdrawals { + result.Withdrawals = nil + } else if customData.Withdrawals != nil { result.Withdrawals = customData.Withdrawals } else if basePayload.Withdrawals != nil { result.Withdrawals = basePayload.Withdrawals @@ -260,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 { @@ -311,7 +316,8 @@ func GenerateInvalidPayload(basePayload *api.ExecutableData, payloadField Invali } if customPayloadMod == nil { - return basePayload, nil + copyPayload := *basePayload + return ©Payload, nil } alteredPayload, err := CustomizePayload(basePayload, customPayloadMod) @@ -321,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/engine/tests.go b/simulators/ethereum/engine/suites/engine/tests.go index 703862e891..df93b3d98c 100644 --- a/simulators/ethereum/engine/suites/engine/tests.go +++ b/simulators/ethereum/engine/suites/engine/tests.go @@ -11,6 +11,7 @@ import ( "github.com/ethereum/hive/simulators/ethereum/engine/client" "github.com/ethereum/hive/simulators/ethereum/engine/client/hive_rpc" "github.com/ethereum/hive/simulators/ethereum/engine/client/node" + client_types "github.com/ethereum/hive/simulators/ethereum/engine/client/types" "github.com/ethereum/hive/simulators/ethereum/engine/clmock" "github.com/ethereum/hive/simulators/ethereum/engine/globals" "github.com/ethereum/hive/simulators/ethereum/engine/helper" @@ -1679,7 +1680,7 @@ func invalidTransitionPayload(t *test.Env) { // process simply to be able to re-org back. OnGetPayload: func() { basePayload := t.CLMock.ExecutedPayloadHistory[t.CLMock.FirstPoSBlockNumber.Uint64()] - alteredPayload, err := helper.GenerateInvalidPayload(&basePayload, helper.InvalidStateRoot) + alteredPayload, err := helper.GenerateInvalidPayload(basePayload, helper.InvalidStateRoot) if err != nil { t.Fatalf("FAIL (%s): Unable to modify payload: %v", t.TestName, err) } @@ -2213,9 +2214,9 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { if len(altChainPayloads) == invalidIndex { var uncle *types.Block if spec.PayloadField == helper.InvalidOmmers { - if unclePayload, ok := t.CLMock.ExecutedPayloadHistory[sideBlock.NumberU64()-1]; ok { + if unclePayload, ok := t.CLMock.ExecutedPayloadHistory[sideBlock.NumberU64()-1]; ok && unclePayload != nil { // Uncle is a PoS payload - uncle, err = api.ExecutableDataToBlock(unclePayload) + uncle, err = api.ExecutableDataToBlock(*unclePayload) if err != nil { t.Fatalf("FAIL (%s): Unable to get uncle block: %v", t.TestName, err) } @@ -2265,7 +2266,9 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { defer cancel() p := api.BlockToExecutableData(altChainPayloads[i], common.Big0).ExecutionPayload - status, err := secondaryClient.NewPayloadV1(ctx, p) + pv1 := &client_types.ExecutableDataV1{} + pv1.FromExecutableData(p) + status, err := secondaryClient.NewPayloadV1(ctx, pv1) if err != nil { t.Fatalf("FAIL (%s): TEST ISSUE - Unable to send new payload: %v", t.TestName, err) } @@ -2388,7 +2391,7 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { // We need to send the canonical chain to the main client here for i := t.CLMock.FirstPoSBlockNumber.Uint64(); i <= t.CLMock.LatestExecutedPayload.Number; i++ { if payload, ok := t.CLMock.ExecutedPayloadHistory[i]; ok { - r := t.TestEngine.TestEngineNewPayloadV1(&payload) + r := t.TestEngine.TestEngineNewPayloadV1(payload) r.ExpectStatus(test.Valid) } } @@ -3181,7 +3184,7 @@ func reExecPayloads(t *test.Env) { t.Fatalf("FAIL (%s): (test issue) Payload with index %d does not exist", i) } - r := t.TestEngine.TestEngineNewPayloadV1(&payload) + r := t.TestEngine.TestEngineNewPayloadV1(payload) r.ExpectStatus(test.Valid) r.ExpectLatestValidHash(&payload.BlockHash) } @@ -3292,7 +3295,7 @@ func inOrderPayloads(t *test.Env) { for k := t.CLMock.FirstPoSBlockNumber.Uint64(); k <= t.CLMock.LatestExecutedPayload.Number; k++ { payload := t.CLMock.ExecutedPayloadHistory[k] - s := secondaryTestEngineClient.TestEngineNewPayloadV1(&payload) + s := secondaryTestEngineClient.TestEngineNewPayloadV1(payload) s.ExpectStatus(test.Valid) s.ExpectLatestValidHash(&payload.BlockHash) @@ -3418,7 +3421,7 @@ func missingFcu(t *test.Env) { // Send each payload in the correct order but skip the ForkchoiceUpdated for each for i := t.CLMock.FirstPoSBlockNumber.Uint64(); i <= t.CLMock.LatestHeadNumber.Uint64(); i++ { payload := t.CLMock.ExecutedPayloadHistory[i] - p := secondaryEngineTest.TestEngineNewPayloadV1(&payload) + p := secondaryEngineTest.TestEngineNewPayloadV1(payload) p.ExpectStatus(test.Valid) p.ExpectLatestValidHash(&payload.BlockHash) } diff --git a/simulators/ethereum/engine/suites/sync/tests.go b/simulators/ethereum/engine/suites/sync/tests.go index 95414240ef..8afe77e700 100644 --- a/simulators/ethereum/engine/suites/sync/tests.go +++ b/simulators/ethereum/engine/suites/sync/tests.go @@ -201,7 +201,7 @@ func incrementalPostMergeSync(t *test.Env) { t.Fatalf("FAIL (%s): TEST ISSUE - Payload not found: %d", t.TestName, i) } for { - secondaryEngineTest.TestEngineNewPayloadV1(&payload) + secondaryEngineTest.TestEngineNewPayloadV1(payload) secondaryEngineTest.TestEngineForkchoiceUpdatedV1(&api.ForkchoiceStateV1{ HeadBlockHash: payload.BlockHash, }, nil) diff --git a/simulators/ethereum/engine/suites/withdrawals/README.md b/simulators/ethereum/engine/suites/withdrawals/README.md index 8b3ee53d7d..a5491eaaa5 100644 --- a/simulators/ethereum/engine/suites/withdrawals/README.md +++ b/simulators/ethereum/engine/suites/withdrawals/README.md @@ -6,8 +6,18 @@ This test suite verifies behavior of the Engine API on each client after the Sha ## Shanghai Fork - Genesis Shanghai: Tests the withdrawals fork happening since genesis (e.g. on a testnet). - Shanghai Fork on Block 1: Tests the withdrawals fork happening directly after genesis. -- Shanghai Fork on Block 2: Tests the transition to the withdrawals fork after a single block has happened. Block 1 is sent with invalid non-null withdrawals payload and client is expected to respond with the appropriate error. -- Shanghai Fork on Block 3: Tests the transition to the withdrawals fork after two blocks have happened. Block 2 is sent with invalid non-null withdrawals payload (both in `engine_newPayloadV2` and the attributes of `engine_forkchoiceUpdatedV2`) and client is expected to respond with the appropriate error. +- Shanghai Fork on Block 2: Tests the transition to the withdrawals fork after a single block has happened. Block 1 is used to send invalid non-null withdrawals payload. +- Shanghai Fork on Block 3: Tests the transition to the withdrawals fork after two blocks have happened. Block 1 and 2 are used to send invalid non-null withdrawals payload. +- `INVALID` response on corrupted block hash on `engine_newPayloadV2` + +All test cases contain following verifications: +- Verify client responds with error -32602 on the following scenarios: + - Send `ExecutionPayloadV2` using a custom valid (correct BlockHash) execution payload that includes an empty array for withdrawals on `timestamp < SHANGHAI_TIMESTAMP` using `engine_newPayloadV2` + - Send `PayloadAttributesV2` using an empty array for withdrawals on `timestamp < SHANGHAI_TIMESTAMP` using `engine_forkchoiceUpdatedV2` + - Send `ExecutionPayloadV1` using a custom valid (correct BlockHash) execution payload that includes `null` as withdrawals on `timestamp >= SHANGHAI_TIMESTAMP` using `engine_newPayloadV2` + - Send `PayloadAttributesV2` using `null` for withdrawals on `timestamp < SHANGHAI_TIMESTAMP` using `engine_forkchoiceUpdatedV2` +- Use `engine_forkchoiceUpdatedV2` and `engine_newPayloadV2` to send pre-Shanghai payloads/payload attributes, and verify method call succeeds. +- Use `engine_getPayloadV2` to get a pre-Shanghai payload, and verify method call succeeds ## Withdrawals - Withdraw to a single account: Make multiple withdrawals to a single account. @@ -16,6 +26,11 @@ This test suite verifies behavior of the Engine API on each client after the Sha - Withdraw zero amount: Make multiple withdrawals where the amount withdrawn is 0. - Empty Withdrawals: Produce withdrawals block with zero withdrawals. +All test cases contain the following verifications: +- Verify all withdrawal amounts (sent in Gwei) correctly translate to a wei balance increase in the execution client (after the payload has been set as head using `engine_forkchoiceUpdatedV2`). +- Verify using `eth_getBalance` that the balance of all withdrawn accounts match the expected amount on each block of the chain and `latest`. +- Payload returned by `engine_getPayloadV2` contains the same list of withdrawals as were passed by `PayloadAttributesV2` in the `engine_forkchoiceUpdatedV2` method call. + ## Sync to Shanghai - Sync after 2 blocks - Shanghai on Block 1 - Single Withdrawal Account - No Transactions: - Spawn a first client @@ -160,11 +175,13 @@ This test suite verifies behavior of the Engine API on each client after the Sha - 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 @@ -176,3 +193,12 @@ This test suite verifies behavior of the Engine API on each client after the Sha - 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 of unknown hashes are returned as null in the returned list + +## Block Value Tests +- Block Value on GetPayloadV2 Post-Shanghai + - Create a Shanghai chain where the fork transition happens at block 1 + - Send transactions, submit forkchoice and get payload built + - Verify transactions were included in payload created + - Set forkchoice head to the new payload + - Calculate the block value by requesting the transaction receipts + - Verify that the `blockValue` returned by `engine_getPayloadV2` matches the expected calculated value diff --git a/simulators/ethereum/engine/suites/withdrawals/tests.go b/simulators/ethereum/engine/suites/withdrawals/tests.go index 4285e8a80c..53f11946ee 100644 --- a/simulators/ethereum/engine/suites/withdrawals/tests.go +++ b/simulators/ethereum/engine/suites/withdrawals/tests.go @@ -172,6 +172,32 @@ var Tests = []test.SpecInterface{ WithdrawalsPerBlock: 0, }, + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Corrupted Block Hash Payload (INVALID)", + About: ` + Send a valid payload with a corrupted hash using engine_newPayloadV2. + `, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 1, + TestCorrupedHashPayloads: true, + }, + + // Block value tests + &BlockValueSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "GetPayloadV2 Block Value", + About: ` + Verify the block value returned in GetPayloadV2. + `, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 1, + }, + }, + // Sync Tests &WithdrawalsSyncSpec{ WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ @@ -516,17 +542,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, @@ -546,6 +580,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{ @@ -558,14 +623,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, }, }, @@ -600,7 +673,7 @@ var Tests = []test.SpecInterface{ Start: 1, End: 32, }, - GetPayloadBodyRequestByHashIndex{ + GetPayloadBodyRequestByHashIndex{ // Existing+Random hashes BlockNumbers: []uint64{ 32, 1000, @@ -610,6 +683,16 @@ var Tests = []test.SpecInterface{ 1000, }, }, + GetPayloadBodyRequestByHashIndex{ // All Random hashes + BlockNumbers: []uint64{ + 1000, + 1000, + 1000, + 1000, + 1000, + 1000, + }, + }, }, }, @@ -739,6 +822,7 @@ type WithdrawalsBaseSpec struct { WithdrawalsHistory WithdrawalsHistory // Internal withdrawals history that keeps track of all withdrawals WithdrawAmounts []uint64 // Amounts of withdrawn wei on each withdrawal (round-robin) TransactionsPerBlock *big.Int // Amount of test transactions to include in withdrawal blocks + TestCorrupedHashPayloads bool // Send a valid payload with corrupted hash SkipBaseVerifications bool // For code reuse of the base spec procedure } @@ -768,6 +852,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) { @@ -997,18 +1086,38 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { HeadBlockHash: t.CLMock.LatestHeader.Hash(), }, &beacon.PayloadAttributes{ - Timestamp: t.CLMock.LatestHeader.Time + 1, + Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(), Random: common.Hash{}, SuggestedFeeRecipient: common.Address{}, Withdrawals: make(types.Withdrawals, 0), }, ) - r.ExpectationDescription = "Sent pre-shanghai fcu using EngineForkchoiceUpdatedV2+Withdrawals, error is expected" + r.ExpectationDescription = "Sent pre-shanghai Forkchoice using ForkchoiceUpdatedV2 + Withdrawals, error is expected" r.ExpectErrorCode(InvalidParamsError) + + // Send a valid Pre-Shanghai request using ForkchoiceUpdatedV2 + // (CLMock uses V1 by default) + r = t.TestEngine.TestEngineForkchoiceUpdatedV2( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: t.CLMock.LatestHeader.Hash(), + }, + &beacon.PayloadAttributes{ + Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(), + Random: common.Hash{}, + SuggestedFeeRecipient: common.Address{}, + Withdrawals: nil, + }, + ) + r.ExpectationDescription = "Sent pre-shanghai Forkchoice ForkchoiceUpdatedV2 + null withdrawals, no error is expected" + r.ExpectNoError() } }, OnGetPayload: func() { if !ws.SkipBaseVerifications { + // Try to get the same payload but use `engine_getPayloadV2` + g := t.TestEngine.TestEngineGetPayloadV2(t.CLMock.NextPayloadID) + g.ExpectPayload(&t.CLMock.LatestPayloadBuilt) + // Send produced payload but try to include non-nil // `withdrawals`, it should fail. emptyWithdrawalsList := make(types.Withdrawals, 0) @@ -1021,6 +1130,11 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { r := t.TestEngine.TestEngineNewPayloadV2(payloadPlusWithdrawals) r.ExpectationDescription = "Sent pre-shanghai payload using NewPayloadV2+Withdrawals, error is expected" r.ExpectErrorCode(InvalidParamsError) + + // Send valid ExecutionPayloadV1 using engine_newPayloadV2 + r = t.TestEngine.TestEngineNewPayloadV2(&t.CLMock.LatestPayloadBuilt) + r.ExpectationDescription = "Sent pre-shanghai payload using NewPayloadV2, no error is expected" + r.ExpectStatus(test.Valid) } }, OnNewPayloadBroadcast: func() { @@ -1047,12 +1161,31 @@ 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) ) t.CLMock.ProduceBlocks(int(ws.WithdrawalsBlockCount), clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { + + if !ws.SkipBaseVerifications { + // Try to send a PayloadAttributesV1 with null withdrawals after + // Shanghai + r := t.TestEngine.TestEngineForkchoiceUpdatedV2( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: t.CLMock.LatestHeader.Hash(), + }, + &beacon.PayloadAttributes{ + Timestamp: t.CLMock.LatestHeader.Time + ws.GetBlockTimeIncrements(), + Random: common.Hash{}, + SuggestedFeeRecipient: common.Address{}, + Withdrawals: nil, + }, + ) + r.ExpectationDescription = "Sent shanghai fcu using PayloadAttributesV1, error is expected" + r.ExpectErrorCode(InvalidParamsError) + } + // Send some withdrawals t.CLMock.NextWithdrawals, nextIndex = ws.GenerateWithdrawalsForBlock(nextIndex, startAccount) ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber] = t.CLMock.NextWithdrawals @@ -1078,7 +1211,38 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { } }, OnGetPayload: func() { - // TODO: Send new payload with `withdrawals=null` and expect error + if !ws.SkipBaseVerifications { + // Send invalid `ExecutionPayloadV1` by replacing withdrawals list + // with null, and client must respond with `InvalidParamsError`. + // Note that StateRoot is also incorrect but null withdrawals should + // be checked first instead of responding `INVALID` + nilWithdrawalsPayload, err := helper.CustomizePayload(&t.CLMock.LatestPayloadBuilt, &helper.CustomPayloadData{ + RemoveWithdrawals: true, + }) + if err != nil { + t.Fatalf("Unable to append withdrawals: %v", err) + } + r := t.TestEngine.TestEngineNewPayloadV2(nilWithdrawalsPayload) + r.ExpectationDescription = "Sent shanghai payload using ExecutionPayloadV1, error is expected" + r.ExpectErrorCode(InvalidParamsError) + + // Verify the list of withdrawals returned on the payload built + // completely matches the list provided in the + // engine_forkchoiceUpdatedV2 method call + if sentList, ok := ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber]; !ok { + panic("withdrawals sent list was not saved") + } else { + if len(sentList) != len(t.CLMock.LatestPayloadBuilt.Withdrawals) { + t.Fatalf("FAIL (%s): Incorrect list of withdrawals on built payload: want=%d, got=%d", t.TestName, len(sentList), len(t.CLMock.LatestPayloadBuilt.Withdrawals)) + } + for i := 0; i < len(sentList); i++ { + if err := test.CompareWithdrawal(sentList[i], t.CLMock.LatestPayloadBuilt.Withdrawals[i]); err != nil { + t.Fatalf("FAIL (%s): Incorrect withdrawal on index %d: %v", t.TestName, i, err) + } + } + + } + } }, OnNewPayloadBroadcast: func() { // Check withdrawal addresses and verify withdrawal balances @@ -1102,6 +1266,18 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { ) r.ExpectBalanceEqual(expectedAccountBalance) } + + if ws.TestCorrupedHashPayloads { + payload := t.CLMock.LatestExecutedPayload + + // Corrupt the hash + rand.Read(payload.BlockHash[:]) + + // On engine_newPayloadV2 `INVALID_BLOCK_HASH` is deprecated + // in favor of reusing `INVALID` + n := t.TestEngine.TestEngineNewPayloadV2(&payload) + n.ExpectStatus(test.Invalid) + } } }, OnForkchoiceBroadcast: func() { @@ -1664,12 +1840,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 { @@ -1677,8 +1853,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 @@ -1687,19 +1863,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, @@ -1715,13 +1893,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) @@ -1734,8 +1912,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) @@ -1750,7 +1928,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 @@ -1769,7 +1947,94 @@ 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) + } +} + +type BlockValueSpec struct { + *WithdrawalsBaseSpec +} + +func (s *BlockValueSpec) Execute(t *test.Env) { + s.WithdrawalsBaseSpec.SkipBaseVerifications = true + s.WithdrawalsBaseSpec.Execute(t) + + // Get the latest block and the transactions included + b := t.TestEngine.TestBlockByNumber(nil) + b.ExpectNoError() + + totalValue := new(big.Int) + txs := b.Block.Transactions() + if len(txs) == 0 { + t.Fatalf("FAIL (%s): No transactions included in latest block", t.TestName) + } + for _, tx := range txs { + r := t.TestEngine.TestTransactionReceipt(tx.Hash()) + r.ExpectNoError() + + receipt := r.Receipt + + gasUsed := new(big.Int).SetUint64(receipt.GasUsed) + txTip, _ := tx.EffectiveGasTip(b.Block.Header().BaseFee) + txTip.Mul(txTip, gasUsed) + totalValue.Add(totalValue, txTip) + } + + if totalValue.Cmp(t.CLMock.LatestBlockValue) != 0 { + t.Fatalf("FAIL (%s): Unexpected block value returned on GetPayloadV2: want=%d, got=%d", t.TestName, totalValue, t.CLMock.LatestBlockValue) } } diff --git a/simulators/ethereum/engine/test/expect.go b/simulators/ethereum/engine/test/expect.go index 6b51ab408c..13880f46c0 100644 --- a/simulators/ethereum/engine/test/expect.go +++ b/simulators/ethereum/engine/test/expect.go @@ -200,7 +200,9 @@ type NewPayloadResponseExpectObject struct { func (tec *TestEngineClient) TestEngineNewPayloadV1(payload *api.ExecutableData) *NewPayloadResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() - status, err := tec.Engine.NewPayloadV1(ctx, payload) + edv1 := &client_types.ExecutableDataV1{} + edv1.FromExecutableData(payload) + status, err := tec.Engine.NewPayloadV1(ctx, edv1) ret := &NewPayloadResponseExpectObject{ ExpectEnv: &ExpectEnv{Env: tec.Env}, Payload: payload, @@ -364,6 +366,84 @@ func (exp *GetPayloadResponseExpectObject) ExpectErrorCode(code int) { } } +func ComparePayloads(want *api.ExecutableData, got *api.ExecutableData) error { + if want == nil || got == nil { + if want == nil && got == nil { + return nil + } + return fmt.Errorf("want: %v, got: %v", want, got) + } + + if !bytes.Equal(want.ParentHash[:], got.ParentHash[:]) { + return fmt.Errorf("unexpected ParentHash: want=%v, got=%v", want.ParentHash, got.ParentHash) + } + + if !bytes.Equal(want.FeeRecipient[:], got.FeeRecipient[:]) { + return fmt.Errorf("unexpected FeeRecipient: want=%v, got=%v", want.FeeRecipient, got.FeeRecipient) + } + + if !bytes.Equal(want.StateRoot[:], got.StateRoot[:]) { + return fmt.Errorf("unexpected StateRoot: want=%v, got=%v", want.StateRoot, got.StateRoot) + } + + if !bytes.Equal(want.ReceiptsRoot[:], got.ReceiptsRoot[:]) { + return fmt.Errorf("unexpected ReceiptsRoot: want=%v, got=%v", want.ReceiptsRoot, got.ReceiptsRoot) + } + + if !bytes.Equal(want.Random[:], got.Random[:]) { + return fmt.Errorf("unexpected Random: want=%v, got=%v", want.Random, got.Random) + } + + if !bytes.Equal(want.BlockHash[:], got.BlockHash[:]) { + return fmt.Errorf("unexpected BlockHash: want=%v, got=%v", want.BlockHash, got.BlockHash) + } + + if !bytes.Equal(want.LogsBloom, got.LogsBloom) { + return fmt.Errorf("unexpected LogsBloom: want=%v, got=%v", want.LogsBloom, got.LogsBloom) + } + + if !bytes.Equal(want.ExtraData, got.ExtraData) { + return fmt.Errorf("unexpected ExtraData: want=%v, got=%v", want.ExtraData, got.ExtraData) + } + + if want.Number != got.Number { + return fmt.Errorf("unexpected Number: want=%d, got=%d", want.Number, got.Number) + } + + if want.GasLimit != got.GasLimit { + return fmt.Errorf("unexpected GasLimit: want=%d, got=%d", want.GasLimit, got.GasLimit) + } + + if want.GasUsed != got.GasUsed { + return fmt.Errorf("unexpected GasUsed: want=%d, got=%d", want.GasUsed, got.GasUsed) + } + + if want.Timestamp != got.Timestamp { + return fmt.Errorf("unexpected Timestamp: want=%d, got=%d", want.Timestamp, got.Timestamp) + } + + if want.BaseFeePerGas.Cmp(got.BaseFeePerGas) != 0 { + return fmt.Errorf("unexpected BaseFeePerGas: want=%d, got=%d", want.BaseFeePerGas, got.BaseFeePerGas) + } + + if err := CompareTransactions(want.Transactions, got.Transactions); err != nil { + return err + } + + if err := CompareWithdrawals(want.Withdrawals, got.Withdrawals); err != nil { + return err + } + + return nil +} + +func (exp *GetPayloadResponseExpectObject) ExpectPayload(expectedPayload *api.ExecutableData) { + exp.ExpectNoError() + if err := ComparePayloads(expectedPayload, &exp.Payload); err != nil { + exp.Fatalf("FAIL (%s): Unexpected payload returned on EngineGetPayloadV%d: %v", exp.TestName, exp.Version, err) + } +} + func (exp *GetPayloadResponseExpectObject) ExpectPayloadParentHash(expectedParentHash common.Hash) { exp.ExpectNoError() if exp.Payload.ParentHash != expectedParentHash { @@ -465,61 +545,86 @@ func (exp *GetPayloadBodiesResponseExpectObject) ExpectPayloadBodiesCount(count } } -func CompareWithdrawals(want *types.Withdrawal, got *types.Withdrawal) error { - if want == nil && got != nil || want != nil && got == nil { - return fmt.Errorf("want=%v, got=%v", want, got) +func CompareTransactions(want [][]byte, got [][]byte) error { + if len(want) != len(got) { + return fmt.Errorf("incorrect tx length: want=%d, got=%d", len(want), len(got)) } - if want != nil { - if want.Amount != got.Amount || - !bytes.Equal(want.Address[:], got.Address[:]) || - want.Index != got.Index || - want.Validator != got.Validator { - wantStr, _ := json.MarshalIndent(want, "", " ") - gotStr, _ := json.MarshalIndent(got, "", " ") - return fmt.Errorf("want=%v, got=%v", wantStr, gotStr) + + for i, a_tx := range want { + b_tx := got[i] + if !bytes.Equal(a_tx, b_tx) { + return fmt.Errorf("tx %d not equal: want=%x, got=%x", i, a_tx, b_tx) } } + return nil } -func ComparePayloadBodies(want *client_types.ExecutionPayloadBodyV1, got *client_types.ExecutionPayloadBodyV1) error { - if (want == nil || got == nil) && want != got { - if want == nil { - return fmt.Errorf("wanted null, got object") +func CompareWithdrawal(want *types.Withdrawal, got *types.Withdrawal) error { + if want == nil || got == nil { + if want == nil && got != nil { + got, _ := json.MarshalIndent(got, "", " ") + return fmt.Errorf("want=null, got=%s", got) + } else if want != nil && got == nil { + want, _ := json.MarshalIndent(want, "", " ") + return fmt.Errorf("want=%s, got=null", want) } - return fmt.Errorf("wanted object, got null") + return nil + } + if want.Amount != got.Amount || + !bytes.Equal(want.Address[:], got.Address[:]) || + want.Index != got.Index || + want.Validator != got.Validator { + want, _ := json.MarshalIndent(want, "", " ") + got, _ := json.MarshalIndent(got, "", " ") + return fmt.Errorf("want=%s, got=%s", want, got) } + return nil +} - if want != nil { - if len(want.Transactions) != len(got.Transactions) { - return fmt.Errorf("incorrect tx length: want=%d, got=%d", len(want.Transactions), len(got.Transactions)) +func CompareWithdrawals(want []*types.Withdrawal, got []*types.Withdrawal) error { + if want == nil || got == nil { + if want == nil && got == nil { + return nil } - - if want.Withdrawals == nil && got.Withdrawals != nil { - return fmt.Errorf("wanted null withdrawals, got object") - } else if want.Withdrawals != nil && got.Withdrawals == nil { - return fmt.Errorf("wanted object, got null withdrawals") + if want == nil && got != nil { + got, _ := json.MarshalIndent(got, "", " ") + return fmt.Errorf("incorrect withdrawals: want: null, got: %s", got) + } else { + want, _ := json.MarshalIndent(want, "", " ") + return fmt.Errorf("incorrect withdrawals: want: %s, got: null", want) } - if len(want.Withdrawals) != len(got.Withdrawals) { - return fmt.Errorf("incorrect withdrawals length: want=%d, got=%d", len(want.Withdrawals), len(got.Withdrawals)) - } + } - for i, a_tx := range want.Transactions { - b_tx := got.Transactions[i] - if !bytes.Equal(a_tx, b_tx) { - return fmt.Errorf("tx %d not equal: want=%x, got=%x", i, a_tx, b_tx) - } + if len(want) != len(got) { + return fmt.Errorf("incorrect withdrawals length: want=%d, got=%d", len(want), len(got)) + } + + for i, a_w := range want { + b_w := got[i] + if err := CompareWithdrawal(a_w, b_w); err != nil { + return fmt.Errorf("withdrawal %d not equal: %v", i, err) } + } - if want.Withdrawals != nil { - for i, a_w := range want.Withdrawals { - b_w := got.Withdrawals[i] - if err := CompareWithdrawals(a_w, b_w); err != nil { - return fmt.Errorf("withdrawal %d not equal: %v", i, err) - } - } + return nil +} + +func ComparePayloadBodies(want *client_types.ExecutionPayloadBodyV1, got *client_types.ExecutionPayloadBodyV1) error { + if want == nil || got == nil { + if want == nil && got == nil { + return nil } + return fmt.Errorf("want: %v, got: %v", want, got) + } + + if err := CompareTransactions(want.Transactions, got.Transactions); err != nil { + return err + } + + if err := CompareWithdrawals(want.Withdrawals, got.Withdrawals); err != nil { + return err } return nil