diff --git a/simulators/ethereum/engine/client/engine.go b/simulators/ethereum/engine/client/engine.go index ba3aa9cdcb..c8b353babb 100644 --- a/simulators/ethereum/engine/client/engine.go +++ b/simulators/ethereum/engine/client/engine.go @@ -10,6 +10,8 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + + client_types "github.com/ethereum/hive/simulators/ethereum/engine/client/types" ) type Eth interface { @@ -31,9 +33,13 @@ 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) NewPayloadV2(ctx context.Context, payload *api.ExecutableData) (api.PayloadStatusV1, error) + GetPayloadBodiesByRangeV1(ctx context.Context, start uint64, count uint64) ([]*client_types.ExecutionPayloadBodyV1, error) + GetPayloadBodiesByHashV1(ctx context.Context, hashes []common.Hash) ([]*client_types.ExecutionPayloadBodyV1, error) + LatestForkchoiceSent() (fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) LatestNewPayloadSent() (payload *api.ExecutableData) diff --git a/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go b/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go index 25312ee993..cfbb1f5df0 100644 --- a/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go +++ b/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go @@ -20,6 +20,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/hive/hivesim" "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" "github.com/golang-jwt/jwt/v4" @@ -334,7 +335,7 @@ func (ec *HiveRPCEngineClient) GetPayload(ctx context.Context, version int, payl } if version == 2 { - var response api.ExecutableDataV2 + var response api.ExecutionPayloadEnvelope err = ec.c.CallContext(ctx, &response, rpcString, payloadId) if response.ExecutionPayload != nil { executableData = *response.ExecutionPayload @@ -356,6 +357,32 @@ func (ec *HiveRPCEngineClient) GetPayloadV2(ctx context.Context, payloadId *api. return ec.GetPayload(ctx, 2, payloadId) } +func (ec *HiveRPCEngineClient) GetPayloadBodiesByRangeV1(ctx context.Context, start uint64, count uint64) ([]*client_types.ExecutionPayloadBodyV1, error) { + var ( + result []*client_types.ExecutionPayloadBodyV1 + err error + ) + if err = ec.PrepareDefaultAuthCallToken(); err != nil { + return nil, err + } + + err = ec.c.CallContext(ctx, &result, "engine_getPayloadBodiesByRangeV1", start, count) + return result, err +} + +func (ec *HiveRPCEngineClient) GetPayloadBodiesByHashV1(ctx context.Context, hashes []common.Hash) ([]*client_types.ExecutionPayloadBodyV1, error) { + var ( + result []*client_types.ExecutionPayloadBodyV1 + err error + ) + if err = ec.PrepareDefaultAuthCallToken(); err != nil { + return nil, err + } + + err = ec.c.CallContext(ctx, &result, "engine_getPayloadBodiesByHashV1", hashes) + return result, err +} + func (ec *HiveRPCEngineClient) NewPayload(ctx context.Context, version int, payload *api.ExecutableData) (api.PayloadStatusV1, error) { var result api.PayloadStatusV1 if err := ec.PrepareDefaultAuthCallToken(); err != nil { @@ -381,6 +408,15 @@ func (ec *HiveRPCEngineClient) ExchangeTransitionConfigurationV1(ctx context.Con return result, err } +func (ec *HiveRPCEngineClient) ExchangeCapabilities(ctx context.Context, clCapabilities []string) ([]string, error) { + var result []string + if err := ec.PrepareDefaultAuthCallToken(); err != nil { + return result, err + } + err := ec.c.CallContext(ctx, &result, "engine_exchangeCapabilities", clCapabilities) + return result, err +} + func (ec *HiveRPCEngineClient) GetNextAccountNonce(testCtx context.Context, account common.Address) (uint64, error) { // First get the current head of the client where we will send the tx ctx, cancel := context.WithTimeout(testCtx, globals.RPCTimeout) diff --git a/simulators/ethereum/engine/client/node/node.go b/simulators/ethereum/engine/client/node/node.go index edf1a519f1..f08cf623c7 100644 --- a/simulators/ethereum/engine/client/node/node.go +++ b/simulators/ethereum/engine/client/node/node.go @@ -36,6 +36,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/hive/hivesim" "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" ) @@ -734,6 +735,14 @@ func (n *GethNode) GetPayloadV2(ctx context.Context, payloadId *beacon.PayloadID return *p.ExecutionPayload, p.BlockValue, err } +func (n *GethNode) GetPayloadBodiesByRangeV1(ctx context.Context, start uint64, count uint64) ([]*client_types.ExecutionPayloadBodyV1, error) { + return nil, fmt.Errorf("not implemented") +} + +func (n *GethNode) GetPayloadBodiesByHashV1(ctx context.Context, hashes []common.Hash) ([]*client_types.ExecutionPayloadBodyV1, error) { + return nil, fmt.Errorf("not implemented") +} + // Eth JSON RPC const ( SafeBlockNumber = rpc.BlockNumber(-4) // This is not yet true diff --git a/simulators/ethereum/engine/client/types/gen_epbv1.go b/simulators/ethereum/engine/client/types/gen_epbv1.go new file mode 100644 index 0000000000..aa7ab75e2e --- /dev/null +++ b/simulators/ethereum/engine/client/types/gen_epbv1.go @@ -0,0 +1,53 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package types + +import ( + "encoding/json" + "errors" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +var _ = (*executionPayloadBodyV1Marshaling)(nil) + +// MarshalJSON marshals as JSON. +func (e ExecutionPayloadBodyV1) MarshalJSON() ([]byte, error) { + type ExecutionPayloadBodyV1 struct { + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + } + var enc ExecutionPayloadBodyV1 + if e.Transactions != nil { + enc.Transactions = make([]hexutil.Bytes, len(e.Transactions)) + for k, v := range e.Transactions { + enc.Transactions[k] = v + } + } + enc.Withdrawals = e.Withdrawals + return json.Marshal(&enc) +} + +// UnmarshalJSON unmarshals from JSON. +func (e *ExecutionPayloadBodyV1) UnmarshalJSON(input []byte) error { + type ExecutionPayloadBodyV1 struct { + Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + } + var dec ExecutionPayloadBodyV1 + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.Transactions == nil { + return errors.New("missing required field 'transactions' for ExecutionPayloadBodyV1") + } + e.Transactions = make([][]byte, len(dec.Transactions)) + for k, v := range dec.Transactions { + e.Transactions[k] = v + } + if dec.Withdrawals != nil { + e.Withdrawals = dec.Withdrawals + } + return nil +} diff --git a/simulators/ethereum/engine/client/types/types.go b/simulators/ethereum/engine/client/types/types.go new file mode 100644 index 0000000000..7346f65dbd --- /dev/null +++ b/simulators/ethereum/engine/client/types/types.go @@ -0,0 +1,17 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +//go:generate go run github.com/fjl/gencodec -type ExecutionPayloadBodyV1 -field-override executionPayloadBodyV1Marshaling -out gen_epbv1.go +type ExecutionPayloadBodyV1 struct { + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` +} + +// JSON type overrides for executableData. +type executionPayloadBodyV1Marshaling struct { + Transactions []hexutil.Bytes +} diff --git a/simulators/ethereum/engine/go.mod b/simulators/ethereum/engine/go.mod index bf3927f6f9..7b34fbe775 100644 --- a/simulators/ethereum/engine/go.mod +++ b/simulators/ethereum/engine/go.mod @@ -6,6 +6,7 @@ require ( github.com/ethereum/go-ethereum v1.10.26 github.com/ethereum/hive v0.0.0-20230124094404-5b561920e19b github.com/golang-jwt/jwt/v4 v4.4.3 + golang.org/x/exp v0.0.0-20230126173853-a67bb567ff2e ) require ( @@ -69,4 +70,4 @@ require ( gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect ) -replace github.com/ethereum/go-ethereum v1.10.26 => github.com/lightclient/go-ethereum v1.10.10-0.20230116085521-6ab6d738866f +replace github.com/ethereum/go-ethereum v1.10.26 => github.com/ethereum/go-ethereum v1.10.24-0.20230127123926-a63875bf4d7f diff --git a/simulators/ethereum/engine/go.sum b/simulators/ethereum/engine/go.sum index f8ce52af39..c9caa6ac28 100644 --- a/simulators/ethereum/engine/go.sum +++ b/simulators/ethereum/engine/go.sum @@ -76,6 +76,8 @@ github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/ethereum/go-ethereum v1.10.24-0.20230127123926-a63875bf4d7f h1:4zLYUPuCDwBfQSCY1QWJfVn9fmOFmX1D4GCnMC8UlU8= +github.com/ethereum/go-ethereum v1.10.24-0.20230127123926-a63875bf4d7f/go.mod h1:C5Xij/C5W1rXkm7pCvr7p2HWUiVk6RmTF3EUvp8LXMY= github.com/ethereum/hive v0.0.0-20230124094404-5b561920e19b h1:TW8iKjNf7tZZkleHIGNG2wkawQC8BzGce++cxRw10lU= github.com/ethereum/hive v0.0.0-20230124094404-5b561920e19b/go.mod h1:pqToex2uV+Tg7bhBla/uvB3APKZ6FogJMMpow33nUIA= github.com/fjl/memsize v0.0.1 h1:+zhkb+dhUgx0/e+M8sF0QqiouvMQUiKR+QYvdxIOKcQ= @@ -216,8 +218,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lightclient/go-ethereum v1.10.10-0.20230116085521-6ab6d738866f h1:Pox2oxBYKWVEw2JUCyiEybqEIurSkq8VHjRp6s9jdf8= -github.com/lightclient/go-ethereum v1.10.10-0.20230116085521-6ab6d738866f/go.mod h1:n7VlOgCwYheLB/mi+V8ni2yf8K2qM3N9WAmalxkhk+c= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -363,6 +363,8 @@ golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm0 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20230126173853-a67bb567ff2e h1:nEzRHNOazEST44vMvEwxGxnYGrzXEmxJmnti5mKSWTk= +golang.org/x/exp v0.0.0-20230126173853-a67bb567ff2e/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/simulators/ethereum/engine/main.go b/simulators/ethereum/engine/main.go index 3d2e7bb5d4..bca92e73cc 100644 --- a/simulators/ethereum/engine/main.go +++ b/simulators/ethereum/engine/main.go @@ -12,6 +12,7 @@ import ( suite_auth "github.com/ethereum/hive/simulators/ethereum/engine/suites/auth" suite_engine "github.com/ethereum/hive/simulators/ethereum/engine/suites/engine" + suite_ex_cap "github.com/ethereum/hive/simulators/ethereum/engine/suites/exchange_capabilities" suite_transition "github.com/ethereum/hive/simulators/ethereum/engine/suites/transition" suite_withdrawals "github.com/ethereum/hive/simulators/ethereum/engine/suites/withdrawals" ) @@ -34,6 +35,11 @@ func main() { Name: "engine-auth", Description: ` Test Engine API authentication features.`[1:], + } + excap = hivesim.Suite{ + Name: "engine-exchange-capabilities", + Description: ` + Test Engine API exchange capabilities.`[1:], } sync = hivesim.Suite{ Name: "engine-sync", @@ -52,6 +58,7 @@ func main() { addTestsToSuite(simulator, &engine, specToInterface(suite_engine.Tests), "full") addTestsToSuite(simulator, &transition, specToInterface(suite_transition.Tests), "full") addTestsToSuite(simulator, &auth, specToInterface(suite_auth.Tests), "full") + addTestsToSuite(simulator, &excap, specToInterface(suite_ex_cap.Tests), "full") //suite_sync.AddSyncTestsToSuite(simulator, &sync, suite_sync.Tests) addTestsToSuite(simulator, &withdrawals, suite_withdrawals.Tests, "full") @@ -59,6 +66,7 @@ func main() { hivesim.MustRunSuite(simulator, engine) hivesim.MustRunSuite(simulator, transition) hivesim.MustRunSuite(simulator, auth) + hivesim.MustRunSuite(simulator, excap) hivesim.MustRunSuite(simulator, sync) hivesim.MustRunSuite(simulator, withdrawals) } @@ -118,6 +126,7 @@ func addTestsToSuite(sim *hivesim.Simulation, suite *hivesim.Suite, tests []test delete(newParams, "HIVE_CLIQUE_PRIVATEKEY") delete(newParams, "HIVE_CLIQUE_PERIOD") delete(newParams, "HIVE_MINER") + newParams = newParams.Set("HIVE_POST_MERGE_GENESIS", "true") } if clientTypes, err := sim.ClientTypes(); err == nil { diff --git a/simulators/ethereum/engine/suites/exchange_capabilities/tests.go b/simulators/ethereum/engine/suites/exchange_capabilities/tests.go new file mode 100644 index 0000000000..f0e2612ce8 --- /dev/null +++ b/simulators/ethereum/engine/suites/exchange_capabilities/tests.go @@ -0,0 +1,35 @@ +package suite_exchange_capabilities + +import ( + "github.com/ethereum/hive/simulators/ethereum/engine/test" + "golang.org/x/exp/slices" +) + +var Tests = []test.Spec{ + { + Name: "Exchange Capabilities", + Run: exCapTests, + }, +} + +var minimalSetExpectedSupportedELCapabilities = []string{ + "engine_newPayloadV1", + "engine_newPayloadV2", + "engine_forkchoiceUpdatedV1", + "engine_forkchoiceUpdatedV2", + "engine_getPayloadV1", + "engine_getPayloadV2", + // "engine_getPayloadBodiesByRangeV1", +} + +func exCapTests(t *test.Env) { + if returnedCapabilities, err := t.HiveEngine.ExchangeCapabilities(t.TestContext, minimalSetExpectedSupportedELCapabilities); err != nil { + t.Fatalf("FAIL (%s): Unable request capabilities: %v", t.TestName, err) + } else { + for _, cap := range minimalSetExpectedSupportedELCapabilities { + if !slices.Contains(returnedCapabilities, cap) { + t.Fatalf("FAIL (%s): Expected capability (%s) not found", t.TestName, cap) + } + } + } +} diff --git a/simulators/ethereum/engine/suites/withdrawals/README.md b/simulators/ethereum/engine/suites/withdrawals/README.md index cd25ceca14..8b3ee53d7d 100644 --- a/simulators/ethereum/engine/suites/withdrawals/README.md +++ b/simulators/ethereum/engine/suites/withdrawals/README.md @@ -151,4 +151,28 @@ This test suite verifies behavior of the Engine API on each client after the Sha - Verify that `TxB` returns error on `eth_sendRawTransaction` and also should be absent from the transaction pool of the client - Request a new payload from the client and verify that the payload built only includes `TxA`, and `TxB` is not included, nor the contract it could create is present in the `stateRoot`. - Create a modified payload where `TxA` is replaced by `TxB` and send using `engine_newPayloadV2` - - Verify that `engine_newPayloadV2` returns `INVALID` nad `latestValidHash` points to the latest valid payload in the canonical chain. \ No newline at end of file + - Verify that `engine_newPayloadV2` returns `INVALID` nad `latestValidHash` points to the latest valid payload in the canonical chain. + +## GetPayloadBodies Tests + +- Payload Bodies By Range - 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 + - Payloads produced of the following characteristics + - [x] 16 Transactions, 16 Withdrawals + - [x] 0 Transactions, 0 Withdrawals + - 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 + +- 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 + - Payloads produced of the following characteristics + - [x] 16 Transactions, 16 Withdrawals + - [x] 0 Transactions, 0 Withdrawals + - 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 of unknown hashes are returned as null in the returned list diff --git a/simulators/ethereum/engine/suites/withdrawals/tests.go b/simulators/ethereum/engine/suites/withdrawals/tests.go index cbdb742213..4285e8a80c 100644 --- a/simulators/ethereum/engine/suites/withdrawals/tests.go +++ b/simulators/ethereum/engine/suites/withdrawals/tests.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "math/big" + "math/rand" "time" "github.com/ethereum/go-ethereum/common" @@ -13,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/hive/simulators/ethereum/engine/client/hive_rpc" + 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" @@ -26,6 +28,14 @@ var ( Safe = big.NewInt(-4) InvalidParamsError = -32602 MAX_INITCODE_SIZE = 49152 + + WARM_COINBASE_ADDRESS = common.HexToAddress("0x0101010101010101010101010101010101010101") + PUSH0_ADDRESS = common.HexToAddress("0x0202020202020202020202020202020202020202") + + TX_CONTRACT_ADDRESSES = []common.Address{ + WARM_COINBASE_ADDRESS, + PUSH0_ADDRESS, + } ) // Execution specification reference: @@ -469,6 +479,164 @@ var Tests = []test.SpecInterface{ OverflowMaxInitcodeTxCountBeforeFork: 0, OverflowMaxInitcodeTxCountAfterFork: 1, }, + + // Get Payload Bodies Requests + &GetPayloadBodiesSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "GetPayloadBodiesByRange", + About: ` + Make multiple withdrawals to 16 accounts each payload. + Retrieve many of the payloads' bodies by number range. + `, + TimeoutSeconds: 240, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 17, + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 1024, + }, + GetPayloadBodiesRequests: []GetPayloadBodyRequest{ + GetPayloadBodyRequestByRange{ + Start: 1, + Count: 4, + }, + GetPayloadBodyRequestByRange{ + Start: 1, + Count: 8, + }, + GetPayloadBodyRequestByRange{ + Start: 1, + Count: 1, + }, + GetPayloadBodyRequestByRange{ + Start: 4, + Count: 1, + }, + GetPayloadBodyRequestByRange{ + Start: 15, + Count: 2, + }, + GetPayloadBodyRequestByRange{ + Start: 16, + Count: 16, + }, + GetPayloadBodyRequestByRange{ + Start: 1, + Count: 32, + }, + GetPayloadBodyRequestByRange{ + Start: 33, + Count: 1, + }, + GetPayloadBodyRequestByRange{ + Start: 33, + Count: 32, + }, + GetPayloadBodyRequestByRange{ + Start: 32, + Count: 0, + }, + GetPayloadBodyRequestByRange{ + Start: 0, + Count: 1, + }, + }, + }, + + &GetPayloadBodiesSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "GetPayloadBodiesByRange (Empty Transactions/Withdrawals)", + About: ` + Make no withdrawals and no transactions in many payloads. + Retrieve many of the payloads' bodies by number range. + `, + TimeoutSeconds: 240, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 17, + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 0, + TransactionsPerBlock: common.Big0, + }, + GetPayloadBodiesRequests: []GetPayloadBodyRequest{ + GetPayloadBodyRequestByRange{ + Start: 16, + Count: 2, + }, + }, + }, + &GetPayloadBodiesSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "GetPayloadBodiesByHash", + About: ` + Make multiple withdrawals to 16 accounts each payload. + Retrieve many of the payloads' bodies by hash. + `, + TimeoutSeconds: 240, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 17, + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 1024, + }, + GetPayloadBodiesRequests: []GetPayloadBodyRequest{ + GetPayloadBodyRequestByHashIndex{ + BlockNumbers: []uint64{ + 1, + 16, + 2, + 17, + }, + }, + GetPayloadBodyRequestByHashIndex{ + Start: 1, + End: 32, + }, + GetPayloadBodyRequestByHashIndex{ + BlockNumbers: []uint64{ + 32, + 1000, + 31, + 1000, + 30, + 1000, + }, + }, + }, + }, + + &GetPayloadBodiesSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "GetPayloadBodiesByHash (Empty Transactions/Withdrawals)", + About: ` + Make no withdrawals and no transactions in many payloads. + Retrieve many of the payloads' bodies by hash. + `, + TimeoutSeconds: 240, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 17, + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 0, + TransactionsPerBlock: common.Big0, + }, + GetPayloadBodiesRequests: []GetPayloadBodyRequest{ + GetPayloadBodyRequestByHashIndex{ + Start: 16, + End: 17, + }, + }, + }, } // Helper types to convert gwei into wei more easily @@ -631,12 +799,79 @@ func (ws *WithdrawalsBaseSpec) GetGenesis() *core.Genesis { genesis.Config.Clique = nil genesis.ExtraData = []byte{} + // Add some accounts to withdraw to with unconditional SSTOREs startAccount := big.NewInt(0x1000) endAccount := big.NewInt(0x1000 + int64(ws.GetWithdrawableAccountCount()) - 1) AddUnconditionalBytecode(genesis, startAccount, endAccount) + + // Add accounts that use the coinbase (EIP-3651) + warmCoinbaseCode := []byte{ + 0x5A, // GAS + 0x60, // PUSH1(0x00) + 0x00, + 0x60, // PUSH1(0x00) + 0x00, + 0x60, // PUSH1(0x00) + 0x00, + 0x60, // PUSH1(0x00) + 0x00, + 0x60, // PUSH1(0x00) + 0x00, + 0x41, // COINBASE + 0x60, // PUSH1(0xFF) + 0xFF, + 0xF1, // CALL + 0x5A, // GAS + 0x90, // SWAP1 + 0x50, // POP - Call result + 0x90, // SWAP1 + 0x03, // SUB + 0x60, // PUSH1(0x16) - GAS + PUSH * 6 + COINBASE + 0x16, + 0x90, // SWAP1 + 0x03, // SUB + 0x43, // NUMBER + 0x55, // SSTORE + } + genesis.Alloc[WARM_COINBASE_ADDRESS] = core.GenesisAccount{ + Code: warmCoinbaseCode, + Balance: common.Big0, + } + + // Add accounts that use the PUSH0 (EIP-3855) + push0Code := []byte{ + 0x43, // NUMBER + 0x5F, // PUSH0 + 0x55, // SSTORE + } + genesis.Alloc[PUSH0_ADDRESS] = core.GenesisAccount{ + Code: push0Code, + Balance: common.Big0, + } return genesis } +func (ws *WithdrawalsBaseSpec) VerifyContractsStorage(t *test.Env) { + if ws.GetTransactionCountPerPayload() < uint64(len(TX_CONTRACT_ADDRESSES)) { + return + } + // Assume that forkchoice updated has been already sent + latestPayloadNumber := t.CLMock.LatestExecutedPayload.Number + latestPayloadNumberBig := big.NewInt(int64(latestPayloadNumber)) + + r := t.TestEngine.TestStorageAt(WARM_COINBASE_ADDRESS, common.BigToHash(latestPayloadNumberBig), latestPayloadNumberBig) + p := t.TestEngine.TestStorageAt(PUSH0_ADDRESS, common.Hash{}, latestPayloadNumberBig) + if latestPayloadNumber >= ws.WithdrawalsForkHeight { + // Shanghai + r.ExpectBigIntStorageEqual(big.NewInt(100)) // WARM_STORAGE_READ_COST + p.ExpectBigIntStorageEqual(latestPayloadNumberBig) // tx succeeded + } else { + // Pre-Shanghai + r.ExpectBigIntStorageEqual(big.NewInt(2600)) // COLD_ACCOUNT_ACCESS_COST + p.ExpectBigIntStorageEqual(big.NewInt(0)) // tx must've failed + } +} + // Changes the CL Mocker default time increments of 1 to the value specified // in the test spec. func (ws *WithdrawalsBaseSpec) ConfigureCLMock(cl *clmock.CLMocker) { @@ -731,6 +966,29 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { // Produce any blocks necessary to reach withdrawals fork t.CLMock.ProduceBlocks(int(ws.GetPreWithdrawalsBlockCount()), clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { + + // Send some transactions + for i := uint64(0); i < ws.GetTransactionCountPerPayload(); i++ { + + var destAddr = TX_CONTRACT_ADDRESSES[int(i)%len(TX_CONTRACT_ADDRESSES)] + + _, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &destAddr, + Amount: common.Big1, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) + + if err != nil { + t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) + } + } + if !ws.SkipBaseVerifications { // Try to send a ForkchoiceUpdatedV2 with non-null // withdrawals before Shanghai @@ -779,6 +1037,11 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { r.ExpectWithdrawalsRoot(nil) } }, + OnForkchoiceBroadcast: func() { + if !ws.SkipBaseVerifications { + ws.VerifyContractsStorage(t) + } + }, }) // Produce requested post-shanghai blocks @@ -795,17 +1058,20 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber] = t.CLMock.NextWithdrawals // Send some transactions for i := uint64(0); i < ws.GetTransactionCountPerPayload(); i++ { + var destAddr = TX_CONTRACT_ADDRESSES[int(i)%len(TX_CONTRACT_ADDRESSES)] + _, err := helper.SendNextTransaction( t.TestContext, t.CLMock.NextBlockProducer, &helper.BaseTransactionCreator{ - Recipient: &globals.PrevRandaoContractAddr, + Recipient: &destAddr, Amount: common.Big1, Payload: nil, TxType: t.TestTransactionType, GasLimit: 75000, }, ) + if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -874,6 +1140,8 @@ func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { to verify withdrawalsRoot with the following withdrawals: %s`, jsWithdrawals) r.ExpectWithdrawalsRoot(&expectedWithdrawalsRoot) + + ws.VerifyContractsStorage(t) } }, }) @@ -1390,3 +1658,118 @@ func (s *MaxInitcodeSizeSpec) Execute(t *test.Env) { }, }) } + +// Withdrawals sync spec: +// Specifies a withdrawals test where the withdrawals happen and then a +// client needs to sync and apply the withdrawals. +type GetPayloadBodiesSpec struct { + *WithdrawalsBaseSpec + + GetPayloadBodiesRequests []GetPayloadBodyRequest +} + +type GetPayloadBodyRequest interface { + Verify(*test.Env) +} + +type GetPayloadBodyRequestByRange struct { + Start uint64 + Count uint64 +} + +func (req GetPayloadBodyRequestByRange) Verify(t *test.Env) { + r := t.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 + value less than 1, therefore error is expected. + `, req.Start, req.Count) + r.ExpectErrorCode(InvalidParamsError) + return + } + if req.Start > t.CLMock.CurrentPayloadNumber { + 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) + r.ExpectPayloadBodiesCount(0) + } else { + var count = req.Count + if req.Start+req.Count-1 > t.CLMock.CurrentPayloadNumber { + count = t.CLMock.CurrentPayloadNumber - req.Start + 1 + } + r.ExpectPayloadBodiesCount(count) + for i := req.Start; i < req.Start+count; i++ { + p := t.CLMock.ExecutedPayloadHistory[i] + + r.ExpectPayloadBody(i-req.Start, &client_types.ExecutionPayloadBodyV1{ + Transactions: p.Transactions, + Withdrawals: p.Withdrawals, + }) + } + } +} + +type GetPayloadBodyRequestByHashIndex struct { + BlockNumbers []uint64 + Start uint64 + End uint64 +} + +func (req GetPayloadBodyRequestByHashIndex) Verify(t *test.Env) { + 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) + hashes = append(hashes, p.BlockHash) + } else { + // signal to request an unknown hash (random) + randHash := common.Hash{} + rand.Read(randHash[:]) + payloads = append(payloads, nil) + hashes = append(hashes, randHash) + } + } + } + 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) + hashes = append(hashes, p.BlockHash) + } else { + // signal to request an unknown hash (random) + randHash := common.Hash{} + rand.Read(randHash[:]) + payloads = append(payloads, nil) + hashes = append(hashes, randHash) + } + } + } + if len(payloads) == 0 { + panic("invalid test") + } + + r := t.TestEngine.TestEngineGetPayloadBodiesByHashV1(hashes) + r.ExpectPayloadBodiesCount(uint64(len(payloads))) + for i, p := range payloads { + var expectedPayloadBody *client_types.ExecutionPayloadBodyV1 + if p != nil { + expectedPayloadBody = &client_types.ExecutionPayloadBodyV1{ + Transactions: p.Transactions, + Withdrawals: p.Withdrawals, + } + } + r.ExpectPayloadBody(uint64(i), expectedPayloadBody) + } + +} + +func (ws *GetPayloadBodiesSpec) Execute(t *test.Env) { + // Do the base withdrawal test first, skipping base verifications + ws.WithdrawalsBaseSpec.SkipBaseVerifications = true + ws.WithdrawalsBaseSpec.Execute(t) + for _, req := range ws.GetPayloadBodiesRequests { + req.Verify(t) + } +} diff --git a/simulators/ethereum/engine/test/expect.go b/simulators/ethereum/engine/test/expect.go index b063db4c40..6b51ab408c 100644 --- a/simulators/ethereum/engine/test/expect.go +++ b/simulators/ethereum/engine/test/expect.go @@ -1,6 +1,7 @@ package test import ( + "bytes" "context" "encoding/json" "fmt" @@ -13,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" "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" ) @@ -292,7 +294,7 @@ func (exp *NewPayloadResponseExpectObject) ExpectLatestValidHash(lvh *common.Has } } -// GetPayloadV1 +// GetPayload type GetPayloadResponseExpectObject struct { *ExpectEnv Payload api.ExecutableData @@ -390,6 +392,156 @@ func (exp *GetPayloadResponseExpectObject) ExpectTimestamp(expectedTimestamp uin } } +// GetPayloadBodies +type GetPayloadBodiesResponseExpectObject struct { + *ExpectEnv + PayloadBodies []*client_types.ExecutionPayloadBodyV1 + BlockValue *big.Int + Version int + Error error + ErrorCode int +} + +func (tec *TestEngineClient) TestEngineGetPayloadBodiesByRangeV1(start uint64, count uint64) *GetPayloadBodiesResponseExpectObject { + ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) + defer cancel() + payloadBodies, err := tec.Engine.GetPayloadBodiesByRangeV1(ctx, start, count) + ret := &GetPayloadBodiesResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, + PayloadBodies: payloadBodies, + Version: 1, + BlockValue: nil, + Error: err, + } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (tec *TestEngineClient) TestEngineGetPayloadBodiesByHashV1(hashes []common.Hash) *GetPayloadBodiesResponseExpectObject { + ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) + defer cancel() + payloadBodies, err := tec.Engine.GetPayloadBodiesByHashV1(ctx, hashes) + ret := &GetPayloadBodiesResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, + PayloadBodies: payloadBodies, + Version: 1, + BlockValue: nil, + Error: err, + } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (exp *GetPayloadBodiesResponseExpectObject) ExpectNoError() { + if exp.Error != nil { + exp.Fatalf("FAIL (%s): Expected no error on EngineGetPayloadBodiesV%d: error=%v", exp.TestName, exp.Version, exp.Error) + } +} + +func (exp *GetPayloadBodiesResponseExpectObject) ExpectError() { + if exp.Error == nil { + exp.Fatalf("FAIL (%s): Expected error on EngineGetPayloadBodiesV%d: payload=%v", exp.TestName, exp.Version, exp.PayloadBodies) + } +} + +func (exp *GetPayloadBodiesResponseExpectObject) ExpectErrorCode(code int) { + exp.ExpectError() + if exp.ErrorCode != code { + exp.Fatalf("FAIL (%s): Expected error code on EngineGetPayloadBodiesV%d: want=%d, got=%d", exp.TestName, exp.Version, code, exp.ErrorCode) + } +} + +func (exp *GetPayloadBodiesResponseExpectObject) ExpectPayloadBodiesCount(count uint64) { + exp.ExpectNoError() + if exp.PayloadBodies == nil { + exp.Fatalf("FAIL (%s): Expected payload bodies list count on EngineGetPayloadBodiesV%d: want=%d, got=nil", exp.TestName, exp.Version, count) + } + if uint64(len(exp.PayloadBodies)) != count { + exp.Fatalf("FAIL (%s): Expected payload bodies list count on EngineGetPayloadBodiesV%d: want=%d, got=%d", exp.TestName, exp.Version, count, len(exp.PayloadBodies)) + } +} + +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) + } + 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) + } + } + 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") + } + return fmt.Errorf("wanted object, got null") + } + + 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)) + } + + 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 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 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 (exp *GetPayloadBodiesResponseExpectObject) ExpectPayloadBody(index uint64, payloadBody *client_types.ExecutionPayloadBodyV1) { + exp.ExpectNoError() + if exp.PayloadBodies == nil { + exp.Fatalf("FAIL (%s): Expected payload body in list on EngineGetPayloadBodiesV%d, but list is nil", exp.TestName, exp.Version) + } + if index >= uint64(len(exp.PayloadBodies)) { + exp.Fatalf("FAIL (%s): Expected payload body in list on EngineGetPayloadBodiesV%d, list is smaller than index: want=%s", exp.TestName, exp.Version, index) + } + + checkItem := exp.PayloadBodies[index] + + if err := ComparePayloadBodies(payloadBody, checkItem); err != nil { + exp.Fatalf("FAIL (%s): Unexpected payload body on EngineGetPayloadBodiesV%d at index %d: %v", exp.TestName, exp.Version, index, err) + + } +} + // BlockNumber type BlockNumberResponseExpectObject struct { *ExpectEnv @@ -634,6 +786,9 @@ func (exp *BalanceResponseExpectObject) ExpectBalanceEqual(expBalance *big.Int) type StorageResponseExpectObject struct { *ExpectEnv Call string + Account common.Address + Key common.Hash + Number *big.Int Storage []byte Error error ErrorCode int @@ -646,6 +801,9 @@ func (tec *TestEngineClient) TestStorageAt(account common.Address, key common.Ha ret := &StorageResponseExpectObject{ ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "StorageAt", + Account: account, + Key: key, + Number: number, Storage: storage, Error: err, } @@ -667,7 +825,7 @@ func (exp *StorageResponseExpectObject) ExpectBigIntStorageEqual(expBigInt *big. bigInt.SetBytes(exp.Storage) if ((bigInt == nil || expBigInt == nil) && bigInt != expBigInt) || (bigInt != nil && expBigInt != nil && bigInt.Cmp(expBigInt) != 0) { - exp.Fatalf("FAIL (%s): Unexpected storage on %s: %v, expected=%v", exp.TestName, exp.Call, bigInt, expBigInt) + exp.Fatalf("FAIL (%s): Unexpected storage on %s (addr=%s, key=%s, block=%d): got=%d, expected=%d", exp.TestName, exp.Call, exp.Account, exp.Key, exp.Number, bigInt, expBigInt) } }