diff --git a/clients/erigon/mapper.jq b/clients/erigon/mapper.jq index 3c0da79ed6..c033bd22ce 100644 --- a/clients/erigon/mapper.jq +++ b/clients/erigon/mapper.jq @@ -51,5 +51,6 @@ def to_bool: "berlinBlock": env.HIVE_FORK_BERLIN|to_int, "londonBlock": env.HIVE_FORK_LONDON|to_int, "terminalTotalDifficulty": env.HIVE_TERMINAL_TOTAL_DIFFICULTY|to_int, + "shanghaiTime": env.HIVE_SHANGHAI_TIMESTAMP|to_int, }|remove_empty } diff --git a/clients/go-ethereum/Dockerfile b/clients/go-ethereum/Dockerfile index 27564e7d18..debceeba20 100644 --- a/clients/go-ethereum/Dockerfile +++ b/clients/go-ethereum/Dockerfile @@ -4,29 +4,30 @@ # produce a very minimalistic container that can be reused many times without # needing to constantly rebuild. -ARG branch=latest -FROM ethereum/client-go:$branch +ARG branch=withdrawals-timestamp +# FROM ethereum/client-go:$branch -RUN apk add --update bash curl jq +# RUN apk add --update bash curl jq -# FROM alpine:latest -# ARG branch=master +FROM alpine:latest +ARG branch=master # Build go-ethereum on the fly and delete all build tools afterwards -# RUN \ -# apk add --update bash curl jq go git make gcc musl-dev \ -# ca-certificates linux-headers && \ -# git clone --depth 1 --branch $branch https://github.com/ethereum/go-ethereum && \ -# (cd go-ethereum && make geth) && \ -# (cd go-ethereum && \ -# echo "{}" \ -# | jq ".+ {\"repo\":\"$(git config --get remote.origin.url)\"}" \ -# | jq ".+ {\"branch\":\"$(git rev-parse --abbrev-ref HEAD)\"}" \ -# | jq ".+ {\"commit\":\"$(git rev-parse HEAD)\"}" \ -# > /version.json) && \ -# cp go-ethereum/build/bin/geth /geth && \ -# apk del go git make gcc musl-dev linux-headers && \ -# rm -rf /go-ethereum && rm -rf /var/cache/apk/* +RUN echo https://dl-cdn.alpinelinux.org/alpine/edge/testing >> /etc/apk/repositories +RUN \ + apk add --update bash curl jq go delve git make gcc musl-dev \ + ca-certificates linux-headers && \ + git clone --depth 1 --branch withdrawals-timestamp https://github.com/lightclient/go-ethereum && \ + (cd go-ethereum && make geth) && \ + (cd go-ethereum && \ + echo "{}" \ + | jq ".+ {\"repo\":\"$(git config --get remote.origin.url)\"}" \ + | jq ".+ {\"branch\":\"$(git rev-parse --abbrev-ref HEAD)\"}" \ + | jq ".+ {\"commit\":\"$(git rev-parse HEAD)\"}" \ + > /version.json) && \ + cp go-ethereum/build/bin/geth /usr/local/bin/geth && \ + apk del go git make gcc musl-dev linux-headers && \ + rm -rf /var/cache/apk/* RUN /usr/local/bin/geth console --exec 'console.log(admin.nodeInfo.name)' --maxpeers=0 --nodiscover --dev 2>/dev/null | head -1 > /version.txt @@ -43,7 +44,7 @@ RUN chmod +x /hive-bin/enode.sh ADD genesis.json /genesis.json # Export the usual networking ports to allow outside access to the node -EXPOSE 8545 8546 8547 8551 30303 30303/udp +EXPOSE 8545 8546 8547 8551 30303 30303/udp 40000 # Generate the ethash verification caches RUN \ diff --git a/clients/go-ethereum/geth.sh b/clients/go-ethereum/geth.sh index afe47ca6f5..59f944b9be 100644 --- a/clients/go-ethereum/geth.sh +++ b/clients/go-ethereum/geth.sh @@ -173,5 +173,10 @@ fi # Run the go-ethereum implementation with the requested flags. FLAGS="$FLAGS --nat=none" -echo "Running go-ethereum with flags $FLAGS" -$geth $FLAGS +if [ "$HIVE_CLIENT_DEBUGGING" != "" ]; then + echo "Running go-ethereum in debugger mode with flags $FLAGS" + "dlv" "--listen=:40000" "--headless=true" "--api-version=2" "--accept-multiclient" "--allow-non-terminal-interactive" "exec" $geth -- $FLAGS +else + echo "Running go-ethereum with flags $FLAGS" + $geth $FLAGS +fi \ No newline at end of file diff --git a/clients/go-ethereum/mapper.jq b/clients/go-ethereum/mapper.jq index d416bd6c3c..bbad21c66a 100644 --- a/clients/go-ethereum/mapper.jq +++ b/clients/go-ethereum/mapper.jq @@ -54,5 +54,6 @@ def to_bool: "londonBlock": env.HIVE_FORK_LONDON|to_int, "mergeForkBlock": env.HIVE_MERGE_BLOCK_ID|to_int, "terminalTotalDifficulty": env.HIVE_TERMINAL_TOTAL_DIFFICULTY|to_int, + "shanghaiTime": env.HIVE_SHANGHAI_TIMESTAMP|to_int, }|remove_empty } diff --git a/clients/nethermind/mapper.jq b/clients/nethermind/mapper.jq index 937d64c43f..4af69952c1 100644 --- a/clients/nethermind/mapper.jq +++ b/clients/nethermind/mapper.jq @@ -134,7 +134,10 @@ def clique_engine: "eip3198Transition": env.HIVE_FORK_LONDON|to_hex, # Merge - "MergeForkIdTransition": env.HIVE_MERGE_BLOCK_ID|to_hex, + # "MergeForkIdTransition": env.HIVE_MERGE_BLOCK_ID|to_hex, + + # Shanghai + "eip4895TransitionTimestamp": env.HIVE_SHANGHAI_TIMESTAMP|to_hex, # Other chain parameters "networkID": env.HIVE_NETWORK_ID|to_hex, diff --git a/go.work.sum b/go.work.sum index 04f870e551..4e3d136b24 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,4 +1,13 @@ +cloud.google.com/go/bigtable v1.2.0 h1:F4cCmA4nuV84V5zYQ3MKY+M1Cw1avHDuf3S/LcZPA9c= +github.com/DATA-DOG/go-sqlmock v1.3.3 h1:CWUqKXe0s8A2z6qCgkP4Kru7wC11YoAnoupUKFDnH08= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/apache/arrow/go/arrow v0.0.0-20191024131854-af6fa24be0db h1:nxAtV4VajJDhKysp2kdcJZsq8Ss1xSA0vZTkVHHJd0E= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/c-bata/go-prompt v0.2.2 h1:uyKRz6Z6DUyj49QVijyM339UJV9yhbr70gESwbNU3e0= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5 h1:2U0HzY8BJ8hVwDKIzp7y4voR9CX/nvcfymLmg2UiOio= +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/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= diff --git a/simulators/ethereum/engine/README.md b/simulators/ethereum/engine/README.md index 22f633037c..b13234ac06 100644 --- a/simulators/ethereum/engine/README.md +++ b/simulators/ethereum/engine/README.md @@ -455,4 +455,7 @@ Invalid token error is expected. - Positive time drift, within limit, correct secret: Engine API call where the `iat` claim contains a positive time drift smaller than the maximum threshold, and the secret to calculate the token is correct. -No error is expected. \ No newline at end of file +No error is expected. + +## Engine API Shanghai Upgrade Tests: +See [withdrawals](suites/withdrawals/README.md). \ No newline at end of file diff --git a/simulators/ethereum/engine/client/engine.go b/simulators/ethereum/engine/client/engine.go index 767063688f..ba3aa9cdcb 100644 --- a/simulators/ethereum/engine/client/engine.go +++ b/simulators/ethereum/engine/client/engine.go @@ -4,6 +4,7 @@ import ( "context" "math/big" + "github.com/ethereum/go-ethereum/core" api "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/hive/hivesim" @@ -21,15 +22,20 @@ type Eth interface { StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) + TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) } type Engine interface { - ForkchoiceUpdatedV1(ctx context.Context, fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributesV1) (api.ForkChoiceResponse, error) - GetPayloadV1(ctx context.Context, payloadId *api.PayloadID) (api.ExecutableDataV1, error) - NewPayloadV1(ctx context.Context, payload *api.ExecutableDataV1) (api.PayloadStatusV1, error) + ForkchoiceUpdatedV1(ctx context.Context, fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) (api.ForkChoiceResponse, error) + ForkchoiceUpdatedV2(ctx context.Context, fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) (api.ForkChoiceResponse, error) - LatestForkchoiceSent() (fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributesV1) - LatestNewPayloadSent() (payload *api.ExecutableDataV1) + 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) + + LatestForkchoiceSent() (fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) + LatestNewPayloadSent() (payload *api.ExecutableData) LatestForkchoiceResponse() (fcuResponse *api.ForkChoiceResponse) LatestNewPayloadResponse() (payloadResponse *api.PayloadStatusV1) @@ -57,7 +63,7 @@ type EngineClient interface { } type EngineStarter interface { - StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...EngineClient) (EngineClient, error) + StartClient(T *hivesim.T, testContext context.Context, genesis *core.Genesis, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...EngineClient) (EngineClient, error) } var ( diff --git a/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go b/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go index b75af5c040..4346d2d93c 100644 --- a/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go +++ b/simulators/ethereum/engine/client/hive_rpc/hive_rpc.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" api "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" @@ -34,7 +35,7 @@ type HiveRPCEngineStarter struct { JWTSecret []byte } -func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (client.EngineClient, error) { +func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Context, genesis *core.Genesis, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (client.EngineClient, error) { var ( clientType = s.ClientType enginePort = s.EnginePort @@ -45,10 +46,10 @@ func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Cont if clientType == "" { cs, err := T.Sim.ClientTypes() if err != nil { - return nil, fmt.Errorf("Client type was not supplied and simulator returned error on trying to get all client types: %v", err) + return nil, fmt.Errorf("client type was not supplied and simulator returned error on trying to get all client types: %v", err) } - if cs == nil || len(cs) == 0 { - return nil, fmt.Errorf("Client type was not supplied and simulator returned empty client types: %v", cs) + if len(cs) == 0 { + return nil, fmt.Errorf("client type was not supplied and simulator returned empty client types: %v", cs) } clientType = cs[0].Name } @@ -64,24 +65,21 @@ func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Cont if s.ChainFile != "" { ClientFiles = ClientFiles.Set("/chain.rlp", "./chains/"+s.ChainFile) } - if _, ok := ClientFiles["/genesis.json"]; !ok { - return nil, fmt.Errorf("Cannot start without genesis file") - } if ttd == nil { if ttdStr, ok := ClientParams["HIVE_TERMINAL_TOTAL_DIFFICULTY"]; ok { // Retrieve TTD from parameters ttd, ok = new(big.Int).SetString(ttdStr, 10) if !ok { - return nil, fmt.Errorf("Unable to parse TTD from parameters") + return nil, fmt.Errorf("unable to parse TTD from parameters") } } } else { // Real TTD must be calculated adding the genesis difficulty - ttdInt := helper.CalculateRealTTD(ClientFiles["/genesis.json"], ttd.Int64()) + ttdInt := helper.CalculateRealTTD(genesis, ttd.Int64()) ClientParams = ClientParams.Set("HIVE_TERMINAL_TOTAL_DIFFICULTY", fmt.Sprintf("%d", ttdInt)) ttd = big.NewInt(ttdInt) } - if bootClients != nil && len(bootClients) > 0 { + if len(bootClients) > 0 { var ( enodes = make([]string, len(bootClients)) err error @@ -89,7 +87,7 @@ func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Cont for i, bootClient := range bootClients { enodes[i], err = bootClient.EnodeURL() if err != nil { - return nil, fmt.Errorf("Unable to obtain bootnode: %v", err) + return nil, fmt.Errorf("unable to obtain bootnode: %v", err) } } enodeString := strings.Join(enodes, ",") @@ -97,14 +95,18 @@ func (s HiveRPCEngineStarter) StartClient(T *hivesim.T, testContext context.Cont } // Start the client and create the engine client object - c := T.StartClient(clientType, ClientParams, hivesim.WithStaticFiles(ClientFiles)) + genesisStart, err := helper.GenesisStartOption(genesis) + if err != nil { + return nil, err + } + c := T.StartClient(clientType, genesisStart, ClientParams, hivesim.WithStaticFiles(ClientFiles)) if err := CheckEthEngineLive(c); err != nil { return nil, fmt.Errorf("Engine/Eth ports were never open for client: %v", err) } ec := NewHiveRPCEngineClient(c, enginePort, ethPort, jwtSecret, ttd, &helper.LoggingRoundTrip{ - T: T, - Hc: c, - Inner: http.DefaultTransport, + Logger: T, + ID: c.Container, + Inner: http.DefaultTransport, }) return ec, nil } @@ -152,10 +154,10 @@ type HiveRPCEngineClient struct { // Engine updates info latestFcUStateSent *api.ForkchoiceStateV1 - latestPAttrSent *api.PayloadAttributesV1 + latestPAttrSent *api.PayloadAttributes latestFcUResponse *api.ForkChoiceResponse - latestPayloadSent *api.ExecutableDataV1 + latestPayloadSent *api.ExecutableData latestPayloadStatusReponse *api.PayloadStatusV1 // Test account nonces @@ -164,23 +166,25 @@ type HiveRPCEngineClient struct { // NewClient creates a engine client that uses the given RPC client. func NewHiveRPCEngineClient(h *hivesim.Client, enginePort int, ethPort int, jwtSecretBytes []byte, ttd *big.Int, transport http.RoundTripper) *HiveRPCEngineClient { - client := &http.Client{ - Transport: transport, - } // Prepare HTTP Client - rpcHttpClient, _ := rpc.DialHTTPWithClient(fmt.Sprintf("http://%s:%d/", h.IP, enginePort), client) + httpClient := rpc.WithHTTPClient(&http.Client{Transport: transport}) + + engineRpcClient, err := rpc.DialOptions(context.Background(), fmt.Sprintf("http://%s:%d/", h.IP, enginePort), httpClient) + if err != nil { + panic(err) + } // Prepare ETH Client - client = &http.Client{ - Transport: transport, + ethRpcClient, err := rpc.DialOptions(context.Background(), fmt.Sprintf("http://%s:%d/", h.IP, ethPort), httpClient) + if err != nil { + panic(err) } - rpcClient, _ := rpc.DialHTTPWithClient(fmt.Sprintf("http://%s:%d/", h.IP, ethPort), client) - eth := ethclient.NewClient(rpcClient) + eth := ethclient.NewClient(ethRpcClient) return &HiveRPCEngineClient{ h: h, - c: rpcHttpClient, + c: engineRpcClient, Client: eth, - cEth: rpcClient, + cEth: ethRpcClient, ttd: ttd, JWTSecretBytes: jwtSecretBytes, accTxInfoMap: make(map[common.Address]*AccountTransactionInfo), @@ -293,38 +297,82 @@ func (ec *HiveRPCEngineClient) PrepareDefaultAuthCallToken() error { } // Engine API Call Methods -func (ec *HiveRPCEngineClient) ForkchoiceUpdatedV1(ctx context.Context, fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributesV1) (api.ForkChoiceResponse, error) { +func (ec *HiveRPCEngineClient) ForkchoiceUpdated(ctx context.Context, version int, fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) (api.ForkChoiceResponse, error) { var result api.ForkChoiceResponse if err := ec.PrepareDefaultAuthCallToken(); err != nil { return result, err } ec.latestFcUStateSent = fcState ec.latestPAttrSent = pAttributes - err := ec.c.CallContext(ctx, &result, "engine_forkchoiceUpdatedV1", fcState, pAttributes) + err := ec.c.CallContext(ctx, + &result, + fmt.Sprintf("engine_forkchoiceUpdatedV%d", version), + fcState, + pAttributes) ec.latestFcUResponse = &result return result, err } -func (ec *HiveRPCEngineClient) GetPayloadV1(ctx context.Context, payloadId *api.PayloadID) (api.ExecutableDataV1, error) { - var result api.ExecutableDataV1 - if err := ec.PrepareDefaultAuthCallToken(); err != nil { - return result, err +func (ec *HiveRPCEngineClient) ForkchoiceUpdatedV1(ctx context.Context, fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) (api.ForkChoiceResponse, error) { + return ec.ForkchoiceUpdated(ctx, 1, fcState, pAttributes) +} + +func (ec *HiveRPCEngineClient) ForkchoiceUpdatedV2(ctx context.Context, fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) (api.ForkChoiceResponse, error) { + return ec.ForkchoiceUpdated(ctx, 2, fcState, pAttributes) +} + +func (ec *HiveRPCEngineClient) GetPayload(ctx context.Context, version int, payloadId *api.PayloadID) (api.ExecutableData, *big.Int, error) { + var ( + executableData api.ExecutableData + blockValue *big.Int + err error + rpcString = fmt.Sprintf("engine_getPayloadV%d", version) + ) + + if err = ec.PrepareDefaultAuthCallToken(); err != nil { + return executableData, nil, err } - err := ec.c.CallContext(ctx, &result, "engine_getPayloadV1", payloadId) - return result, err + + if version == 2 { + var response api.ExecutableDataV2 + err = ec.c.CallContext(ctx, &response, rpcString, payloadId) + executableData = *response.ExecutionPayload + blockValue = response.BlockValue + } else { + err = ec.c.CallContext(ctx, &executableData, rpcString, payloadId) + } + + return executableData, blockValue, err } -func (ec *HiveRPCEngineClient) NewPayloadV1(ctx context.Context, payload *api.ExecutableDataV1) (api.PayloadStatusV1, error) { +func (ec *HiveRPCEngineClient) GetPayloadV1(ctx context.Context, payloadId *api.PayloadID) (api.ExecutableData, error) { + ed, _, err := ec.GetPayload(ctx, 1, payloadId) + return ed, err +} + +func (ec *HiveRPCEngineClient) GetPayloadV2(ctx context.Context, payloadId *api.PayloadID) (api.ExecutableData, *big.Int, error) { + return ec.GetPayload(ctx, 2, payloadId) +} + +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 { return result, err } ec.latestPayloadSent = payload - err := ec.c.CallContext(ctx, &result, "engine_newPayloadV1", 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) NewPayloadV2(ctx context.Context, payload *api.ExecutableData) (api.PayloadStatusV1, error) { + return ec.NewPayload(ctx, 2, payload) +} + func (ec *HiveRPCEngineClient) ExchangeTransitionConfigurationV1(ctx context.Context, tConf *api.TransitionConfigurationV1) (api.TransitionConfigurationV1, error) { var result api.TransitionConfigurationV1 err := ec.c.CallContext(ctx, &result, "engine_exchangeTransitionConfigurationV1", tConf) @@ -366,11 +414,11 @@ func (ec *HiveRPCEngineClient) PostRunVerifications() error { return nil } -func (ec *HiveRPCEngineClient) LatestForkchoiceSent() (fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributesV1) { +func (ec *HiveRPCEngineClient) LatestForkchoiceSent() (fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) { return ec.latestFcUStateSent, ec.latestPAttrSent } -func (ec *HiveRPCEngineClient) LatestNewPayloadSent() *api.ExecutableDataV1 { +func (ec *HiveRPCEngineClient) LatestNewPayloadSent() *api.ExecutableData { return ec.latestPayloadSent } diff --git a/simulators/ethereum/engine/client/node/node.go b/simulators/ethereum/engine/client/node/node.go index 7db6a552e6..edf1a519f1 100644 --- a/simulators/ethereum/engine/client/node/node.go +++ b/simulators/ethereum/engine/client/node/node.go @@ -19,6 +19,7 @@ import ( "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth" @@ -90,20 +91,15 @@ var ( DefaultTerminalBlockSiblingDepth = big.NewInt(1) ) -func (s GethNodeEngineStarter) StartClient(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (client.EngineClient, error) { - return s.StartGethNode(T, testContext, ClientParams, ClientFiles, bootClients...) +func (s GethNodeEngineStarter) StartClient(T *hivesim.T, testContext context.Context, genesis *core.Genesis, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (client.EngineClient, error) { + return s.StartGethNode(T, testContext, genesis, ClientParams, ClientFiles, bootClients...) } -func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.Context, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (*GethNode, error) { +func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.Context, genesis *core.Genesis, ClientParams hivesim.Params, ClientFiles hivesim.Params, bootClients ...client.EngineClient) (*GethNode, error) { var ( ttd = s.TerminalTotalDifficulty err error ) - genesisPath, ok := ClientFiles["/genesis.json"] - if !ok { - return nil, fmt.Errorf("Cannot start without genesis file") - } - genesis := helper.LoadGenesis(genesisPath) if ttd == nil { if ttdStr, ok := ClientParams["HIVE_TERMINAL_TOTAL_DIFFICULTY"]; ok { @@ -114,10 +110,15 @@ func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.C } } } else { - ttd = big.NewInt(helper.CalculateRealTTD(genesisPath, ttd.Int64())) + ttd = big.NewInt(helper.CalculateRealTTD(genesis, ttd.Int64())) ClientParams = ClientParams.Set("HIVE_TERMINAL_TOTAL_DIFFICULTY", fmt.Sprintf("%d", ttd)) } - genesis.Config.TerminalTotalDifficulty = ttd + + // Not sure if this hack works + genesisCopy := *genesis + configCopy := *genesisCopy.Config + configCopy.TerminalTotalDifficulty = ttd + genesisCopy.Config = &configCopy var enodes []string if bootClients != nil && len(bootClients) > 0 { @@ -150,7 +151,7 @@ func (s GethNodeEngineStarter) StartGethNode(T *hivesim.T, testContext context.C s.Config.TerminalBlockSiblingDepth = DefaultTerminalBlockSiblingDepth } - g, err := newNode(s.Config, enodes, &genesis) + g, err := newNode(s.Config, enodes, &genesisCopy) if err != nil { return nil, err } @@ -190,10 +191,10 @@ type GethNode struct { // Engine updates info latestFcUStateSent *beacon.ForkchoiceStateV1 - latestPAttrSent *beacon.PayloadAttributesV1 + latestPAttrSent *beacon.PayloadAttributes latestFcUResponse *beacon.ForkChoiceResponse - latestPayloadSent *beacon.ExecutableDataV1 + latestPayloadSent *beacon.ExecutableData latestPayloadStatusReponse *beacon.PayloadStatusV1 // Test specific configuration @@ -237,7 +238,7 @@ func restart(startConfig GethNodeTestConfiguration, bootnodes []string, datadir SyncMode: downloader.FullSync, DatabaseCache: 256, DatabaseHandles: 256, - TxPool: core.DefaultTxPoolConfig, + TxPool: txpool.DefaultConfig, GPO: ethconfig.Defaults.GPO, Ethash: ethconfig.Defaults.Ethash, Miner: ethconfig.Defaults.Miner, @@ -383,7 +384,7 @@ func (n *GethNode) PrepareNextBlock(currentBlock *types.Block) (*state.StateDB, if n.eth.BlockChain().Config().IsLondon(nextHeader.Number) { nextHeader.BaseFee = misc.CalcBaseFee(n.eth.BlockChain().Config(), currentBlock.Header()) if !n.eth.BlockChain().Config().IsLondon(currentBlock.Number()) { - parentGasLimit := currentBlock.GasLimit() * params.ElasticityMultiplier + parentGasLimit := currentBlock.GasLimit() * n.eth.BlockChain().Config().ElasticityMultiplier() nextHeader.GasLimit = parentGasLimit } } @@ -397,7 +398,7 @@ func (n *GethNode) PrepareNextBlock(currentBlock *types.Block) (*state.StateDB, if err != nil { panic(err) } - b, err := n.ethashEngine.FinalizeAndAssemble(n.eth.BlockChain(), nextHeader, state, nil, nil, nil) + b, err := n.ethashEngine.FinalizeAndAssemble(n.eth.BlockChain(), nextHeader, state, nil, nil, nil, nil) return state, b, err } @@ -688,14 +689,20 @@ func (n *GethNode) SetBlock(block *types.Block, parentNumber uint64, parentRoot } // Engine API -func (n *GethNode) NewPayloadV1(ctx context.Context, pl *beacon.ExecutableDataV1) (beacon.PayloadStatusV1, error) { +func (n *GethNode) NewPayloadV1(ctx context.Context, pl *beacon.ExecutableData) (beacon.PayloadStatusV1, error) { n.latestPayloadSent = pl resp, err := n.api.NewPayloadV1(*pl) n.latestPayloadStatusReponse = &resp return resp, err } +func (n *GethNode) NewPayloadV2(ctx context.Context, pl *beacon.ExecutableData) (beacon.PayloadStatusV1, error) { + n.latestPayloadSent = pl + resp, err := n.api.NewPayloadV2(*pl) + n.latestPayloadStatusReponse = &resp + return resp, err +} -func (n *GethNode) ForkchoiceUpdatedV1(ctx context.Context, fcs *beacon.ForkchoiceStateV1, payload *beacon.PayloadAttributesV1) (beacon.ForkChoiceResponse, error) { +func (n *GethNode) ForkchoiceUpdatedV1(ctx context.Context, fcs *beacon.ForkchoiceStateV1, payload *beacon.PayloadAttributes) (beacon.ForkChoiceResponse, error) { n.latestFcUStateSent = fcs n.latestPAttrSent = payload fcr, err := n.api.ForkchoiceUpdatedV1(*fcs, payload) @@ -703,14 +710,30 @@ func (n *GethNode) ForkchoiceUpdatedV1(ctx context.Context, fcs *beacon.Forkchoi return fcr, err } -func (n *GethNode) GetPayloadV1(ctx context.Context, payloadId *beacon.PayloadID) (beacon.ExecutableDataV1, error) { +func (n *GethNode) ForkchoiceUpdatedV2(ctx context.Context, fcs *beacon.ForkchoiceStateV1, payload *beacon.PayloadAttributes) (beacon.ForkChoiceResponse, error) { + n.latestFcUStateSent = fcs + n.latestPAttrSent = payload + fcr, err := n.api.ForkchoiceUpdatedV2(*fcs, payload) + n.latestFcUResponse = &fcr + return fcr, err +} + +func (n *GethNode) GetPayloadV1(ctx context.Context, payloadId *beacon.PayloadID) (beacon.ExecutableData, error) { p, err := n.api.GetPayloadV1(*payloadId) if p == nil || err != nil { - return beacon.ExecutableDataV1{}, err + return beacon.ExecutableData{}, err } return *p, err } +func (n *GethNode) GetPayloadV2(ctx context.Context, payloadId *beacon.PayloadID) (beacon.ExecutableData, *big.Int, error) { + p, err := n.api.GetPayloadV2(*payloadId) + if p == nil || err != nil { + return beacon.ExecutableData{}, nil, err + } + return *p.ExecutionPayload, p.BlockValue, err +} + // Eth JSON RPC const ( SafeBlockNumber = rpc.BlockNumber(-4) // This is not yet true @@ -806,6 +829,10 @@ func (n *GethNode) NonceAt(ctx context.Context, account common.Address, blockNum return stateDB.GetNonce(account), nil } +func (n *GethNode) TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) { + panic("NOT IMPLEMENTED") +} + func (n *GethNode) GetBlockTotalDifficulty(ctx context.Context, hash common.Hash) (*big.Int, error) { block := n.eth.BlockChain().GetBlockByHash(hash) if block == nil { @@ -878,11 +905,11 @@ func (n *GethNode) PostRunVerifications() error { return nil } -func (n *GethNode) LatestForkchoiceSent() (fcState *beacon.ForkchoiceStateV1, pAttributes *beacon.PayloadAttributesV1) { +func (n *GethNode) LatestForkchoiceSent() (fcState *beacon.ForkchoiceStateV1, pAttributes *beacon.PayloadAttributes) { return n.latestFcUStateSent, n.latestPAttrSent } -func (n *GethNode) LatestNewPayloadSent() (payload *beacon.ExecutableDataV1) { +func (n *GethNode) LatestNewPayloadSent() (payload *beacon.ExecutableData) { return n.latestPayloadSent } diff --git a/simulators/ethereum/engine/clmock/clmock.go b/simulators/ethereum/engine/clmock/clmock.go index a4e7f97c8c..7e3b82daf5 100644 --- a/simulators/ethereum/engine/clmock/clmock.go +++ b/simulators/ethereum/engine/clmock/clmock.go @@ -2,6 +2,7 @@ package clmock import ( "context" + "encoding/json" "fmt" "math/big" "math/rand" @@ -41,22 +42,27 @@ type CLMocker struct { // Wait time before attempting to get the payload PayloadProductionClientDelay time.Duration + // Block production related + BlockTimestampIncrement *big.Int + // Block Production State - NextBlockProducer client.EngineClient - NextFeeRecipient common.Address - NextPayloadID *api.PayloadID + NextBlockProducer client.EngineClient + NextFeeRecipient common.Address + NextPayloadID *api.PayloadID + CurrentPayloadNumber uint64 // PoS Chain History Information PrevRandaoHistory map[uint64]common.Hash - ExecutedPayloadHistory map[uint64]api.ExecutableDataV1 + ExecutedPayloadHistory map[uint64]api.ExecutableData HeadHashHistory []common.Hash // Latest broadcasted data using the PoS Engine API LatestHeadNumber *big.Int LatestHeader *types.Header - LatestPayloadBuilt api.ExecutableDataV1 - LatestPayloadAttributes api.PayloadAttributesV1 - LatestExecutedPayload api.ExecutableDataV1 + LatestPayloadBuilt api.ExecutableData + LatestBlockValue *big.Int + LatestPayloadAttributes api.PayloadAttributes + LatestExecutedPayload api.ExecutableData LatestForkchoice api.ForkchoiceStateV1 // Merge related @@ -65,12 +71,20 @@ type CLMocker struct { TransitionPayloadTimestamp *big.Int SafeSlotsToImportOptimistically *big.Int + // Shanghai Related + ShanghaiTimestamp *big.Int + NextWithdrawals types.Withdrawals + // Global context which all procedures shall stop TestContext context.Context TimeoutContext context.Context } -func NewCLMocker(t *hivesim.T, slotsToSafe, slotsToFinalized, safeSlotsToImportOptimistically *big.Int) *CLMocker { +func isShanghai(blockTimestamp uint64, shanghaiTimestamp *big.Int) bool { + return shanghaiTimestamp != nil && big.NewInt(int64(blockTimestamp)).Cmp(shanghaiTimestamp) >= 0 +} + +func NewCLMocker(t *hivesim.T, slotsToSafe, slotsToFinalized, safeSlotsToImportOptimistically *big.Int, shanghaiTime *big.Int) *CLMocker { // Init random seed for different purposes seed := time.Now().Unix() t.Logf("Randomness seed: %v\n", seed) @@ -89,7 +103,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.ExecutableDataV1{}, + ExecutedPayloadHistory: map[uint64]api.ExecutableData{}, SlotsToSafe: slotsToSafe, SlotsToFinalized: slotsToFinalized, SafeSlotsToImportOptimistically: safeSlotsToImportOptimistically, @@ -104,7 +118,8 @@ func NewCLMocker(t *hivesim.T, slotsToSafe, slotsToFinalized, safeSlotsToImportO SafeBlockHash: common.Hash{}, FinalizedBlockHash: common.Hash{}, }, - TestContext: context.Background(), + ShanghaiTimestamp: shanghaiTime, + TestContext: context.Background(), } return newCLMocker @@ -171,6 +186,8 @@ func (cl *CLMocker) SetTTDBlockClient(ec client.EngineClient) { cl.Fatalf("CLMocker: Attempted to set TTD Block when TTD had not been reached: %d > %d", ec.TerminalTotalDifficulty(), ttd) } else { cl.Logf("CLMocker: TTD has been reached at block %d (%d>=%d)\n", cl.LatestHeader.Number, ttd, ec.TerminalTotalDifficulty()) + jsH, _ := json.MarshalIndent(cl.LatestHeader, "", " ") + cl.Logf("CLMocker: Block %d: %s\n", cl.LatestHeader.Number, jsH) } cl.TTDReached = true @@ -205,6 +222,24 @@ func (cl *CLMocker) IsBlockPoS(bn *big.Int) bool { return true } +// Return the per-block timestamp value increment +func (cl *CLMocker) GetTimestampIncrement() uint64 { + if cl.BlockTimestampIncrement == nil { + return 1 + } + return cl.BlockTimestampIncrement.Uint64() +} + +// Returns the timestamp value to be included in the next payload attributes +func (cl *CLMocker) GetNextBlockTimestamp() uint64 { + if cl.FirstPoSBlockNumber == nil && cl.TransitionPayloadTimestamp != nil { + // We are producing the transition payload and there's a value specified + // for this specific payload + return cl.TransitionPayloadTimestamp.Uint64() + } + return cl.LatestHeader.Time + cl.GetTimestampIncrement() +} + // Picks the next payload producer from the set of clients registered func (cl *CLMocker) pickNextPayloadProducer() { if len(cl.EngineClients) == 0 { @@ -244,21 +279,23 @@ func (cl *CLMocker) pickNextPayloadProducer() { } } -func (cl *CLMocker) GetNextPayloadID() { +func (cl *CLMocker) SetNextWithdrawals(nextWithdrawals types.Withdrawals) { + cl.NextWithdrawals = nextWithdrawals +} + +func (cl *CLMocker) RequestNextPayload() { // Generate a random value for the PrevRandao field nextPrevRandao := common.Hash{} rand.Read(nextPrevRandao[:]) - cl.LatestPayloadAttributes = api.PayloadAttributesV1{ + cl.LatestPayloadAttributes = api.PayloadAttributes{ Random: nextPrevRandao, SuggestedFeeRecipient: cl.NextFeeRecipient, + Timestamp: cl.GetNextBlockTimestamp(), } - if cl.FirstPoSBlockNumber == nil && cl.TransitionPayloadTimestamp != nil { - // We are producing the transition payload - cl.LatestPayloadAttributes.Timestamp = cl.TransitionPayloadTimestamp.Uint64() - } else { - cl.LatestPayloadAttributes.Timestamp = cl.LatestHeader.Time + 1 + if isShanghai(cl.LatestPayloadAttributes.Timestamp, cl.ShanghaiTimestamp) && cl.NextWithdrawals != nil { + cl.LatestPayloadAttributes.Withdrawals = cl.NextWithdrawals } // Save random value @@ -266,9 +303,21 @@ func (cl *CLMocker) GetNextPayloadID() { ctx, cancel := context.WithTimeout(cl.TestContext, globals.RPCTimeout) defer cancel() - resp, err := cl.NextBlockProducer.ForkchoiceUpdatedV1(ctx, &cl.LatestForkchoice, &cl.LatestPayloadAttributes) + var ( + resp api.ForkChoiceResponse + fcUVersion int + err error + ) + if isShanghai(cl.LatestPayloadAttributes.Timestamp, cl.ShanghaiTimestamp) { + fcUVersion = 2 + resp, err = cl.NextBlockProducer.ForkchoiceUpdatedV2(ctx, &cl.LatestForkchoice, &cl.LatestPayloadAttributes) + + } else { + fcUVersion = 1 + resp, err = cl.NextBlockProducer.ForkchoiceUpdatedV1(ctx, &cl.LatestForkchoice, &cl.LatestPayloadAttributes) + } if err != nil { - cl.Fatalf("CLMocker: Could not send forkchoiceUpdatedV1 (%v): %v", cl.NextBlockProducer.ID(), err) + cl.Fatalf("CLMocker: Could not send forkchoiceUpdatedV%d (%v): %v", fcUVersion, cl.NextBlockProducer.ID(), err) } if resp.PayloadStatus.Status != api.VALID { cl.Fatalf("CLMocker: Unexpected forkchoiceUpdated Response from Payload builder: %v", resp) @@ -283,7 +332,13 @@ func (cl *CLMocker) GetNextPayload() { var err error ctx, cancel := context.WithTimeout(cl.TestContext, globals.RPCTimeout) defer cancel() - cl.LatestPayloadBuilt, err = cl.NextBlockProducer.GetPayloadV1(ctx, cl.NextPayloadID) + if isShanghai(cl.LatestPayloadAttributes.Timestamp, cl.ShanghaiTimestamp) { + cl.LatestPayloadBuilt, cl.LatestBlockValue, err = cl.NextBlockProducer.GetPayloadV2(ctx, cl.NextPayloadID) + + } else { + cl.LatestPayloadBuilt, err = cl.NextBlockProducer.GetPayloadV1(ctx, cl.NextPayloadID) + cl.LatestBlockValue = nil + } if err != nil { cl.Fatalf("CLMocker: Could not getPayload (%v, %v): %v", cl.NextBlockProducer.ID(), cl.NextPayloadID, err) } @@ -341,7 +396,11 @@ func (cl *CLMocker) broadcastNextNewPayload() { } func (cl *CLMocker) broadcastLatestForkchoice() { - for _, resp := range cl.BroadcastForkchoiceUpdated(&cl.LatestForkchoice, nil) { + version := 1 + if isShanghai(cl.LatestExecutedPayload.Timestamp, cl.ShanghaiTimestamp) { + version = 2 + } + for _, resp := range cl.BroadcastForkchoiceUpdated(&cl.LatestForkchoice, nil, version) { if resp.Error != nil { cl.Logf("CLMocker: BroadcastForkchoiceUpdated Error (%v): %v\n", resp.Container, resp.Error) } else if resp.ForkchoiceResponse.PayloadStatus.Status == api.VALID { @@ -365,7 +424,7 @@ func (cl *CLMocker) broadcastLatestForkchoice() { type BlockProcessCallbacks struct { OnPayloadProducerSelected func() - OnGetPayloadID func() + OnRequestNextPayload func() OnGetPayload func() OnNewPayloadBroadcast func() OnForkchoiceBroadcast func() @@ -383,16 +442,26 @@ func (cl *CLMocker) ProduceSingleBlock(callbacks BlockProcessCallbacks) { cl.EngineClientsLock.Lock() defer cl.EngineClientsLock.Unlock() + cl.CurrentPayloadNumber = cl.LatestHeader.Number.Uint64() + 1 + cl.pickNextPayloadProducer() + // Check if next withdrawals necessary, test can override this value on + // `OnPayloadProducerSelected` callback + if cl.NextWithdrawals == nil { + cl.SetNextWithdrawals(make(types.Withdrawals, 0)) + } + if callbacks.OnPayloadProducerSelected != nil { callbacks.OnPayloadProducerSelected() } - cl.GetNextPayloadID() + cl.RequestNextPayload() + + cl.SetNextWithdrawals(nil) - if callbacks.OnGetPayloadID != nil { - callbacks.OnGetPayloadID() + if callbacks.OnRequestNextPayload != nil { + callbacks.OnRequestNextPayload() } // Give the client a delay between getting the payload ID and actually retrieving the payload @@ -499,13 +568,21 @@ type ExecutePayloadOutcome struct { Error error } -func (cl *CLMocker) BroadcastNewPayload(payload *api.ExecutableDataV1) []ExecutePayloadOutcome { +func (cl *CLMocker) BroadcastNewPayload(payload *api.ExecutableData) []ExecutePayloadOutcome { responses := make([]ExecutePayloadOutcome, len(cl.EngineClients)) for i, ec := range cl.EngineClients { responses[i].Container = ec.ID() ctx, cancel := context.WithTimeout(cl.TestContext, globals.RPCTimeout) defer cancel() - execPayloadResp, err := ec.NewPayloadV1(ctx, payload) + var ( + execPayloadResp api.PayloadStatusV1 + err error + ) + if isShanghai(payload.Timestamp, cl.ShanghaiTimestamp) { + execPayloadResp, err = ec.NewPayloadV2(ctx, payload) + } else { + execPayloadResp, err = ec.NewPayloadV1(ctx, payload) + } if err != nil { cl.Errorf("CLMocker: Could not ExecutePayloadV1: %v", err) responses[i].Error = err @@ -523,7 +600,7 @@ type ForkChoiceOutcome struct { Error error } -func (cl *CLMocker) BroadcastForkchoiceUpdated(fcstate *api.ForkchoiceStateV1, payloadAttr *api.PayloadAttributesV1) []ForkChoiceOutcome { +func (cl *CLMocker) BroadcastForkchoiceUpdated(fcstate *api.ForkchoiceStateV1, payloadAttr *api.PayloadAttributes, version int) []ForkChoiceOutcome { responses := make([]ForkChoiceOutcome, len(cl.EngineClients)) for i, ec := range cl.EngineClients { responses[i].Container = ec.ID() @@ -531,7 +608,15 @@ func (cl *CLMocker) BroadcastForkchoiceUpdated(fcstate *api.ForkchoiceStateV1, p if cl.IsOptimisticallySyncing() || newPayloadStatus.Status == "VALID" { ctx, cancel := context.WithTimeout(cl.TestContext, globals.RPCTimeout) defer cancel() - fcUpdatedResp, err := ec.ForkchoiceUpdatedV1(ctx, fcstate, payloadAttr) + var ( + fcUpdatedResp api.ForkChoiceResponse + err error + ) + if version == 2 { + fcUpdatedResp, err = ec.ForkchoiceUpdatedV2(ctx, fcstate, payloadAttr) + } else if version == 1 { + fcUpdatedResp, err = ec.ForkchoiceUpdatedV1(ctx, fcstate, payloadAttr) + } if err != nil { cl.Errorf("CLMocker: Could not ForkchoiceUpdatedV1: %v", err) responses[i].Error = err diff --git a/simulators/ethereum/engine/globals/globals.go b/simulators/ethereum/engine/globals/globals.go index efd65ceea6..6f205b567f 100644 --- a/simulators/ethereum/engine/globals/globals.go +++ b/simulators/ethereum/engine/globals/globals.go @@ -13,10 +13,11 @@ import ( var ( // Test chain parameters - ChainID = big.NewInt(7) - GasPrice = big.NewInt(30 * params.GWei) - GasTipPrice = big.NewInt(1 * params.GWei) - NetworkID = big.NewInt(7) + ChainID = big.NewInt(7) + GasPrice = big.NewInt(30 * params.GWei) + GasTipPrice = big.NewInt(1 * params.GWei) + NetworkID = big.NewInt(7) + GenesisTimestamp = int64(0x1234) // RPC Timeout for every call RPCTimeout = 10 * time.Second diff --git a/simulators/ethereum/engine/go.mod b/simulators/ethereum/engine/go.mod index 252cb0a150..4acc35efb9 100644 --- a/simulators/ethereum/engine/go.mod +++ b/simulators/ethereum/engine/go.mod @@ -16,11 +16,12 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/deckarep/golang-set v1.8.0 // indirect + github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect github.com/deepmap/oapi-codegen v1.12.4 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/fjl/memsize v0.0.1 // indirect + github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect @@ -32,7 +33,7 @@ require ( github.com/gorilla/websocket v1.5.0 // indirect github.com/graph-gophers/graphql-go v1.4.0 // indirect github.com/hashicorp/go-bexpr v0.1.11 // indirect - github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect + github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/uint256 v1.2.1 // indirect github.com/huin/goupnp v1.0.3 // indirect @@ -51,7 +52,6 @@ require ( github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/tsdb v0.10.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect - github.com/rjeczalik/notify v0.9.2 // indirect github.com/rs/cors v1.8.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect @@ -71,3 +71,5 @@ require ( golang.org/x/time v0.3.0 // indirect 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 diff --git a/simulators/ethereum/engine/go.sum b/simulators/ethereum/engine/go.sum index 5e8d36d095..8ad2fa4065 100644 --- a/simulators/ethereum/engine/go.sum +++ b/simulators/ethereum/engine/go.sum @@ -89,8 +89,8 @@ github.com/dave/jennifer v1.2.0/go.mod h1:fIb+770HOpJ2fmN9EPPKOqm1vMGhB+TwXKMZhr github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= -github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= +github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= @@ -111,8 +111,6 @@ github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8E github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 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.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s= -github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= github.com/ethereum/hive v0.0.0-20221123180504-f0f647240e9b h1:uWfa1x0lA7o5O6XgFxENIxAlevy9W5U6nAp79naD7e4= github.com/ethereum/hive v0.0.0-20221123180504-f0f647240e9b/go.mod h1:G8XiiUErpj6++yb93r6y6T1nWBh4Bgoi3QFwUSbzt/U= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -123,8 +121,9 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 h1:f6D9Hr8xV8uYKlyuj8XIruxlh9WjVjdh1gIicAS7ays= github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -238,12 +237,12 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= -github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e h1:pIYdhNkDh+YENVNi3gto8n9hAmRxKxoar0iE6BLucjw= +github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT0oKJ38PylVfqohClLr3CvDC+Qcg+lhU= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.1 h1:XRtyuda/zw2l+Bq/38n5XUoEF72aSOu/77Thd9pPp2o= @@ -300,6 +299,8 @@ 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/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= @@ -421,8 +422,6 @@ github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1 github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rjeczalik/notify v0.9.2 h1:MiTWrPj55mNDHEiIX5YUSKefw/+lCQVoAFmD6oQm5w8= -github.com/rjeczalik/notify v0.9.2/go.mod h1:aErll2f0sUX9PXZnVNyeiObbmTlk5jnMoCa4QEjJeqM= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= @@ -589,7 +588,6 @@ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -624,6 +622,7 @@ golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/simulators/ethereum/engine/helper/helper.go b/simulators/ethereum/engine/helper/helper.go index 7bafe69f8e..a3e90fdfcc 100644 --- a/simulators/ethereum/engine/helper/helper.go +++ b/simulators/ethereum/engine/helper/helper.go @@ -2,6 +2,7 @@ package helper import ( "context" + "crypto/ecdsa" "strings" "sync" "time" @@ -32,10 +33,14 @@ import ( // From ethereum/rpc: // LoggingRoundTrip writes requests and responses to the test log. +type LogF interface { + Logf(format string, values ...interface{}) +} + type LoggingRoundTrip struct { - T *hivesim.T - Hc *hivesim.Client - Inner http.RoundTripper + Logger LogF + ID string + Inner http.RoundTripper } func (rt *LoggingRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) { @@ -45,7 +50,7 @@ func (rt *LoggingRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) if err != nil { return nil, err } - rt.T.Logf(">> (%s) %s", rt.Hc.Container, bytes.TrimSpace(reqBytes)) + rt.Logger.Logf(">> (%s) %s", rt.ID, bytes.TrimSpace(reqBytes)) reqCopy := *req reqCopy.Body = ioutil.NopCloser(bytes.NewReader(reqBytes)) @@ -63,33 +68,34 @@ func (rt *LoggingRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) } respCopy := *resp respCopy.Body = ioutil.NopCloser(bytes.NewReader(respBytes)) - rt.T.Logf("<< (%s) %s", rt.Hc.Container, bytes.TrimSpace(respBytes)) + rt.Logger.Logf("<< (%s) %s", rt.ID, bytes.TrimSpace(respBytes)) return &respCopy, nil } type InvalidPayloadBlockField string const ( - InvalidParentHash InvalidPayloadBlockField = "ParentHash" - InvalidStateRoot = "StateRoot" - InvalidReceiptsRoot = "ReceiptsRoot" - InvalidNumber = "Number" - InvalidGasLimit = "GasLimit" - InvalidGasUsed = "GasUsed" - InvalidTimestamp = "Timestamp" - InvalidPrevRandao = "PrevRandao" - InvalidOmmers = "Ommers" - RemoveTransaction = "Incomplete Transactions" - InvalidTransactionSignature = "Transaction Signature" - InvalidTransactionNonce = "Transaction Nonce" - InvalidTransactionGas = "Transaction Gas" - InvalidTransactionGasPrice = "Transaction GasPrice" - InvalidTransactionGasTipPrice = "Transaction GasTipCapPrice" - InvalidTransactionValue = "Transaction Value" - InvalidTransactionChainID = "Transaction ChainID" + InvalidParentHash = "ParentHash" + InvalidStateRoot = "StateRoot" + InvalidReceiptsRoot = "ReceiptsRoot" + InvalidNumber = "Number" + InvalidGasLimit = "GasLimit" + InvalidGasUsed = "GasUsed" + InvalidTimestamp = "Timestamp" + InvalidPrevRandao = "PrevRandao" + InvalidOmmers = "Ommers" + InvalidWithdrawals = "Withdrawals" + RemoveTransaction = "Incomplete Transactions" + InvalidTransactionSignature = "Transaction Signature" + InvalidTransactionNonce = "Transaction Nonce" + InvalidTransactionGas = "Transaction Gas" + InvalidTransactionGasPrice = "Transaction GasPrice" + InvalidTransactionGasTipPrice = "Transaction GasTipCapPrice" + InvalidTransactionValue = "Transaction Value" + InvalidTransactionChainID = "Transaction ChainID" ) -func TransactionInPayload(payload *api.ExecutableDataV1, tx *types.Transaction) bool { +func TransactionInPayload(payload *api.ExecutableData, tx *types.Transaction) bool { for _, bytesTx := range payload.Transactions { var currentTx types.Transaction if err := currentTx.UnmarshalBinary(bytesTx); err == nil { @@ -144,16 +150,16 @@ func gethDebugPrevRandaoTransaction(ctx context.Context, c *rpc.Client, tx *type for i, l := range er.StructLogs { if l.Op == "DIFFICULTY" || l.Op == "PREVRANDAO" { if i+1 >= len(er.StructLogs) { - return errors.New(fmt.Sprintf("No information after PREVRANDAO operation")) + return errors.New("no information after PREVRANDAO operation") } prevRandaoFound = true stack := *(er.StructLogs[i+1].Stack) if len(stack) < 1 { - return errors.New(fmt.Sprintf("Invalid stack after PREVRANDAO operation: %v", l.Stack)) + return fmt.Errorf("invalid stack after PREVRANDAO operation: %v", l.Stack) } stackHash := common.HexToHash(stack[0]) if stackHash != *expectedPrevRandao { - return errors.New(fmt.Sprintf("Invalid stack after PREVRANDAO operation, %v != %v", stackHash, expectedPrevRandao)) + return fmt.Errorf("invalid stack after PREVRANDAO operation, %v != %v", stackHash, expectedPrevRandao) } } } @@ -171,6 +177,12 @@ func nethermindDebugPrevRandaoTransaction(ctx context.Context, c *rpc.Client, tx return nil } +func bytesSource(data []byte) func() (io.ReadCloser, error) { + return func() (io.ReadCloser, error) { + return ioutil.NopCloser(bytes.NewReader(data)), nil + } +} + func LoadChain(path string) types.Blocks { fh, err := os.Open(path) if err != nil { @@ -210,6 +222,14 @@ func LoadGenesisBlock(path string) *types.Block { return genesis.ToBlock() } +func GenesisStartOption(genesis *core.Genesis) (hivesim.StartOption, error) { + out, err := json.Marshal(genesis) + if err != nil { + return nil, fmt.Errorf("failed to serialize genesis state: %v", err) + } + return hivesim.WithDynamicFile("/genesis.json", bytesSource(out)), nil +} + func CalculateTotalDifficulty(genesis core.Genesis, chain types.Blocks, lastBlock uint64) *big.Int { result := new(big.Int).Set(genesis.Difficulty) for _, b := range chain { @@ -222,8 +242,7 @@ func CalculateTotalDifficulty(genesis core.Genesis, chain types.Blocks, lastBloc } // TTD is the value specified in the test.Spec + Genesis.Difficulty -func CalculateRealTTD(genesisPath string, ttdValue int64) int64 { - g := LoadGenesis(genesisPath) +func CalculateRealTTD(g *core.Genesis, ttdValue int64) int64 { return g.Difficulty.Int64() + ttdValue } @@ -285,7 +304,7 @@ func WaitForTTDWithTimeout(ec client.EngineClient, ctx context.Context) error { return nil } case <-ctx.Done(): - return fmt.Errorf("Timeout reached") + return fmt.Errorf("timeout reached") } } } @@ -319,11 +338,24 @@ const ( DynamicFeeTxOnly ) -func MakeTransaction(nonce uint64, recipient *common.Address, gasLimit uint64, amount *big.Int, payload []byte, txType TestTransactionType) (*types.Transaction, error) { +type TransactionCreator interface { + MakeTransaction(nonce uint64) (*types.Transaction, error) +} + +type BaseTransactionCreator struct { + Recipient *common.Address + GasLimit uint64 + Amount *big.Int + Payload []byte + TxType TestTransactionType + PrivateKey *ecdsa.PrivateKey +} + +func (tc *BaseTransactionCreator) MakeTransaction(nonce uint64) (*types.Transaction, error) { var newTxData types.TxData var txTypeToUse int - switch txType { + switch tc.TxType { case UnspecifiedTransactionType: // Test case has no specific type of transaction to use. // Select the type of tx based on the nonce. @@ -344,70 +376,48 @@ func MakeTransaction(nonce uint64, recipient *common.Address, gasLimit uint64, a case types.LegacyTxType: newTxData = &types.LegacyTx{ Nonce: nonce, - To: recipient, - Value: amount, - Gas: gasLimit, + To: tc.Recipient, + Value: tc.Amount, + Gas: tc.GasLimit, GasPrice: globals.GasPrice, - Data: payload, + Data: tc.Payload, } case types.DynamicFeeTxType: gasFeeCap := new(big.Int).Set(globals.GasPrice) gasTipCap := new(big.Int).Set(globals.GasTipPrice) newTxData = &types.DynamicFeeTx{ Nonce: nonce, - Gas: gasLimit, + Gas: tc.GasLimit, GasTipCap: gasTipCap, GasFeeCap: gasFeeCap, - To: recipient, - Value: amount, - Data: payload, + To: tc.Recipient, + Value: tc.Amount, + Data: tc.Payload, } } tx := types.NewTx(newTxData) - signedTx, err := types.SignTx(tx, types.NewLondonSigner(globals.ChainID), globals.VaultKey) + key := tc.PrivateKey + if key == nil { + key = globals.VaultKey + } + signedTx, err := types.SignTx(tx, types.NewLondonSigner(globals.ChainID), key) if err != nil { return nil, err } return signedTx, nil } -// Determines if the error we got from sending the raw tx is because the client -// already knew the tx (might happen if we produced a re-org where the tx was -// unwind back into the txpool) -func SentTxAlreadyKnown(err error) bool { - return strings.Contains(err.Error(), "already known") -} - -func SendNextTransaction(testCtx context.Context, node client.EngineClient, recipient common.Address, amount *big.Int, payload []byte, txType TestTransactionType) (*types.Transaction, error) { - nonce, err := node.GetNextAccountNonce(testCtx, globals.VaultAccountAddress) - if err != nil { - return nil, err - } - tx, err := MakeTransaction(nonce, &recipient, 75000, amount, payload, txType) - for { - ctx, cancel := context.WithTimeout(testCtx, globals.RPCTimeout) - defer cancel() - err := node.SendTransaction(ctx, tx) - if err == nil { - return tx, nil - } else if SentTxAlreadyKnown(err) { - return tx, nil - } - select { - case <-time.After(time.Second): - case <-testCtx.Done(): - return nil, testCtx.Err() - } - } +// Create a contract filled with zeros without going over the specified GasLimit +type BigContractTransactionCreator struct { + BaseTransactionCreator } -// Method that attempts to create a contract filled with zeros without going over the specified gasLimit -func MakeBigContractTransaction(nonce uint64, gasLimit uint64, txType TestTransactionType) (*types.Transaction, error) { +func (tc *BigContractTransactionCreator) MakeTransaction(nonce uint64) (*types.Transaction, error) { // Total GAS: Gtransaction == 21000, Gcreate == 32000, Gcodedeposit == 200 contractLength := uint64(0) - if gasLimit > (21000 + 32000) { - contractLength = (gasLimit - 21000 - 32000) / 200 + if tc.GasLimit > (21000 + 32000) { + contractLength = (tc.GasLimit - 21000 - 32000) / 200 if contractLength >= 1 { // Reduce by 1 to guarantee using less gas than requested contractLength -= 1 @@ -416,22 +426,65 @@ func MakeBigContractTransaction(nonce uint64, gasLimit uint64, txType TestTransa buf := make([]byte, 8) binary.BigEndian.PutUint64(buf, contractLength) - initCode := []byte{ + tc.Payload = []byte{ 0x67, // PUSH8 } - initCode = append(initCode, buf...) // Size of the contract in byte length - initCode = append(initCode, 0x38) // CODESIZE == 0x00 - initCode = append(initCode, 0xF3) // RETURN(offset, length) + tc.Payload = append(tc.Payload, buf...) // Size of the contract in byte length + tc.Payload = append(tc.Payload, 0x38) // CODESIZE == 0x00 + tc.Payload = append(tc.Payload, 0xF3) // RETURN(offset, length) + if tc.Recipient != nil { + panic("invalid configuration for big contract tx creator") + } + return tc.BaseTransactionCreator.MakeTransaction(nonce) +} + +// Create a tx with the specified initcode length (all zeros) +type BigInitcodeTransactionCreator struct { + BaseTransactionCreator + InitcodeLength int + PadByte uint8 + Initcode []byte +} + +func (tc *BigInitcodeTransactionCreator) MakeTransaction(nonce uint64) (*types.Transaction, error) { + // This method caches the payload with the crafted initcode after first execution. + if tc.Payload == nil { + // Prepare initcode payload + if tc.Initcode != nil { + if len(tc.Initcode) > tc.InitcodeLength { + panic(fmt.Errorf("invalid initcode (too big)")) + } + tc.Payload = tc.Initcode + } else { + tc.Payload = []byte{} + } + + for { + if len(tc.Payload) == tc.InitcodeLength { + break + } + tc.Payload = append(tc.Payload, tc.PadByte) + } + } + if tc.Recipient != nil { + panic("invalid configuration for big contract tx creator") + } + return tc.BaseTransactionCreator.MakeTransaction(nonce) +} - return MakeTransaction(nonce, nil, gasLimit, common.Big0, initCode, txType) +// Determines if the error we got from sending the raw tx is because the client +// already knew the tx (might happen if we produced a re-org where the tx was +// unwind back into the txpool) +func SentTxAlreadyKnown(err error) bool { + return strings.Contains(err.Error(), "already known") } -func SendNextBigContractTransaction(testCtx context.Context, node client.EngineClient, gasLimit uint64, txType TestTransactionType) (*types.Transaction, error) { +func SendNextTransaction(testCtx context.Context, node client.EngineClient, txCreator TransactionCreator) (*types.Transaction, error) { nonce, err := node.GetNextAccountNonce(testCtx, globals.VaultAccountAddress) if err != nil { return nil, err } - tx, err := MakeBigContractTransaction(nonce, gasLimit, txType) + tx, err := txCreator.MakeTransaction(nonce) if err != nil { return nil, err } @@ -441,6 +494,8 @@ func SendNextBigContractTransaction(testCtx context.Context, node client.EngineC err := node.SendTransaction(ctx, tx) if err == nil { return tx, nil + } else if SentTxAlreadyKnown(err) { + return tx, nil } select { case <-time.After(time.Second): diff --git a/simulators/ethereum/engine/helper/payload.go b/simulators/ethereum/engine/helper/payload.go index 1538512d89..0a9a86b22f 100644 --- a/simulators/ethereum/engine/helper/payload.go +++ b/simulators/ethereum/engine/helper/payload.go @@ -10,6 +10,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" api "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" "github.com/ethereum/hive/simulators/ethereum/engine/globals" ) @@ -28,11 +29,12 @@ type CustomPayloadData struct { BaseFeePerGas *big.Int BlockHash *common.Hash Transactions *[][]byte + Withdrawals types.Withdrawals } // Construct a customized payload by taking an existing payload as base and mixing it CustomPayloadData // BlockHash is calculated automatically. -func CustomizePayload(basePayload *api.ExecutableDataV1, customData *CustomPayloadData) (*api.ExecutableDataV1, error) { +func CustomizePayload(basePayload *api.ExecutableData, customData *CustomPayloadData) (*api.ExecutableData, error) { txs := basePayload.Transactions if customData.Transactions != nil { txs = *customData.Transactions @@ -61,7 +63,6 @@ func CustomizePayload(basePayload *api.ExecutableDataV1, customData *CustomPaylo Nonce: types.BlockNonce{0}, // could be overwritten BaseFee: basePayload.BaseFeePerGas, } - // Overwrite custom information if customData.ParentHash != nil { customPayloadHeader.ParentHash = *customData.ParentHash @@ -99,9 +100,16 @@ func CustomizePayload(basePayload *api.ExecutableDataV1, customData *CustomPaylo if customData.BaseFeePerGas != nil { customPayloadHeader.BaseFee = customData.BaseFeePerGas } + if customData.Withdrawals != nil { + h := types.DeriveSha(customData.Withdrawals, trie.NewStackTrie(nil)) + customPayloadHeader.WithdrawalsHash = &h + } else if basePayload.Withdrawals != nil { + h := types.DeriveSha(types.Withdrawals(basePayload.Withdrawals), trie.NewStackTrie(nil)) + customPayloadHeader.WithdrawalsHash = &h + } // Return the new payload - return &api.ExecutableDataV1{ + result := &api.ExecutableData{ ParentHash: customPayloadHeader.ParentHash, FeeRecipient: customPayloadHeader.Coinbase, StateRoot: customPayloadHeader.Root, @@ -116,7 +124,27 @@ func CustomizePayload(basePayload *api.ExecutableDataV1, customData *CustomPaylo BaseFeePerGas: customPayloadHeader.BaseFee, BlockHash: customPayloadHeader.Hash(), Transactions: txs, - }, nil + } + if customData.Withdrawals != nil { + result.Withdrawals = customData.Withdrawals + } else if basePayload.Withdrawals != nil { + result.Withdrawals = basePayload.Withdrawals + } + return result, nil +} + +func CustomizePayloadTransactions(basePayload *api.ExecutableData, customTransactions types.Transactions) (*api.ExecutableData, error) { + byteTxs := make([][]byte, 0) + for _, tx := range customTransactions { + bytes, err := tx.MarshalBinary() + if err != nil { + return nil, err + } + byteTxs = append(byteTxs, bytes) + } + return CustomizePayload(basePayload, &CustomPayloadData{ + Transactions: &byteTxs, + }) } func (customData *CustomPayloadData) String() string { @@ -160,12 +188,15 @@ func (customData *CustomPayloadData) String() string { if customData.Transactions != nil { customFieldsList = append(customFieldsList, fmt.Sprintf("Transactions=%v", customData.Transactions)) } + if customData.Withdrawals != nil { + customFieldsList = append(customFieldsList, fmt.Sprintf("Withdrawals=%v", customData.Withdrawals)) + } return strings.Join(customFieldsList, ", ") } // This function generates an invalid payload by taking a base payload and modifying the specified field such that it ends up being invalid. // One small consideration is that the payload needs to contain transactions and specially transactions using the PREVRANDAO opcode for all the fields to be compatible with this function. -func GenerateInvalidPayload(basePayload *api.ExecutableDataV1, payloadField InvalidPayloadBlockField) (*api.ExecutableDataV1, error) { +func GenerateInvalidPayload(basePayload *api.ExecutableData, payloadField InvalidPayloadBlockField) (*api.ExecutableData, error) { var customPayloadMod *CustomPayloadData switch payloadField { diff --git a/simulators/ethereum/engine/helper/withdrawals.go b/simulators/ethereum/engine/helper/withdrawals.go new file mode 100644 index 0000000000..6397ee2c9d --- /dev/null +++ b/simulators/ethereum/engine/helper/withdrawals.go @@ -0,0 +1,19 @@ +package helper + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/trie" +) + +var ( + EmptyWithdrawalsRootHash = &types.EmptyRootHash +) + +func ComputeWithdrawalsRoot(ws types.Withdrawals) common.Hash { + // Using RLP root but might change to ssz + return types.DeriveSha( + ws, + trie.NewStackTrie(nil), + ) +} diff --git a/simulators/ethereum/engine/main.go b/simulators/ethereum/engine/main.go index 768b5fa558..6fbe0cda52 100644 --- a/simulators/ethereum/engine/main.go +++ b/simulators/ethereum/engine/main.go @@ -12,8 +12,8 @@ import ( suite_auth "github.com/ethereum/hive/simulators/ethereum/engine/suites/auth" suite_engine "github.com/ethereum/hive/simulators/ethereum/engine/suites/engine" - suite_sync "github.com/ethereum/hive/simulators/ethereum/engine/suites/sync" suite_transition "github.com/ethereum/hive/simulators/ethereum/engine/suites/transition" + suite_withdrawals "github.com/ethereum/hive/simulators/ethereum/engine/suites/withdrawals" ) func main() { @@ -40,68 +40,112 @@ func main() { Description: ` Test Engine API sync, pre/post merge.`[1:], } + withdrawals = hivesim.Suite{ + Name: "engine-withdrawals", + Description: ` + Test Engine API withdrawals, pre/post Shanghai.`[1:], + } ) simulator := hivesim.New() - addTestsToSuite(&engine, suite_engine.Tests, "full") - addTestsToSuite(&transition, suite_transition.Tests, "full") - addTestsToSuite(&auth, suite_auth.Tests, "full") - suite_sync.AddSyncTestsToSuite(simulator, &sync, suite_sync.Tests) + addTestsToSuite(simulator, &engine, specToInterface(suite_engine.Tests), "full") + addTestsToSuite(simulator, &transition, specToInterface(suite_transition.Tests), "full") + addTestsToSuite(simulator, &auth, specToInterface(suite_auth.Tests), "full") + //suite_sync.AddSyncTestsToSuite(simulator, &sync, suite_sync.Tests) + addTestsToSuite(simulator, &withdrawals, suite_withdrawals.Tests, "full") // Mark suites for execution hivesim.MustRunSuite(simulator, engine) hivesim.MustRunSuite(simulator, transition) hivesim.MustRunSuite(simulator, auth) hivesim.MustRunSuite(simulator, sync) + hivesim.MustRunSuite(simulator, withdrawals) +} + +func specToInterface(src []test.Spec) []test.SpecInterface { + res := make([]test.SpecInterface, len(src)) + for i := 0; i < len(src); i++ { + res[i] = src[i] + } + return res } // Add test cases to a given test suite -func addTestsToSuite(suite *hivesim.Suite, tests []test.Spec, nodeType string) { +func addTestsToSuite(sim *hivesim.Simulation, suite *hivesim.Suite, tests []test.SpecInterface, nodeType string) { for _, currentTest := range tests { currentTest := currentTest - genesisPath := "./init/genesis.json" - // If the test.Spec specified a custom genesis file, use that instead. - if currentTest.GenesisFile != "" { - genesisPath = "./init/" + currentTest.GenesisFile + + // Load the genesis file specified and dynamically bundle it. + genesis := currentTest.GetGenesis() + genesisStartOption, err := helper.GenesisStartOption(genesis) + if err != nil { + panic("unable to inject genesis") } - testFiles := hivesim.Params{"/genesis.json": genesisPath} + // Calculate and set the TTD for this test - ttd := helper.CalculateRealTTD(genesisPath, currentTest.TTD) + ttd := helper.CalculateRealTTD(genesis, currentTest.GetTTD()) + // Configure Forks newParams := globals.DefaultClientEnv.Set("HIVE_TERMINAL_TOTAL_DIFFICULTY", fmt.Sprintf("%d", ttd)) + if currentTest.GetForkConfig().ShanghaiTimestamp != nil { + newParams = newParams.Set("HIVE_SHANGHAI_TIMESTAMP", fmt.Sprintf("%d", currentTest.GetForkConfig().ShanghaiTimestamp)) + } + if nodeType != "" { newParams = newParams.Set("HIVE_NODETYPE", nodeType) } - if currentTest.ChainFile != "" { + + testFiles := hivesim.Params{} + if currentTest.GetChainFile() != "" { // We are using a Proof of Work chain file, remove all clique-related settings // TODO: Nethermind still requires HIVE_MINER for the Engine API // delete(newParams, "HIVE_MINER") delete(newParams, "HIVE_CLIQUE_PRIVATEKEY") delete(newParams, "HIVE_CLIQUE_PERIOD") // Add the new file to be loaded as chain.rlp - testFiles = testFiles.Set("/chain.rlp", "./chains/"+currentTest.ChainFile) + testFiles = testFiles.Set("/chain.rlp", "./chains/"+currentTest.GetChainFile()) } - if currentTest.DisableMining { + if currentTest.IsMiningDisabled() { delete(newParams, "HIVE_MINER") } - suite.Add(hivesim.ClientTestSpec{ - Name: currentTest.Name, - Description: currentTest.About, - Parameters: newParams, - Files: testFiles, - Run: func(t *hivesim.T, c *hivesim.Client) { - t.Logf("Start test (%s): %s", c.Type, currentTest.Name) - defer func() { - t.Logf("End test (%s): %s", c.Type, currentTest.Name) - }() - timeout := globals.DefaultTestCaseTimeout - // If a test.Spec specifies a timeout, use that instead - if currentTest.TimeoutSeconds != 0 { - timeout = time.Second * time.Duration(currentTest.TimeoutSeconds) - } - // Run the test case - test.Run(currentTest.Name, big.NewInt(ttd), currentTest.SlotsToSafe, currentTest.SlotsToFinalized, timeout, t, c, currentTest.Run, newParams, testFiles, currentTest.TestTransactionType, currentTest.SafeSlotsToImportOptimistically) - }, - }) + + if clientTypes, err := sim.ClientTypes(); err == nil { + for _, clientType := range clientTypes { + suite.Add(hivesim.TestSpec{ + Name: fmt.Sprintf("%s (%s)", currentTest.GetName(), clientType.Name), + Description: currentTest.GetAbout(), + Run: func(t *hivesim.T) { + // Start the client with given options + c := t.StartClient( + clientType.Name, + newParams, + genesisStartOption, + hivesim.WithStaticFiles(testFiles), + ) + t.Logf("Start test (%s): %s", c.Type, currentTest.GetName()) + defer func() { + t.Logf("End test (%s): %s", c.Type, currentTest.GetName()) + }() + timeout := globals.DefaultTestCaseTimeout + // If a test.Spec specifies a timeout, use that instead + if currentTest.GetTimeout() != 0 { + timeout = time.Second * time.Duration(currentTest.GetTimeout()) + } + // Run the test case + test.Run( + currentTest, + big.NewInt(ttd), + timeout, + t, + c, + genesis, + newParams, + testFiles, + ) + }, + }) + } + } + } } diff --git a/simulators/ethereum/engine/suites/engine/tests.go b/simulators/ethereum/engine/suites/engine/tests.go index 6b0da6ce44..c6716c428c 100644 --- a/simulators/ethereum/engine/suites/engine/tests.go +++ b/simulators/ethereum/engine/suites/engine/tests.go @@ -30,6 +30,7 @@ var ( Pending = big.NewInt(-2) Finalized = big.NewInt(-3) Safe = big.NewInt(-4) + ZeroAddr = common.Address{} ) var Tests = []test.Spec{ @@ -1196,7 +1197,7 @@ var Tests = []test.Spec{ // Invalid Terminal Block in ForkchoiceUpdated: Client must reject ForkchoiceUpdated directives if the referenced HeadBlockHash does not meet the TTD requirement. func invalidTerminalBlockForkchoiceUpdated(t *test.Env) { - gblock := helper.LoadGenesisBlock(t.ClientFiles["/genesis.json"]) + gblock := t.Genesis.ToBlock() forkchoiceState := api.ForkchoiceStateV1{ HeadBlockHash: gblock.Hash(), @@ -1218,7 +1219,7 @@ func invalidTerminalBlockForkchoiceUpdated(t *test.Env) { // Invalid GetPayload Under PoW: Client must reject GetPayload directives under PoW. func invalidGetPayloadUnderPoW(t *test.Env) { - gblock := helper.LoadGenesisBlock(t.ClientFiles["/genesis.json"]) + gblock := t.Genesis.ToBlock() // We start in PoW and try to get an invalid Payload, which should produce an error but nothing should be disrupted. r := t.TestEngine.TestEngineGetPayloadV1(&api.PayloadID{1, 2, 3, 4, 5, 6, 7, 8}) r.ExpectError() @@ -1229,10 +1230,10 @@ func invalidGetPayloadUnderPoW(t *test.Env) { // Invalid Terminal Block in NewPayload: Client must reject NewPayload directives if the referenced ParentHash does not meet the TTD requirement. func invalidTerminalBlockNewPayload(t *test.Env) { - gblock := helper.LoadGenesisBlock(t.ClientFiles["/genesis.json"]) + gblock := t.Genesis.ToBlock() // Create a dummy payload to send in the NewPayload call - payload := api.ExecutableDataV1{ + payload := api.ExecutableData{ ParentHash: gblock.Hash(), FeeRecipient: common.Address{}, StateRoot: gblock.Root(), @@ -1326,7 +1327,7 @@ func unknownFinalizedBlockHash(t *test.Env) { // Test again using PayloadAttributes, should also return INVALID and no PayloadID r = t.TestEngine.TestEngineForkchoiceUpdatedV1(&forkchoiceStateUnknownFinalizedHash, - &api.PayloadAttributesV1{ + &api.PayloadAttributes{ Timestamp: t.CLMock.LatestExecutedPayload.Timestamp + 1, Random: common.Hash{}, SuggestedFeeRecipient: common.Address{}, @@ -1367,7 +1368,7 @@ func unknownHeadBlockHash(t *test.Env) { // Test again using PayloadAttributes, should also return SYNCING and no PayloadID r = t.TestEngine.TestEngineForkchoiceUpdatedV1(&forkchoiceStateUnknownHeadHash, - &api.PayloadAttributesV1{ + &api.PayloadAttributes{ Timestamp: t.CLMock.LatestExecutedPayload.Timestamp + 1, Random: common.Hash{}, SuggestedFeeRecipient: common.Address{}, @@ -1383,8 +1384,8 @@ func inconsistentForkchoiceStateGen(inconsistency string) func(t *test.Env) { // Wait until TTD is reached by this client t.CLMock.WaitForTTD() - canonicalPayloads := make([]*api.ExecutableDataV1, 0) - alternativePayloads := make([]*api.ExecutableDataV1, 0) + canonicalPayloads := make([]*api.ExecutableData, 0) + alternativePayloads := make([]*api.ExecutableData, 0) // Produce blocks before starting the test t.CLMock.ProduceBlocks(3, clmock.BlockProcessCallbacks{ OnGetPayload: func() { @@ -1458,7 +1459,7 @@ func invalidPayloadAttributesGen(syncing bool) func(*test.Env) { SafeBlockHash: t.CLMock.LatestForkchoice.SafeBlockHash, FinalizedBlockHash: t.CLMock.LatestForkchoice.FinalizedBlockHash, } - attr := api.PayloadAttributesV1{ + attr := api.PayloadAttributes{ Timestamp: 0, Random: common.Hash{}, SuggestedFeeRecipient: common.Address{}, @@ -1496,7 +1497,7 @@ func preTTDFinalizedBlockHash(t *test.Env) { t.CLMock.ProduceBlocks(5, clmock.BlockProcessCallbacks{}) // Send the Genesis block as forkchoice - gblock := helper.LoadGenesisBlock(t.ClientFiles["/genesis.json"]) + gblock := t.Genesis.ToBlock() r := t.TestEngine.TestEngineForkchoiceUpdatedV1(&api.ForkchoiceStateV1{ HeadBlockHash: gblock.Hash(), @@ -1547,7 +1548,7 @@ func badHashOnNewPayloadGen(syncing bool, sidechain bool) func(*test.Env) { t.CLMock.ProduceBlocks(5, clmock.BlockProcessCallbacks{}) var ( - alteredPayload api.ExecutableDataV1 + alteredPayload api.ExecutableData invalidPayloadHash common.Hash ) @@ -1653,7 +1654,17 @@ func invalidTransitionPayload(t *test.Env) { // Produce two blocks before trying to re-org t.CLMock.ProduceBlocks(2, clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { - _, err := helper.SendNextTransaction(t.TestContext, t.CLMock.NextBlockProducer, globals.PrevRandaoContractAddr, big1, nil, t.TestTransactionType) + _, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: big1, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -1697,7 +1708,7 @@ func invalidPayloadTestCaseGen(payloadField helper.InvalidPayloadBlockField, syn if syncing { // To allow sending the primary engine client into SYNCING state, we need a secondary client to guide the payload creation - secondaryClient, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.ClientParams, t.ClientFiles) + secondaryClient, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles) if err != nil { t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) } @@ -1711,7 +1722,17 @@ func invalidPayloadTestCaseGen(payloadField helper.InvalidPayloadBlockField, syn if !emptyTxs { // Function to send at least one transaction each block produced // Send the transaction to the globals.PrevRandaoContractAddr - _, err := helper.SendNextTransaction(t.TestContext, t.CLMock.NextBlockProducer, globals.PrevRandaoContractAddr, big1, nil, t.TestTransactionType) + _, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: big1, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -1737,7 +1758,7 @@ func invalidPayloadTestCaseGen(payloadField helper.InvalidPayloadBlockField, syn } var ( - alteredPayload *api.ExecutableDataV1 + alteredPayload *api.ExecutableData err error ) @@ -1783,7 +1804,7 @@ func invalidPayloadTestCaseGen(payloadField helper.InvalidPayloadBlockField, syn SafeBlockHash: alteredPayload.BlockHash, FinalizedBlockHash: alteredPayload.BlockHash, } - payloadAttrbutes := api.PayloadAttributesV1{ + payloadAttrbutes := api.PayloadAttributes{ Timestamp: alteredPayload.Timestamp + 1, Random: common.Hash{}, SuggestedFeeRecipient: common.Address{}, @@ -1918,7 +1939,7 @@ func invalidMissingAncestorReOrgGen(invalid_index int, payloadField helper.Inval n := 10 // Slice to save the side B chain - altChainPayloads := make([]*api.ExecutableDataV1, 0) + altChainPayloads := make([]*api.ExecutableData, 0) // Append the common ancestor altChainPayloads = append(altChainPayloads, &cA) @@ -1934,7 +1955,17 @@ func invalidMissingAncestorReOrgGen(invalid_index int, payloadField helper.Inval // Empty Txs Payload with invalid stateRoot discovered an issue in geth sync, hence this is customizable. if !emptyTxs { // Send the transaction to the globals.PrevRandaoContractAddr - _, err := helper.SendNextTransaction(t.TestContext, t.CLMock.NextBlockProducer, globals.PrevRandaoContractAddr, big1, nil, t.TestTransactionType) + _, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: big1, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -1942,7 +1973,7 @@ func invalidMissingAncestorReOrgGen(invalid_index int, payloadField helper.Inval }, OnGetPayload: func() { var ( - sidePayload *api.ExecutableDataV1 + sidePayload *api.ExecutableData err error ) // Insert extraData to ensure we deviate from the main payload, which contains empty extradata @@ -2067,9 +2098,9 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { } if spec.ReOrgFromCanonical { // If we are doing a re-org from canonical, we can add both nodes as peers from the start - secondaryClient, err = starter.StartGethNode(t.T, t.TestContext, t.ClientParams, t.ClientFiles, t.Engine) + secondaryClient, err = starter.StartGethNode(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles, t.Engine) } else { - secondaryClient, err = starter.StartGethNode(t.T, t.TestContext, t.ClientParams, t.ClientFiles) + secondaryClient, err = starter.StartGethNode(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles) } if err != nil { t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) @@ -2136,7 +2167,17 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { // Empty Txs Payload with invalid stateRoot discovered an issue in geth sync, hence this is customizable. if !spec.EmptyTransactions { // Send the transaction to the globals.PrevRandaoContractAddr - _, err := helper.SendNextTransaction(t.TestContext, t.CLMock.NextBlockProducer, globals.PrevRandaoContractAddr, big1, nil, t.TestTransactionType) + _, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: big1, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -2144,7 +2185,7 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { }, OnGetPayload: func() { var ( - sidePayload *api.ExecutableDataV1 + sidePayload *api.ExecutableData err error ) // Insert extraData to ensure we deviate from the main payload, which contains empty extradata @@ -2190,7 +2231,7 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { } } } - // Invalidate fields not available in the ExecutableDataV1 + // Invalidate fields not available in the ExecutableData sideBlock, err = helper.GenerateInvalidPayloadBlock(sideBlock, uncle, spec.PayloadField) if err != nil { t.Fatalf("FAIL (%s): Unable to customize payload block: %v", t.TestName, err) @@ -2221,7 +2262,7 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { ctx, cancel := context.WithTimeout(t.TestContext, globals.RPCTimeout) defer cancel() - p := api.BlockToExecutableData(altChainPayloads[i]) + p := api.BlockToExecutableData(altChainPayloads[i], common.Big0).ExecutionPayload status, err := secondaryClient.NewPayloadV1(ctx, p) if err != nil { t.Fatalf("FAIL (%s): TEST ISSUE - Unable to send new payload: %v", t.TestName, err) @@ -2284,7 +2325,7 @@ func (spec InvalidMissingAncestorReOrgSpec) GenerateSync() func(*test.Env) { } // If we are syncing through p2p, we need to keep polling until the client syncs the missing payloads for { - r := t.TestEngine.TestEngineNewPayloadV1(api.BlockToExecutableData(altChainPayloads[n])) + r := t.TestEngine.TestEngineNewPayloadV1(api.BlockToExecutableData(altChainPayloads[n], common.Big0).ExecutionPayload) t.Logf("INFO (%s): Response from main client: %v", t.TestName, r.Status) s := t.TestEngine.TestEngineForkchoiceUpdatedV1(&api.ForkchoiceStateV1{ HeadBlockHash: altChainPayloads[n].Hash(), @@ -2376,7 +2417,16 @@ func blockStatusExecPayloadGen(transitionBlock bool) func(t *test.Env) { t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { var err error - tx, err = helper.SendNextTransaction(t.TestContext, t.Engine, (common.Address{}), big1, nil, t.TestTransactionType) + tx, err = helper.SendNextTransaction( + t.TestContext, + t.Engine, &helper.BaseTransactionCreator{ + Recipient: &ZeroAddr, + Amount: big1, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", err) } @@ -2415,7 +2465,17 @@ func blockStatusHeadBlockGen(transitionBlock bool) func(t *test.Env) { t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { var err error - tx, err = helper.SendNextTransaction(t.TestContext, t.Engine, (common.Address{}), big1, nil, t.TestTransactionType) + tx, err = helper.SendNextTransaction( + t.TestContext, + t.Engine, + &helper.BaseTransactionCreator{ + Recipient: &ZeroAddr, + Amount: big1, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -2479,7 +2539,7 @@ func blockStatusFinalizedBlock(t *test.Env) { func safeFinalizedCanonicalChain(t *test.Env) { // Wait until this client catches up with latest PoS Block t.CLMock.WaitForTTD() - gblock := helper.LoadGenesisBlock(t.ClientFiles["/genesis.json"]) + gblock := t.Genesis.ToBlock() // At the merge we need to have head equal to the last PoW block, and safe/finalized must return error h := t.TestEngine.TestHeaderByNumber(Head) @@ -2544,7 +2604,7 @@ func blockStatusReorg(t *test.Env) { HeadBlockHash: customizedPayload.BlockHash, SafeBlockHash: t.CLMock.LatestForkchoice.SafeBlockHash, FinalizedBlockHash: t.CLMock.LatestForkchoice.FinalizedBlockHash, - }, nil) + }, nil, 1) // Verify the client is serving the latest HeadBlock r := t.TestEngine.TestHeaderByNumber(Head) @@ -2604,7 +2664,7 @@ func reorgPrevValidatedPayloadOnSideChain(t *test.Env) { t.CLMock.ProduceBlocks(5, clmock.BlockProcessCallbacks{}) var ( - sidechainPayloads = make([]*api.ExecutableDataV1, 0) + sidechainPayloads = make([]*api.ExecutableData, 0) sidechainPayloadCount = 5 ) @@ -2641,7 +2701,7 @@ func reorgPrevValidatedPayloadOnSideChain(t *test.Env) { HeadBlockHash: sidechainPayloads[len(sidechainPayloads)-2].BlockHash, SafeBlockHash: t.CLMock.LatestForkchoice.SafeBlockHash, FinalizedBlockHash: t.CLMock.LatestForkchoice.FinalizedBlockHash, - }, &api.PayloadAttributesV1{ + }, &api.PayloadAttributes{ Timestamp: t.CLMock.LatestHeader.Time, Random: prevRandao, SuggestedFeeRecipient: common.Address{}, @@ -2668,7 +2728,7 @@ func safeReorgToSideChain(t *test.Env) { t.CLMock.WaitForTTD() // Produce an alternative chain - sidechainPayloads := make([]*api.ExecutableDataV1, 0) + sidechainPayloads := make([]*api.ExecutableData, 0) // Produce three payloads `P1`, `P2`, `P3`, along with the side chain payloads `P2'`, `P3'` // First payload is finalized so no alternative payload @@ -2735,7 +2795,7 @@ func reorgBackFromSyncing(t *test.Env) { t.CLMock.WaitForTTD() // Produce an alternative chain - sidechainPayloads := make([]*api.ExecutableDataV1, 0) + sidechainPayloads := make([]*api.ExecutableData, 0) t.CLMock.ProduceBlocks(10, clmock.BlockProcessCallbacks{ OnGetPayload: func() { // Generate an alternative payload by simply adding extraData to the block @@ -2795,7 +2855,7 @@ func transactionReorg(t *test.Env) { for i := 0; i < txCount; i++ { var ( - noTxnPayload api.ExecutableDataV1 + noTxnPayload api.ExecutableData tx *types.Transaction ) // Generate two payloads, one with the transaction and the other one without it @@ -2803,7 +2863,7 @@ func transactionReorg(t *test.Env) { OnPayloadProducerSelected: func() { // At this point we have not broadcast the transaction, // therefore any payload we get should not contain any - t.CLMock.GetNextPayloadID() + t.CLMock.RequestNextPayload() t.CLMock.GetNextPayload() noTxnPayload = t.CLMock.LatestPayloadBuilt if len(noTxnPayload.Transactions) != 0 { @@ -2815,7 +2875,17 @@ func transactionReorg(t *test.Env) { data := common.LeftPadBytes([]byte{byte(i)}, 32) t.Logf("transactionReorg, i=%v, data=%v\n", i, data) var err error - tx, err = helper.SendNextTransaction(t.TestContext, t.Engine, sstoreContractAddr, big0, data, t.TestTransactionType) + tx, err = helper.SendNextTransaction( + t.TestContext, + t.Engine, + &helper.BaseTransactionCreator{ + Recipient: &sstoreContractAddr, + Amount: big0, + Payload: data, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -2873,7 +2943,7 @@ func transactionReorg(t *test.Env) { } // Re-org back - t.CLMock.BroadcastForkchoiceUpdated(&t.CLMock.LatestForkchoice, nil) + t.CLMock.BroadcastForkchoiceUpdated(&t.CLMock.LatestForkchoice, nil, 1) }, }) @@ -2898,8 +2968,8 @@ func transactionReorgBlockhash(newNPOnRevert bool) func(t *test.Env) { for i := 0; i < txCount; i++ { var ( - mainPayload *api.ExecutableDataV1 - sidePayload *api.ExecutableDataV1 + mainPayload *api.ExecutableData + sidePayload *api.ExecutableData tx *types.Transaction ) @@ -2907,14 +2977,24 @@ func transactionReorgBlockhash(newNPOnRevert bool) func(t *test.Env) { OnPayloadProducerSelected: func() { // At this point we have not broadcast the transaction, // therefore any payload we get should not contain any - t.CLMock.GetNextPayloadID() + t.CLMock.RequestNextPayload() t.CLMock.GetNextPayload() // Create the transaction data := common.LeftPadBytes([]byte{byte(i)}, 32) t.Logf("transactionReorg, i=%v, data=%v\n", i, data) var err error - tx, err = helper.SendNextTransaction(t.TestContext, t.Engine, sstoreContractAddr, big0, data, t.TestTransactionType) + tx, err = helper.SendNextTransaction( + t.TestContext, + t.Engine, + &helper.BaseTransactionCreator{ + Recipient: &sstoreContractAddr, + Amount: big0, + Payload: data, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -2986,7 +3066,7 @@ func transactionReorgBlockhash(newNPOnRevert bool) func(t *test.Env) { HeadBlockHash: mainPayload.BlockHash, SafeBlockHash: t.CLMock.LatestForkchoice.SafeBlockHash, FinalizedBlockHash: t.CLMock.LatestForkchoice.FinalizedBlockHash, - }, nil) + }, nil, 1) // Not it should be back with main payload txt = t.TestEngine.TestTransactionReceipt(tx.Hash()) @@ -3010,7 +3090,17 @@ func sidechainReorg(t *test.Env) { // Produce two payloads, send fcU with first payload, check transaction outcome, then reorg, check transaction outcome again // This single transaction will change its outcome based on the payload - tx, err := helper.SendNextTransaction(t.TestContext, t.Engine, globals.PrevRandaoContractAddr, big0, nil, t.TestTransactionType) + tx, err := helper.SendNextTransaction( + t.TestContext, + t.Engine, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: big0, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -3024,7 +3114,7 @@ func sidechainReorg(t *test.Env) { rand.Read(alternativePrevRandao[:]) r := t.TestEngine.TestEngineForkchoiceUpdatedV1(&t.CLMock.LatestForkchoice, - &api.PayloadAttributesV1{ + &api.PayloadAttributes{ Timestamp: t.CLMock.LatestHeader.Time + 1, Random: alternativePrevRandao, SuggestedFeeRecipient: t.CLMock.NextFeeRecipient, @@ -3148,7 +3238,17 @@ func inOrderPayloads(t *test.Env) { // We send the transactions after we got the Payload ID, before the CLMocker gets the prepared Payload OnPayloadProducerSelected: func() { for i := 0; i < txPerPayload; i++ { - _, err := helper.SendNextTransaction(t.TestContext, t.CLMock.NextBlockProducer, recipient, amountPerTx, nil, t.TestTransactionType) + _, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &recipient, + Amount: amountPerTx, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -3168,7 +3268,7 @@ func inOrderPayloads(t *test.Env) { r.ExpectBalanceEqual(expectedBalance) // Start a second client to send newPayload consecutively without fcU - secondaryClient, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.ClientParams, t.ClientFiles) + secondaryClient, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles) if err != nil { t.Fatalf("FAIL (%s): Unable to start secondary client: %v", t.TestName, err) } @@ -3215,12 +3315,12 @@ func inOrderPayloads(t *test.Env) { func validPayloadFcUSyncingClient(t *test.Env) { var ( secondaryClient client.EngineClient - previousPayload api.ExecutableDataV1 + previousPayload api.ExecutableData ) { // To allow sending the primary engine client into SYNCING state, we need a secondary client to guide the payload creation var err error - secondaryClient, err = hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.ClientParams, t.ClientFiles) + secondaryClient, err = hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles) if err != nil { t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) @@ -3257,7 +3357,7 @@ func validPayloadFcUSyncingClient(t *test.Env) { HeadBlockHash: t.CLMock.LatestPayloadBuilt.BlockHash, SafeBlockHash: t.CLMock.LatestPayloadBuilt.BlockHash, FinalizedBlockHash: t.CLMock.LatestPayloadBuilt.BlockHash, - }, &api.PayloadAttributesV1{ + }, &api.PayloadAttributes{ Timestamp: t.CLMock.LatestPayloadBuilt.Timestamp + 1, Random: common.Hash{}, SuggestedFeeRecipient: common.Address{}, @@ -3304,7 +3404,7 @@ func missingFcu(t *test.Env) { var secondaryEngineTest *test.TestEngineClient { - secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.ClientParams, t.ClientFiles) + secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles) if err != nil { t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) @@ -3345,7 +3445,7 @@ func missingFcu(t *test.Env) { // P <- INV_P, newPayload(INV_P), fcU(head: P, payloadAttributes: attrs) + getPayload(…) func payloadBuildAfterNewInvalidPayload(t *test.Env) { // Add a second client to build the invalid payload - secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.ClientParams, t.ClientFiles) + secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles) if err != nil { t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) @@ -3368,11 +3468,11 @@ func payloadBuildAfterNewInvalidPayload(t *test.Env) { if t.CLMock.NextBlockProducer == invalidPayloadProducer.Engine { invalidPayloadProducer = secondaryEngineTest } - var inv_p *api.ExecutableDataV1 + var inv_p *api.ExecutableData { // Get a payload from the invalid payload producer and invalidate it - r := invalidPayloadProducer.TestEngineForkchoiceUpdatedV1(&t.CLMock.LatestForkchoice, &api.PayloadAttributesV1{ + r := invalidPayloadProducer.TestEngineForkchoiceUpdatedV1(&t.CLMock.LatestForkchoice, &api.PayloadAttributes{ Timestamp: t.CLMock.LatestHeader.Time + 1, Random: common.Hash{}, SuggestedFeeRecipient: common.Address{}, @@ -3471,7 +3571,17 @@ func suggestedFeeRecipient(t *test.Env) { // Send multiple transactions for i := 0; i < txCount; i++ { - _, err := helper.SendNextTransaction(t.TestContext, t.Engine, globals.VaultAccountAddress, big0, nil, t.TestTransactionType) + _, err := helper.SendNextTransaction( + t.TestContext, + t.Engine, + &helper.BaseTransactionCreator{ + Recipient: &globals.VaultAccountAddress, + Amount: big0, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -3531,7 +3641,17 @@ func prevRandaoOpcodeTx(t *test.Env) { // Try to send many transactions before PoW transition to guarantee at least one enters in the block go func(t *test.Env) { for { - _, err := helper.SendNextTransaction(t.TestContext, t.Engine, globals.PrevRandaoContractAddr, big0, nil, t.TestTransactionType) + _, err := helper.SendNextTransaction( + t.TestContext, + t.Engine, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: big0, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } @@ -3574,7 +3694,17 @@ func prevRandaoOpcodeTx(t *test.Env) { ) t.CLMock.ProduceBlocks(txCount, clmock.BlockProcessCallbacks{ OnPayloadProducerSelected: func() { - tx, err := helper.SendNextTransaction(t.TestContext, t.Engine, globals.PrevRandaoContractAddr, big0, nil, t.TestTransactionType) + tx, err := helper.SendNextTransaction( + t.TestContext, + t.Engine, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: big0, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Error trying to send transaction: %v", t.TestName, err) } diff --git a/simulators/ethereum/engine/suites/sync/tests.go b/simulators/ethereum/engine/suites/sync/tests.go index f15e849395..95414240ef 100644 --- a/simulators/ethereum/engine/suites/sync/tests.go +++ b/simulators/ethereum/engine/suites/sync/tests.go @@ -52,7 +52,8 @@ func AddSyncTestsToSuite(sim *hivesim.Simulation, suite *hivesim.Suite, tests [] } testFiles := hivesim.Params{"/genesis.json": genesisPath} // Calculate and set the TTD for this test - ttd := helper.CalculateRealTTD(genesisPath, currentTest.TTD) + genesis := helper.LoadGenesis(genesisPath) + ttd := helper.CalculateRealTTD(&genesis, currentTest.TTD) newParams := globals.DefaultClientEnv.Set("HIVE_TERMINAL_TOTAL_DIFFICULTY", fmt.Sprintf("%d", ttd)) if currentTest.ChainFile != "" { // We are using a Proof of Work chain file, remove all clique-related settings @@ -98,7 +99,7 @@ func AddSyncTestsToSuite(sim *hivesim.Simulation, suite *hivesim.Suite, tests [] } // Run the test case - test.Run(currentTest.Name, big.NewInt(ttd), currentTest.SlotsToSafe, currentTest.SlotsToFinalized, timeout, t, c, currentTest.Run, syncClientParams, testFiles.Copy(), currentTest.TestTransactionType, currentTest.SafeSlotsToImportOptimistically) + test.Run(currentTest, big.NewInt(ttd), timeout, t, c, &genesis, syncClientParams, testFiles.Copy()) }, }) } @@ -126,7 +127,7 @@ func postMergeSync(t *test.Env) { // Reset block production delay t.CLMock.PayloadProductionClientDelay = time.Second - secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.ClientParams.Set("HIVE_MINER", ""), t.ClientFiles, t.Engine) + secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams.Set("HIVE_MINER", ""), t.ClientFiles, t.Engine) if err != nil { t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) } @@ -182,7 +183,7 @@ func incrementalPostMergeSync(t *test.Env) { // Reset block production delay t.CLMock.PayloadProductionClientDelay = time.Second - secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.ClientParams.Set("HIVE_MINER", ""), t.ClientFiles, t.Engine) + secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams.Set("HIVE_MINER", ""), t.ClientFiles, t.Engine) if err != nil { t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) } diff --git a/simulators/ethereum/engine/suites/transition/tests.go b/simulators/ethereum/engine/suites/transition/tests.go index dfe231c779..2655d1c507 100644 --- a/simulators/ethereum/engine/suites/transition/tests.go +++ b/simulators/ethereum/engine/suites/transition/tests.go @@ -946,7 +946,7 @@ func GenerateMergeTestSpec(mergeTestSpec MergeTestSpec) test.Spec { for i, secondaryClientSpec := range mergeTestSpec.SecondaryClientSpecs { // Start the secondary client with the alternative chain - secondaryClient, err := secondaryClientSpec.ClientStarter.StartClient(t.T, t.CLMock.TestContext, t.ClientParams, t.ClientFiles, t.Engine) + secondaryClient, err := secondaryClientSpec.ClientStarter.StartClient(t.T, t.CLMock.TestContext, t.Genesis, t.ClientParams, t.ClientFiles, t.Engine) t.Logf("INFO (%s): Started secondary client: %v", t.TestName, secondaryClient.ID()) if err != nil { t.Fatalf("FAIL (%s): Unable to start secondary client: %v", t.TestName, err) @@ -1025,7 +1025,17 @@ func GenerateMergeTestSpec(mergeTestSpec MergeTestSpec) test.Spec { // Get the address nonce: // This is because we could have included transactions in the PoW chain of the block // producer, or re-orged. - tx, err := helper.SendNextTransaction(t.TestContext, t.CLMock.NextBlockProducer, globals.PrevRandaoContractAddr, common.Big0, nil, t.TestTransactionType) + tx, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + Amount: common.Big0, + Payload: nil, + TxType: t.TestTransactionType, + GasLimit: 75000, + }, + ) if err != nil { t.Fatalf("FAIL (%s): Unable create next transaction: %v", t.TestName, err) } diff --git a/simulators/ethereum/engine/suites/withdrawals/README.md b/simulators/ethereum/engine/suites/withdrawals/README.md new file mode 100644 index 0000000000..cd25ceca14 --- /dev/null +++ b/simulators/ethereum/engine/suites/withdrawals/README.md @@ -0,0 +1,154 @@ +# Shanghai Withdrawals Testing + +This test suite verifies behavior of the Engine API on each client after the Shanghai upgrade, according to the specification defined at: +- https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md + +## 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. + +## Withdrawals +- Withdraw to a single account: Make multiple withdrawals to a single account. +- Withdraw to two accounts: Make multiple withdrawals to two different accounts, repeated in round-robin. Reasoning: There might be a difference in implementation when an account appears multiple times in the withdrawals list but the list is not in ordered sequence. +- Withdraw to many accounts: Make multiple withdrawals to 1024 different accounts. Execute many blocks this way. +- Withdraw zero amount: Make multiple withdrawals where the amount withdrawn is 0. +- Empty Withdrawals: Produce withdrawals block with zero withdrawals. + +## Sync to Shanghai +- Sync after 2 blocks - Shanghai on Block 1 - Single Withdrawal Account - No Transactions: + - Spawn a first client + - Go through Shanghai fork on Block 1 + - Withdraw to a single account 16 times each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance +- Sync after 2 blocks - Shanghai on Block 1 - Single Withdrawal Account: + - Spawn a first client + - Go through Shanghai fork on Block 1 + - Withdraw to a single account 16 times each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance +- Sync after 2 blocks - Shanghai on Genesis - Single Withdrawal Account: + - Spawn a first client with Shanghai on Genesis + - Withdraw to a single account 16 times each block for 8 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance +- Sync after 2 blocks - Shanghai on Block 2 - Multiple Withdrawal Accounts - No Transactions: + - Spawn a first client + - Go through Shanghai fork on Block 2 + - Withdraw to 16 accounts each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance +- Sync after 2 blocks - Shanghai on Block 2 - Multiple Withdrawal Accounts: + - Spawn a first client + - Go through Shanghai fork on Block 2 + - Withdraw to 16 accounts each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance +- Sync after 128 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts: + - Spawn a first client + - Go through Shanghai fork on Block 2 + - Withdraw to many accounts 16 times each block for 128 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance +- [NOT IMPLEMENTED] Error Syncing Null Withdrawals Block after Shanghai: + - Spawn a first client with configuration `shanghaiTime==2` + - Send Block 1 with `timestamp==1` + - Go through Shanghai fork on Block 2 (`timestamp==2`) + - Withdraw to a single account 16 times each block for 2 blocks + - Spawn a secondary client with configuration `shanghaiTime==1` and send FCUV2(Block 2) + - Wait for sync and verify that `engine_newPayload` and `engine_forkchoiceUpdated` return `INVALID` for Block 2. + +## Shanghai Fork Re-Org Tests + +- Shanghai Fork on Block 1 - 16 Post-Fork Blocks - 1 Block Re-Org via NewPayload: + - Spawn a two clients `A` and `B` + - Go through Shanghai fork on Block 1 + - Produce 15 blocks on both clients `A` and `B`, withdrawing to 16 accounts each block + - Produce a canonical chain `A` block 16 on client `A`, withdrawing to the same 16 accounts + - Produce a side chain `B` block 16 on client `B`, withdrawing to different 16 accounts + - Send `NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify the re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Block 1 - 8 Block Re-Org NewPayload + - Spawn a two clients `A` and `B` + - Go through Shanghai fork on Block 1 + - Produce 8 blocks on both clients `A` and `B`, withdrawing to 16 accounts each block + - Produce canonical chain `A` blocks 9-16 on client `A`, withdrawing to the same 16 accounts + - Produce side chain `B` blocks 9-16 on client `B`, withdrawing to different 16 accounts + - Send `NewPayload(B[9])+FcU(B[9])..NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify the re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Block 1 - 8 Block Re-Org Sync + - Spawn a two clients `A` and `B` + - Go through Shanghai fork on Block 1 + - Produce 8 blocks on both clients `A` and `B`, withdrawing to 16 accounts each block + - Produce canonical chain `A` blocks 9-16 on client `A`, withdrawing to the same 16 accounts + - Produce side chain `B` blocks 9-16 on client `B`, withdrawing to different 16 accounts + - Send `NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify client `A` syncs side chain blocks from client `B` re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Block 8 - 10 Block Re-Org NewPayload + - Spawn a two clients `A` and `B` + - Produce 6 blocks on both clients `A` and `B` + - Produce canonical chain `A` blocks 7-16 on client `A` + - Produce side chain `B` blocks 7-16 on client `B` + - Shanghai fork occurs on blocks `A[8]` and `B[8]` + - Send `NewPayload(B[7])+FcU(B[7])..NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify the re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Block 8 - 10 Block Re-Org Sync + - Spawn a two clients `A` and `B` + - Produce 6 blocks on both clients `A` and `B` + - Produce canonical chain `A` blocks 7-16 on client `A` + - Produce side chain `B` blocks 7-16 on client `B` + - Shanghai fork occurs on blocks `A[8]` and `B[8]` + - Send `NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify client `A` syncs side chain blocks from client `B` re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org NewPayload + - Spawn a two clients `A` and `B` + - Produce 6 blocks on both clients `A` and `B` + - Produce canonical chain `A` blocks 7-16 on client `A` with timestamp increments of 1. + - Produce side chain `B` blocks 7-16 on client `B` with timestamp increments of 2 + - Shanghai fork occurs on blocks `A[8]` and `B[7]` + - Send `NewPayload(B[7])+FcU(B[7])..NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify the re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org Sync + - Spawn a two clients `A` and `B` + - Produce 6 blocks on both clients `A` and `B` + - Produce canonical chain `A` blocks 7-16 on client `A` with timestamp increments of 1. + - Produce side chain `B` blocks 7-16 on client `B` with timestamp increments of 2 + - Shanghai fork occurs on blocks `A[8]` and `B[7]` + - Send `NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify client `A` syncs side chain blocks from client `B` re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Canonical Block 8 / Side Block 9 - 10 Block Re-Org NewPayload + - Spawn a two clients `A` and `B` + - Produce 6 blocks on both clients `A` and `B`, with timestamp increments of 2 + - Produce canonical chain `A` blocks 7-16 on client `A` with timestamp increments of 2. + - Produce side chain `B` blocks 7-16 on client `B` with timestamp increments of 1 + - Shanghai fork occurs on blocks `A[8]` and `B[9]` + - Send `NewPayload(B[7])+FcU(B[7])..NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify the re-org was correctly applied along with the withdrawal balances. + +- Shanghai Fork on Canonical Block 8 / Side Block 9 - 10 Block Re-Org Sync + - Spawn a two clients `A` and `B` + - Produce 6 blocks on both clients `A` and `B`, with timestamp increments of 2 + - Produce canonical chain `A` blocks 7-16 on client `A` with timestamp increments of 2. + - Produce side chain `B` blocks 7-16 on client `B` with timestamp increments of 1 + - Shanghai fork occurs on blocks `A[8]` and `B[9]` + - Send `NewPayload(B[16])+FcU(B[16])` to client `A` + - Verify client `A` syncs side chain blocks from client `B` re-org was correctly applied along with the withdrawal balances. + +## Max Initcode Tests +- Transactions exceeding max initcode should be immediately rejected: + - Send two transactions with valid nonces each + - `TxA`, a smart contract creating transaction with an initcode length equal to MAX_INITCODE_SIZE ([EIP-3860](https://eips.ethereum.org/EIPS/eip-3860)) + - `TxB`, a smart contract creating transaction with an initcode length equal to MAX_INITCODE_SIZE+1. + - 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 diff --git a/simulators/ethereum/engine/suites/withdrawals/spec_unit_test.go b/simulators/ethereum/engine/suites/withdrawals/spec_unit_test.go new file mode 100644 index 0000000000..73e3afb11b --- /dev/null +++ b/simulators/ethereum/engine/suites/withdrawals/spec_unit_test.go @@ -0,0 +1,145 @@ +package suite_withdrawals + +import ( + "testing" + + "github.com/ethereum/hive/simulators/ethereum/engine/globals" +) + +type BaseSpecExpected struct { + Spec *WithdrawalsBaseSpec + ExpectedBlockTimeIncrements uint64 + ExpectedWithdrawalsTimestamp uint64 + ExpectedPreWithdrawalsBlockCount uint64 + ExpectedTotalPayloadCount uint64 +} + +var baseSpecTestCases = []BaseSpecExpected{ + { + Spec: &WithdrawalsBaseSpec{}, + ExpectedBlockTimeIncrements: 1, + ExpectedWithdrawalsTimestamp: uint64(globals.GenesisTimestamp), + ExpectedPreWithdrawalsBlockCount: 0, + ExpectedTotalPayloadCount: 0, + }, + { + Spec: &WithdrawalsBaseSpec{ + WithdrawalsBlockCount: 1, + }, + ExpectedBlockTimeIncrements: 1, + ExpectedWithdrawalsTimestamp: uint64(globals.GenesisTimestamp), + ExpectedPreWithdrawalsBlockCount: 0, + ExpectedTotalPayloadCount: 1, + }, +} + +func TestBaseSpecFunctions(t *testing.T) { + for i, tc := range baseSpecTestCases { + spec := tc.Spec + if spec.GetBlockTimeIncrements() != tc.ExpectedBlockTimeIncrements { + t.Fatalf("tc %d: unexpected block timestamp increments, expected=%d, got=%d", i, tc.ExpectedBlockTimeIncrements, spec.GetBlockTimeIncrements()) + } + if spec.GetWithdrawalsForkTime() != tc.ExpectedWithdrawalsTimestamp { + t.Fatalf("tc %d: unexpected withdrawals timestamp, expected=%d, got=%d", i, tc.ExpectedWithdrawalsTimestamp, spec.GetWithdrawalsForkTime()) + } + if spec.GetPreWithdrawalsBlockCount() != tc.ExpectedPreWithdrawalsBlockCount { + t.Fatalf("tc %d: unexpected pre-withdrawals block count, expected=%d, got=%d", i, tc.ExpectedPreWithdrawalsBlockCount, spec.GetPreWithdrawalsBlockCount()) + } + if spec.GetTotalPayloadCount() != tc.ExpectedTotalPayloadCount { + t.Fatalf("tc %d: unexpected total payload count, expected=%d, got=%d", i, tc.ExpectedTotalPayloadCount, spec.GetTotalPayloadCount()) + } + } +} + +type ReOrgSpecExpected struct { + Spec WithdrawalsReorgSpec + ExpectedSidechainSplitHeight uint64 + ExpectedSidechainBlockTimeIncrements uint64 + ExpectedSidechainWithdrawalsForkHeight uint64 +} + +var reorgSpecTestCases = []ReOrgSpecExpected{ + { + Spec: WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 16, + }, + ReOrgBlockCount: 1, + }, + ExpectedSidechainSplitHeight: 16, + ExpectedSidechainBlockTimeIncrements: 1, + ExpectedSidechainWithdrawalsForkHeight: 1, + }, + { + Spec: WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + TimeIncrements: 12, + WithdrawalsForkHeight: 4, + WithdrawalsBlockCount: 1, + }, + ReOrgBlockCount: 1, + SidechainTimeIncrements: 1, + }, + ExpectedSidechainSplitHeight: 4, + ExpectedSidechainBlockTimeIncrements: 1, + ExpectedSidechainWithdrawalsForkHeight: 15, + }, + { + Spec: WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + TimeIncrements: 1, + WithdrawalsForkHeight: 4, + WithdrawalsBlockCount: 4, + }, + ReOrgBlockCount: 6, + SidechainTimeIncrements: 4, + }, + ExpectedSidechainSplitHeight: 2, + ExpectedSidechainBlockTimeIncrements: 4, + ExpectedSidechainWithdrawalsForkHeight: 2, + }, + { + Spec: WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + TimeIncrements: 1, + WithdrawalsForkHeight: 8, + WithdrawalsBlockCount: 8, + }, + ReOrgBlockCount: 10, + SidechainTimeIncrements: 2, + }, + ExpectedSidechainSplitHeight: 6, + ExpectedSidechainBlockTimeIncrements: 2, + ExpectedSidechainWithdrawalsForkHeight: 7, + }, + { + Spec: WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + TimeIncrements: 2, + WithdrawalsForkHeight: 8, + WithdrawalsBlockCount: 8, + }, + ReOrgBlockCount: 10, + SidechainTimeIncrements: 1, + }, + ExpectedSidechainSplitHeight: 6, + ExpectedSidechainBlockTimeIncrements: 1, + ExpectedSidechainWithdrawalsForkHeight: 11, + }, +} + +func TestReorgSpecFunctions(t *testing.T) { + for i, tc := range reorgSpecTestCases { + spec := tc.Spec + if spec.GetSidechainSplitHeight() != tc.ExpectedSidechainSplitHeight { + t.Fatalf("tc %d: unexpected sidechain split height, expected=%d, got=%d", i, tc.ExpectedSidechainSplitHeight, spec.GetSidechainSplitHeight()) + } + if spec.GetSidechainBlockTimeIncrements() != tc.ExpectedSidechainBlockTimeIncrements { + t.Fatalf("tc %d: unexpected sidechain block timestamp increments, expected=%d, got=%d", i, tc.ExpectedSidechainBlockTimeIncrements, spec.GetSidechainBlockTimeIncrements()) + } + if spec.GetSidechainWithdrawalsForkHeight() != tc.ExpectedSidechainWithdrawalsForkHeight { + t.Fatalf("tc %d: unexpected sidechain withdrawals fork height, expected=%d, got=%d", i, tc.ExpectedSidechainWithdrawalsForkHeight, spec.GetSidechainWithdrawalsForkHeight()) + } + } +} diff --git a/simulators/ethereum/engine/suites/withdrawals/tests.go b/simulators/ethereum/engine/suites/withdrawals/tests.go new file mode 100644 index 0000000000..cbf81005e3 --- /dev/null +++ b/simulators/ethereum/engine/suites/withdrawals/tests.go @@ -0,0 +1,1385 @@ +// # Test suite for withdrawals tests +package suite_withdrawals + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/beacon" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/hive/simulators/ethereum/engine/client/hive_rpc" + "github.com/ethereum/hive/simulators/ethereum/engine/clmock" + "github.com/ethereum/hive/simulators/ethereum/engine/globals" + "github.com/ethereum/hive/simulators/ethereum/engine/helper" + "github.com/ethereum/hive/simulators/ethereum/engine/test" +) + +var ( + Head *big.Int // Nil + Pending = big.NewInt(-2) + Finalized = big.NewInt(-3) + Safe = big.NewInt(-4) + InvalidParamsError = -32602 + MAX_INITCODE_SIZE = 49152 +) + +// Execution specification reference: +// https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md + +// List of all withdrawals tests +var Tests = []test.SpecInterface{ + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork On Genesis", + About: ` + Tests the withdrawals fork happening since genesis (e.g. on a + testnet). + `, + }, + WithdrawalsForkHeight: 0, + WithdrawalsBlockCount: 2, // Genesis is a withdrawals block + WithdrawalsPerBlock: 16, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 1", + About: ` + Tests the withdrawals fork happening directly after genesis. + `, + }, + WithdrawalsForkHeight: 1, // Only Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 16, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 2", + About: ` + 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. + `, + }, + WithdrawalsForkHeight: 2, // Genesis and Block 1 are Pre-Withdrawals + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 16, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 3", + About: ` + Tests the transition to the withdrawals fork after two blocks + have happened. + Block 2 is sent with invalid non-null withdrawals payload and + client is expected to respond with the appropriate error. + `, + }, + WithdrawalsForkHeight: 3, // Genesis, Block 1 and 2 are Pre-Withdrawals + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 16, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdraw to a single account", + About: ` + Make multiple withdrawals to a single account. + `, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 64, + WithdrawableAccountCount: 1, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdraw to two accounts", + About: ` + Make multiple withdrawals to two different accounts, repeated in + round-robin. + Reasoning: There might be a difference in implementation when an + account appears multiple times in the withdrawals list but the list + is not in ordered sequence. + `, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 64, + WithdrawableAccountCount: 2, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdraw many accounts", + About: ` + Make multiple withdrawals to 1024 different accounts. + Execute many blocks this way. + `, + TimeoutSeconds: 240, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 4, + WithdrawalsPerBlock: 1024, + WithdrawableAccountCount: 1024, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdraw zero amount", + About: ` + Make multiple withdrawals where the amount withdrawn is 0. + `, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 64, + WithdrawableAccountCount: 2, + WithdrawAmounts: []uint64{ + 0, + 1, + }, + }, + + &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Empty Withdrawals", + About: ` + Produce withdrawals block with zero withdrawals. + `, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 1, + WithdrawalsPerBlock: 0, + }, + + // Sync Tests + &WithdrawalsSyncSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Sync after 2 blocks - Withdrawals on Block 1 - Single Withdrawal Account - No Transactions", + About: ` + - Spawn a first client + - Go through withdrawals fork on Block 1 + - Withdraw to a single account 16 times each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance + `, + TimeoutSeconds: 6000, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 1, + TransactionsPerBlock: common.Big0, + }, + SyncSteps: 1, + }, + &WithdrawalsSyncSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Sync after 2 blocks - Withdrawals on Block 1 - Single Withdrawal Account", + About: ` + - Spawn a first client + - Go through withdrawals fork on Block 1 + - Withdraw to a single account 16 times each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance + `, + }, + WithdrawalsForkHeight: 1, + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 1, + }, + SyncSteps: 1, + }, + &WithdrawalsSyncSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Sync after 2 blocks - Withdrawals on Genesis - Single Withdrawal Account", + About: ` + - Spawn a first client, with Withdrawals since genesis + - Withdraw to a single account 16 times each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync and verify withdrawn account's balance + `, + }, + WithdrawalsForkHeight: 0, + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 1, + }, + SyncSteps: 1, + }, + &WithdrawalsSyncSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Sync after 2 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts - No Transactions", + About: ` + - Spawn a first client + - Go through withdrawals fork on Block 2 + - Withdraw to 16 accounts each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync, which include syncing a pre-Withdrawals block, and verify withdrawn account's balance + `, + }, + WithdrawalsForkHeight: 2, + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 16, + TransactionsPerBlock: common.Big0, + }, + SyncSteps: 1, + }, + &WithdrawalsSyncSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Sync after 2 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts", + About: ` + - Spawn a first client + - Go through withdrawals fork on Block 2 + - Withdraw to 16 accounts each block for 2 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync, which include syncing a pre-Withdrawals block, and verify withdrawn account's balance + `, + }, + WithdrawalsForkHeight: 2, + WithdrawalsBlockCount: 2, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 16, + }, + SyncSteps: 1, + }, + &WithdrawalsSyncSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Sync after 128 blocks - Withdrawals on Block 2 - Multiple Withdrawal Accounts", + About: ` + - Spawn a first client + - Go through withdrawals fork on Block 2 + - Withdraw to many accounts 16 times each block for 128 blocks + - Spawn a secondary client and send FCUV2(head) + - Wait for sync, which include syncing a pre-Withdrawals block, and verify withdrawn account's balance + `, + TimeoutSeconds: 300, + }, + WithdrawalsForkHeight: 2, + WithdrawalsBlockCount: 128, + WithdrawalsPerBlock: 16, + WithdrawableAccountCount: 1024, + }, + SyncSteps: 1, + }, + + //Re-Org tests + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 1 - 1 Block Re-Org", + About: ` + Tests a simple 1 block re-org + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 1, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 16, + }, + ReOrgBlockCount: 1, + ReOrgViaSync: false, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 1 - 8 Block Re-Org NewPayload", + About: ` + Tests a 8 block re-org using NewPayload + Re-org does not change withdrawals fork height + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 1, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 16, + }, + ReOrgBlockCount: 8, + ReOrgViaSync: false, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 1 - 8 Block Re-Org, Sync", + About: ` + Tests a 8 block re-org using NewPayload + Re-org does not change withdrawals fork height + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 1, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 16, + WithdrawalsPerBlock: 16, + }, + ReOrgBlockCount: 8, + ReOrgViaSync: true, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 8 - 10 Block Re-Org NewPayload", + About: ` + Tests a 10 block re-org using NewPayload + Re-org does not change withdrawals fork height, but changes + the payload at the height of the fork + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 8, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 8, + WithdrawalsPerBlock: 128, + }, + ReOrgBlockCount: 10, + ReOrgViaSync: false, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Block 8 - 10 Block Re-Org Sync", + About: ` + Tests a 10 block re-org using sync + Re-org does not change withdrawals fork height, but changes + the payload at the height of the fork + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 8, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 8, + WithdrawalsPerBlock: 128, + }, + ReOrgBlockCount: 10, + ReOrgViaSync: true, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org", + About: ` + Tests a 10 block re-org using NewPayload + Sidechain reaches withdrawals fork at a lower block height + than the canonical chain + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 8, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 8, + WithdrawalsPerBlock: 128, + }, + ReOrgBlockCount: 10, + ReOrgViaSync: false, + SidechainTimeIncrements: 2, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Canonical Block 8 / Side Block 7 - 10 Block Re-Org Sync", + About: ` + Tests a 10 block re-org using sync + Sidechain reaches withdrawals fork at a lower block height + than the canonical chain + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 8, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 8, + WithdrawalsPerBlock: 128, + }, + ReOrgBlockCount: 10, + ReOrgViaSync: true, + SidechainTimeIncrements: 2, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Canonical Block 8 / Side Block 9 - 10 Block Re-Org", + About: ` + Tests a 10 block re-org using NewPayload + Sidechain reaches withdrawals fork at a higher block height + than the canonical chain + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 8, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 8, + WithdrawalsPerBlock: 128, + TimeIncrements: 2, + }, + ReOrgBlockCount: 10, + ReOrgViaSync: false, + SidechainTimeIncrements: 1, + }, + &WithdrawalsReorgSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Withdrawals Fork on Canonical Block 8 / Side Block 9 - 10 Block Re-Org Sync", + About: ` + Tests a 10 block re-org using sync + Sidechain reaches withdrawals fork at a higher block height + than the canonical chain + `, + SlotsToSafe: big.NewInt(32), + SlotsToFinalized: big.NewInt(64), + }, + WithdrawalsForkHeight: 8, // Genesis is Pre-Withdrawals + WithdrawalsBlockCount: 8, + WithdrawalsPerBlock: 128, + TimeIncrements: 2, + }, + ReOrgBlockCount: 10, + ReOrgViaSync: true, + SidechainTimeIncrements: 1, + }, + // TODO: REORG SYNC WHERE SYNCED BLOCKS HAVE WITHDRAWALS BEFORE TIME + + // EVM Tests (EIP-3651, EIP-3855, EIP-3860) + &MaxInitcodeSizeSpec{ + WithdrawalsBaseSpec: &WithdrawalsBaseSpec{ + Spec: test.Spec{ + Name: "Max Initcode Size", + }, + WithdrawalsForkHeight: 2, // Block 1 is Pre-Withdrawals + WithdrawalsBlockCount: 2, + }, + OverflowMaxInitcodeTxCountBeforeFork: 0, + OverflowMaxInitcodeTxCountAfterFork: 1, + }, +} + +// Helper types to convert gwei into wei more easily +func WeiAmount(w *types.Withdrawal) *big.Int { + return new(big.Int).Mul(new(big.Int).SetUint64(w.Amount), big.NewInt(1e9)) +} + +// Helper structure used to keep history of the amounts withdrawn to each test account. +type WithdrawalsHistory map[uint64]types.Withdrawals + +// Gets an account expected value for a given block, taking into account all +// withdrawals that credited the account. +func (wh WithdrawalsHistory) GetExpectedAccountBalance(account common.Address, block uint64) *big.Int { + balance := big.NewInt(0) + for b := uint64(0); b <= block; b++ { + if withdrawals, ok := wh[b]; ok && withdrawals != nil { + for _, withdrawal := range withdrawals { + if withdrawal.Address == account { + balance.Add(balance, WeiAmount(withdrawal)) + } + } + } + } + return balance +} + +// Get a list of all addresses that were credited by withdrawals on a given block. +func (wh WithdrawalsHistory) GetAddressesWithdrawnOnBlock(block uint64) []common.Address { + addressMap := make(map[common.Address]bool) + if withdrawals, ok := wh[block]; ok && withdrawals != nil { + for _, withdrawal := range withdrawals { + addressMap[withdrawal.Address] = true + } + } + addressList := make([]common.Address, 0) + for addr := range addressMap { + addressList = append(addressList, addr) + } + return addressList +} + +// Get the withdrawals list for a given block. +func (wh WithdrawalsHistory) GetWithdrawals(block uint64) types.Withdrawals { + if w, ok := wh[block]; ok && w != nil { + return w + } + return make(types.Withdrawals, 0) +} + +// Get the withdrawn accounts list until a given block height. +func (wh WithdrawalsHistory) GetWithdrawnAccounts(blockHeight uint64) map[common.Address]*big.Int { + accounts := make(map[common.Address]*big.Int) + for block := uint64(0); block <= blockHeight; block++ { + if withdrawals, ok := wh[block]; ok && withdrawals != nil { + for _, withdrawal := range withdrawals { + if currentBalance, ok2 := accounts[withdrawal.Address]; ok2 { + currentBalance.Add(currentBalance, WeiAmount(withdrawal)) + } else { + accounts[withdrawal.Address] = new(big.Int).Set(WeiAmount(withdrawal)) + } + } + } + } + return accounts +} + +// Verify all withdrawals on a client at a given height +func (wh WithdrawalsHistory) VerifyWithdrawals(block uint64, rpcBlock *big.Int, testEngine *test.TestEngineClient) { + accounts := wh.GetWithdrawnAccounts(block) + for account, expectedBalance := range accounts { + r := testEngine.TestBalanceAt(account, rpcBlock) + r.ExpectBalanceEqual(expectedBalance) + // All withdrawals account have a bytecode that unconditionally set the + // zero storage key to one on EVM execution. + // Withdrawals must not trigger EVM so we expect zero. + s := testEngine.TestStorageAt(account, common.BigToHash(common.Big0), rpcBlock) + s.ExpectBigIntStorageEqual(common.Big0) + } +} + +// Create a new copy of the withdrawals history +func (wh WithdrawalsHistory) Copy() WithdrawalsHistory { + result := make(WithdrawalsHistory) + for k, v := range wh { + result[k] = v + } + return result +} + +// Withdrawals base spec: +// Specifies a simple withdrawals test where the withdrawals fork can happen +// on genesis or afterwards. +type WithdrawalsBaseSpec struct { + test.Spec + TimeIncrements uint64 // Timestamp increments per block throughout the test + WithdrawalsForkHeight uint64 // Withdrawals activation fork height + WithdrawalsBlockCount uint64 // Number of blocks on and after withdrawals fork activation + WithdrawalsPerBlock uint64 // Number of withdrawals per block + WithdrawableAccountCount uint64 // Number of accounts to withdraw to (round-robin) + 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 + SkipBaseVerifications bool // For code reuse of the base spec procedure +} + +// Get the per-block timestamp increments configured for this test +func (ws *WithdrawalsBaseSpec) GetBlockTimeIncrements() uint64 { + if ws.TimeIncrements == 0 { + return 1 + } + return ws.TimeIncrements +} + +// Timestamp delta between genesis and the withdrawals fork +func (ws *WithdrawalsBaseSpec) GetWithdrawalsGenesisTimeDelta() uint64 { + return ws.WithdrawalsForkHeight * ws.GetBlockTimeIncrements() +} + +// Calculates Shanghai fork timestamp given the amount of blocks that need to be +// produced beforehand. +func (ws *WithdrawalsBaseSpec) GetWithdrawalsForkTime() uint64 { + return uint64(globals.GenesisTimestamp) + ws.GetWithdrawalsGenesisTimeDelta() +} + +// Generates the fork config, including withdrawals fork timestamp. +func (ws *WithdrawalsBaseSpec) GetForkConfig() test.ForkConfig { + return test.ForkConfig{ + ShanghaiTimestamp: big.NewInt(int64(ws.GetWithdrawalsForkTime())), + } +} + +// 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) { + accountAddress := common.BigToAddress(start) + // Bytecode to unconditionally set a storage key + g.Alloc[accountAddress] = core.GenesisAccount{ + Code: []byte{ + 0x60, // PUSH1(0x01) + 0x01, + 0x60, // PUSH1(0x00) + 0x00, + 0x55, // SSTORE + 0x00, // STOP + }, // sstore(0, 1) + Nonce: 0, + Balance: common.Big0, + } + } +} + +// Append the accounts we are going to withdraw to, which should also include +// bytecode for testing purposes. +func (ws *WithdrawalsBaseSpec) GetGenesis() *core.Genesis { + genesis := ws.Spec.GetGenesis() + startAccount := big.NewInt(0x1000) + endAccount := big.NewInt(0x1000 + int64(ws.GetWithdrawableAccountCount()) - 1) + AddUnconditionalBytecode(genesis, startAccount, endAccount) + return genesis +} + +// Changes the CL Mocker default time increments of 1 to the value specified +// in the test spec. +func (ws *WithdrawalsBaseSpec) ConfigureCLMock(cl *clmock.CLMocker) { + cl.BlockTimestampIncrement = big.NewInt(int64(ws.GetBlockTimeIncrements())) +} + +// Number of blocks to be produced (not counting genesis) before withdrawals +// fork. +func (ws *WithdrawalsBaseSpec) GetPreWithdrawalsBlockCount() uint64 { + if ws.WithdrawalsForkHeight == 0 { + return 0 + } else { + return ws.WithdrawalsForkHeight - 1 + } +} + +// Number of payloads to be produced (pre and post withdrawals) during the entire test +func (ws *WithdrawalsBaseSpec) GetTotalPayloadCount() uint64 { + return ws.GetPreWithdrawalsBlockCount() + ws.WithdrawalsBlockCount +} + +func (ws *WithdrawalsBaseSpec) GetWithdrawableAccountCount() uint64 { + if ws.WithdrawableAccountCount == 0 { + // Withdraw to 16 accounts by default + return 16 + } + return ws.WithdrawableAccountCount +} + +// Generates a list of withdrawals based on current configuration +func (ws *WithdrawalsBaseSpec) GenerateWithdrawalsForBlock(nextIndex uint64, startAccount *big.Int) (types.Withdrawals, uint64) { + differentAccounts := ws.GetWithdrawableAccountCount() + withdrawAmounts := ws.WithdrawAmounts + if withdrawAmounts == nil { + withdrawAmounts = []uint64{ + 1, + } + } + + nextWithdrawals := make(types.Withdrawals, 0) + for i := uint64(0); i < ws.WithdrawalsPerBlock; i++ { + nextAccount := new(big.Int).Set(startAccount) + nextAccount.Add(nextAccount, big.NewInt(int64(nextIndex%differentAccounts))) + nextWithdrawal := &types.Withdrawal{ + Index: nextIndex, + Validator: nextIndex, + Address: common.BigToAddress(nextAccount), + Amount: withdrawAmounts[int(nextIndex)%len(withdrawAmounts)], + } + nextWithdrawals = append(nextWithdrawals, nextWithdrawal) + nextIndex++ + } + return nextWithdrawals, nextIndex +} + +func (ws *WithdrawalsBaseSpec) GetTransactionCountPerPayload() uint64 { + if ws.TransactionsPerBlock == nil { + return 16 + } + return ws.TransactionsPerBlock.Uint64() +} + +// Base test case execution procedure for withdrawals +func (ws *WithdrawalsBaseSpec) Execute(t *test.Env) { + // Create the withdrawals history object + ws.WithdrawalsHistory = make(WithdrawalsHistory) + + t.CLMock.WaitForTTD() + + // Check if we have pre-Shanghai blocks + if ws.GetWithdrawalsForkTime() > uint64(globals.GenesisTimestamp) { + // Check `latest` during all pre-shanghai blocks, none should + // contain `withdrawalsRoot`, including genesis. + + // Genesis should not contain `withdrawalsRoot` either + r := t.TestEngine.TestBlockByNumber(nil) + r.ExpectationDescription = ` + Requested "latest" block expecting genesis to contain + withdrawalRoot=nil, because genesis.timestamp < shanghaiTime + ` + r.ExpectWithdrawalsRoot(nil) + } else { + // Genesis is post shanghai, it should contain EmptyWithdrawalsRoot + r := t.TestEngine.TestBlockByNumber(nil) + r.ExpectationDescription = ` + Requested "latest" block expecting genesis to contain + withdrawalRoot=EmptyTrieRoot, because genesis.timestamp >= shanghaiTime + ` + r.ExpectWithdrawalsRoot(helper.EmptyWithdrawalsRootHash) + } + + // Produce any blocks necessary to reach withdrawals fork + t.CLMock.ProduceBlocks(int(ws.GetPreWithdrawalsBlockCount()), clmock.BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + if !ws.SkipBaseVerifications { + // Try to send a ForkchoiceUpdatedV2 with non-null + // withdrawals before Shanghai + r := t.TestEngine.TestEngineForkchoiceUpdatedV2( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: t.CLMock.LatestHeader.Hash(), + }, + &beacon.PayloadAttributes{ + Timestamp: t.CLMock.LatestHeader.Time + 1, + Random: common.Hash{}, + SuggestedFeeRecipient: common.Address{}, + Withdrawals: make(types.Withdrawals, 0), + }, + ) + r.ExpectationDescription = "Sent pre-shanghai fcu using EngineForkchoiceUpdatedV2+Withdrawals, error is expected" + r.ExpectErrorCode(InvalidParamsError) + } + }, + OnGetPayload: func() { + if !ws.SkipBaseVerifications { + // Send produced payload but try to include non-nil + // `withdrawals`, it should fail. + emptyWithdrawalsList := make(types.Withdrawals, 0) + payloadPlusWithdrawals, err := helper.CustomizePayload(&t.CLMock.LatestPayloadBuilt, &helper.CustomPayloadData{ + Withdrawals: emptyWithdrawalsList, + }) + if err != nil { + t.Fatalf("Unable to append withdrawals: %v", err) + } + r := t.TestEngine.TestEngineNewPayloadV2(payloadPlusWithdrawals) + r.ExpectationDescription = "Sent pre-shanghai payload using NewPayloadV2+Withdrawals, error is expected" + r.ExpectErrorCode(InvalidParamsError) + } + }, + OnNewPayloadBroadcast: func() { + if !ws.SkipBaseVerifications { + // We sent a pre-shanghai FCU. + // Keep expecting `nil` until Shanghai. + r := t.TestEngine.TestBlockByNumber(nil) + r.ExpectationDescription = fmt.Sprintf(` + Requested "latest" block expecting block to contain + withdrawalRoot=nil, because (block %d).timestamp < shanghaiTime + `, + t.CLMock.LatestPayloadBuilt.Number, + ) + r.ExpectWithdrawalsRoot(nil) + } + }, + }) + + // Produce requested post-shanghai blocks + // (At least 1 block will be produced after this procedure ends). + var ( + startAccount = big.NewInt(0x1000) + nextIndex = uint64(0) + ) + + t.CLMock.ProduceBlocks(int(ws.WithdrawalsBlockCount), clmock.BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + // Send some withdrawals + t.CLMock.NextWithdrawals, nextIndex = ws.GenerateWithdrawalsForBlock(nextIndex, startAccount) + ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber] = t.CLMock.NextWithdrawals + // Send some transactions + for i := uint64(0); i < ws.GetTransactionCountPerPayload(); i++ { + _, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + 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) + } + } + }, + OnGetPayload: func() { + // TODO: Send new payload with `withdrawals=null` and expect error + }, + OnNewPayloadBroadcast: func() { + // Check withdrawal addresses and verify withdrawal balances + // have not yet been applied + if !ws.SkipBaseVerifications { + for _, addr := range ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(t.CLMock.LatestExecutedPayload.Number) { + // Test balance at `latest`, which should not yet have the + // withdrawal applied. + expectedAccountBalance := ws.WithdrawalsHistory.GetExpectedAccountBalance( + addr, + t.CLMock.LatestExecutedPayload.Number-1) + r := t.TestEngine.TestBalanceAt(addr, nil) + r.ExpectationDescription = fmt.Sprintf(` + Requested balance for account %s on "latest" block + after engine_newPayloadV2, expecting balance to be equal + to value on previous block (%d), since the new payload + has not yet been applied. + `, + addr, + t.CLMock.LatestExecutedPayload.Number-1, + ) + r.ExpectBalanceEqual(expectedAccountBalance) + } + } + }, + OnForkchoiceBroadcast: func() { + // Check withdrawal addresses and verify withdrawal balances + // have been applied + if !ws.SkipBaseVerifications { + for _, addr := range ws.WithdrawalsHistory.GetAddressesWithdrawnOnBlock(t.CLMock.LatestExecutedPayload.Number) { + // Test balance at `latest`, which should have the + // withdrawal applied. + r := t.TestEngine.TestBalanceAt(addr, nil) + r.ExpectationDescription = fmt.Sprintf(` + Requested balance for account %s on "latest" block + after engine_forkchoiceUpdatedV2, expecting balance to + be equal to value on latest payload (%d), since the new payload + has not yet been applied. + `, + addr, + t.CLMock.LatestExecutedPayload.Number, + ) + r.ExpectBalanceEqual( + ws.WithdrawalsHistory.GetExpectedAccountBalance( + addr, + t.CLMock.LatestExecutedPayload.Number), + ) + } + // Check the correct withdrawal root on `latest` block + r := t.TestEngine.TestBlockByNumber(nil) + expectedWithdrawalsRoot := helper.ComputeWithdrawalsRoot( + ws.WithdrawalsHistory.GetWithdrawals( + t.CLMock.LatestExecutedPayload.Number, + ), + ) + jsWithdrawals, _ := json.MarshalIndent(ws.WithdrawalsHistory.GetWithdrawals(t.CLMock.LatestExecutedPayload.Number), "", " ") + r.ExpectationDescription = fmt.Sprintf(` + Requested "latest" block after engine_forkchoiceUpdatedV2, + to verify withdrawalsRoot with the following withdrawals: + %s`, jsWithdrawals) + r.ExpectWithdrawalsRoot(&expectedWithdrawalsRoot) + } + }, + }) + // Iterate over balance history of withdrawn accounts using RPC and + // check that the balances match expected values. + // Also check one block before the withdrawal took place, verify that + // withdrawal has not been updated. + if !ws.SkipBaseVerifications { + for block := uint64(0); block <= t.CLMock.LatestExecutedPayload.Number; block++ { + ws.WithdrawalsHistory.VerifyWithdrawals(block, big.NewInt(int64(block)), t.TestEngine) + + // Check the correct withdrawal root on past blocks + r := t.TestEngine.TestBlockByNumber(big.NewInt(int64(block))) + var expectedWithdrawalsRoot *common.Hash = nil + if block >= ws.WithdrawalsForkHeight { + calcWithdrawalsRoot := helper.ComputeWithdrawalsRoot( + ws.WithdrawalsHistory.GetWithdrawals(block), + ) + expectedWithdrawalsRoot = &calcWithdrawalsRoot + } + jsWithdrawals, _ := json.MarshalIndent(ws.WithdrawalsHistory.GetWithdrawals(block), "", " ") + r.ExpectationDescription = fmt.Sprintf(` + Requested block %d to verify withdrawalsRoot with the + following withdrawals: + %s`, block, jsWithdrawals) + + r.ExpectWithdrawalsRoot(expectedWithdrawalsRoot) + + } + + // Verify on `latest` + ws.WithdrawalsHistory.VerifyWithdrawals(t.CLMock.LatestExecutedPayload.Number, nil, t.TestEngine) + } +} + +// Withdrawals sync spec: +// Specifies a withdrawals test where the withdrawals happen and then a +// client needs to sync and apply the withdrawals. +type WithdrawalsSyncSpec struct { + *WithdrawalsBaseSpec + SyncSteps int // Sync block chunks that will be passed as head through FCUs to the syncing client + SyncShouldFail bool // +} + +func (ws *WithdrawalsSyncSpec) Execute(t *test.Env) { + // Do the base withdrawal test first, skipping base verifications + ws.WithdrawalsBaseSpec.SkipBaseVerifications = true + ws.WithdrawalsBaseSpec.Execute(t) + + // Spawn a secondary client which will need to sync to the primary client + secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles, t.Engine) + if err != nil { + t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) + } + secondaryEngineTest := test.NewTestEngineClient(t, secondaryEngine) + t.CLMock.AddEngineClient(secondaryEngine) + + if ws.SyncSteps > 1 { + // TODO + } else { + // Send the FCU to trigger sync on the secondary client + loop: + for { + select { + case <-t.TimeoutContext.Done(): + t.Fatalf("FAIL (%s): Timeout while waiting for secondary client to sync", t.TestName) + case <-time.After(time.Second): + secondaryEngineTest.TestEngineNewPayloadV2( + &t.CLMock.LatestExecutedPayload, + ) + r := secondaryEngineTest.TestEngineForkchoiceUpdatedV2( + &t.CLMock.LatestForkchoice, + nil, + ) + if r.Response.PayloadStatus.Status == test.Valid { + break loop + } + if r.Response.PayloadStatus.Status == test.Invalid { + t.Fatalf("FAIL (%s): Syncing client rejected valid chain: %s", t.TestName, r.Response) + } + } + } + } + ws.WithdrawalsHistory.VerifyWithdrawals(t.CLMock.LatestHeader.Number.Uint64(), nil, secondaryEngineTest) +} + +// Withdrawals re-org spec: +// Specifies a withdrawals test where the withdrawals re-org can happen +// even to a point before withdrawals were enabled, or simply to a previous +// withdrawals block. +type WithdrawalsReorgSpec struct { + *WithdrawalsBaseSpec + + ReOrgBlockCount uint64 // How many blocks the re-org will replace, including the head + ReOrgViaSync bool // Whether the client should fetch the sidechain by syncing from the secondary client + SidechainTimeIncrements uint64 +} + +func (ws *WithdrawalsReorgSpec) GetSidechainSplitHeight() uint64 { + if ws.ReOrgBlockCount > ws.GetTotalPayloadCount() { + panic("invalid payload/re-org configuration") + } + return ws.GetTotalPayloadCount() + 1 - ws.ReOrgBlockCount +} + +func (ws *WithdrawalsReorgSpec) GetSidechainBlockTimeIncrements() uint64 { + if ws.SidechainTimeIncrements == 0 { + return ws.GetBlockTimeIncrements() + } + return ws.SidechainTimeIncrements +} + +func (ws *WithdrawalsReorgSpec) GetSidechainWithdrawalsForkHeight() uint64 { + if ws.GetSidechainBlockTimeIncrements() != ws.GetBlockTimeIncrements() { + // Block timestamp increments in both chains are different so need to calculate different heights, only if split happens before fork + if ws.GetSidechainSplitHeight() == 0 { + // We cannot split by having two different genesis blocks. + panic("invalid sidechain split height") + } + if ws.GetSidechainSplitHeight() <= ws.WithdrawalsForkHeight { + // We need to calculate the height of the fork on the sidechain + sidechainSplitBlockTimestamp := ((ws.GetSidechainSplitHeight() - 1) * ws.GetBlockTimeIncrements()) + remainingTime := (ws.GetWithdrawalsGenesisTimeDelta() - sidechainSplitBlockTimestamp) + if remainingTime == 0 { + return ws.GetSidechainSplitHeight() + } + return ((remainingTime - 1) / ws.SidechainTimeIncrements) + ws.GetSidechainSplitHeight() + + } + } + return ws.WithdrawalsForkHeight +} + +func (ws *WithdrawalsReorgSpec) Execute(t *test.Env) { + // Create the withdrawals history object + ws.WithdrawalsHistory = make(WithdrawalsHistory) + + t.CLMock.WaitForTTD() + + // Spawn a secondary client which will produce the sidechain + secondaryEngine, err := hive_rpc.HiveRPCEngineStarter{}.StartClient(t.T, t.TestContext, t.Genesis, t.ClientParams, t.ClientFiles, t.Engine) + if err != nil { + t.Fatalf("FAIL (%s): Unable to spawn a secondary client: %v", t.TestName, err) + } + secondaryEngineTest := test.NewTestEngineClient(t, secondaryEngine) + // t.CLMock.AddEngineClient(secondaryEngine) + + var ( + canonicalStartAccount = big.NewInt(0x1000) + canonicalNextIndex = uint64(0) + sidechainStartAccount = new(big.Int).SetBit(common.Big0, 160, 1) + sidechainNextIndex = uint64(0) + sidechainWithdrawalsHistory = make(WithdrawalsHistory) + sidechain = make(map[uint64]*beacon.ExecutableData) + sidechainPayloadId *beacon.PayloadID + ) + + // Sidechain withdraws on the max account value range 0xffffffffffffffffffffffffffffffffffffffff + sidechainStartAccount.Sub(sidechainStartAccount, big.NewInt(int64(ws.GetWithdrawableAccountCount())+1)) + + t.CLMock.ProduceBlocks(int(ws.GetPreWithdrawalsBlockCount()+ws.WithdrawalsBlockCount), clmock.BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + t.CLMock.NextWithdrawals = nil + + if t.CLMock.CurrentPayloadNumber >= ws.WithdrawalsForkHeight { + // Prepare some withdrawals + t.CLMock.NextWithdrawals, canonicalNextIndex = ws.GenerateWithdrawalsForBlock(canonicalNextIndex, canonicalStartAccount) + ws.WithdrawalsHistory[t.CLMock.CurrentPayloadNumber] = t.CLMock.NextWithdrawals + } + + if t.CLMock.CurrentPayloadNumber >= ws.GetSidechainSplitHeight() { + // We have split + if t.CLMock.CurrentPayloadNumber >= ws.GetSidechainWithdrawalsForkHeight() { + // And we are past the withdrawals fork on the sidechain + sidechainWithdrawalsHistory[t.CLMock.CurrentPayloadNumber], sidechainNextIndex = ws.GenerateWithdrawalsForBlock(sidechainNextIndex, sidechainStartAccount) + } // else nothing to do + } else { + // We have not split + sidechainWithdrawalsHistory[t.CLMock.CurrentPayloadNumber] = t.CLMock.NextWithdrawals + sidechainNextIndex = canonicalNextIndex + } + + }, + OnRequestNextPayload: func() { + // Send transactions to be included in the payload + for i := uint64(0); i < ws.GetTransactionCountPerPayload(); i++ { + tx, err := helper.SendNextTransaction( + t.TestContext, + t.CLMock.NextBlockProducer, + &helper.BaseTransactionCreator{ + Recipient: &globals.PrevRandaoContractAddr, + 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) + } + // Error will be ignored here since the tx could have been already relayed + secondaryEngine.SendTransaction(t.TestContext, tx) + } + + if t.CLMock.CurrentPayloadNumber >= ws.GetSidechainSplitHeight() { + // Also request a payload from the sidechain + fcU := beacon.ForkchoiceStateV1{ + HeadBlockHash: t.CLMock.LatestForkchoice.HeadBlockHash, + } + + if t.CLMock.CurrentPayloadNumber > ws.GetSidechainSplitHeight() { + if lastSidePayload, ok := sidechain[t.CLMock.CurrentPayloadNumber-1]; !ok { + panic("sidechain payload not found") + } else { + fcU.HeadBlockHash = lastSidePayload.BlockHash + } + } + + var version int + pAttributes := beacon.PayloadAttributes{ + Random: t.CLMock.LatestPayloadAttributes.Random, + SuggestedFeeRecipient: t.CLMock.LatestPayloadAttributes.SuggestedFeeRecipient, + } + if t.CLMock.CurrentPayloadNumber > ws.GetSidechainSplitHeight() { + pAttributes.Timestamp = sidechain[t.CLMock.CurrentPayloadNumber-1].Timestamp + uint64(ws.GetSidechainBlockTimeIncrements()) + } else if t.CLMock.CurrentPayloadNumber == ws.GetSidechainSplitHeight() { + pAttributes.Timestamp = t.CLMock.LatestHeader.Time + uint64(ws.GetSidechainBlockTimeIncrements()) + } else { + pAttributes.Timestamp = t.CLMock.LatestPayloadAttributes.Timestamp + } + if t.CLMock.CurrentPayloadNumber >= ws.GetSidechainWithdrawalsForkHeight() { + // Withdrawals + version = 2 + pAttributes.Withdrawals = sidechainWithdrawalsHistory[t.CLMock.CurrentPayloadNumber] + } else { + // No withdrawals + version = 1 + } + + t.Logf("INFO (%s): Requesting sidechain payload %d: %v", t.TestName, t.CLMock.CurrentPayloadNumber, pAttributes) + + r := secondaryEngineTest.TestEngineForkchoiceUpdated(&fcU, &pAttributes, version) + r.ExpectNoError() + r.ExpectPayloadStatus(test.Valid) + if r.Response.PayloadID == nil { + t.Fatalf("FAIL (%s): Unable to get a payload ID on the sidechain", t.TestName) + } + sidechainPayloadId = r.Response.PayloadID + } + }, + OnGetPayload: func() { + var ( + version int + payload *beacon.ExecutableData + ) + if t.CLMock.CurrentPayloadNumber >= ws.GetSidechainWithdrawalsForkHeight() { + version = 2 + } else { + version = 1 + } + if t.CLMock.LatestPayloadBuilt.Number >= ws.GetSidechainSplitHeight() { + // This payload is built by the secondary client, hence need to manually fetch it here + r := secondaryEngineTest.TestEngineGetPayload(sidechainPayloadId, version) + r.ExpectNoError() + payload = &r.Payload + sidechain[payload.Number] = payload + } else { + // This block is part of both chains, simply forward it to the secondary client + payload = &t.CLMock.LatestPayloadBuilt + } + r := secondaryEngineTest.TestEngineNewPayload(payload, version) + r.ExpectStatus(test.Valid) + p := secondaryEngineTest.TestEngineForkchoiceUpdated( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: payload.BlockHash, + }, + nil, + version, + ) + p.ExpectPayloadStatus(test.Valid) + }, + }) + + sidechainHeight := t.CLMock.LatestExecutedPayload.Number + + if ws.WithdrawalsForkHeight < ws.GetSidechainWithdrawalsForkHeight() { + // This means the canonical chain forked before the sidechain. + // Therefore we need to produce more sidechain payloads to reach + // at least`ws.WithdrawalsBlockCount` withdrawals payloads produced on + // the sidechain. + for i := uint64(0); i < ws.GetSidechainWithdrawalsForkHeight()-ws.WithdrawalsForkHeight; i++ { + sidechainWithdrawalsHistory[sidechainHeight+1], sidechainNextIndex = ws.GenerateWithdrawalsForBlock(sidechainNextIndex, sidechainStartAccount) + pAttributes := beacon.PayloadAttributes{ + Timestamp: sidechain[sidechainHeight].Timestamp + ws.GetSidechainBlockTimeIncrements(), + Random: t.CLMock.LatestPayloadAttributes.Random, + SuggestedFeeRecipient: t.CLMock.LatestPayloadAttributes.SuggestedFeeRecipient, + Withdrawals: sidechainWithdrawalsHistory[sidechainHeight+1], + } + r := secondaryEngineTest.TestEngineForkchoiceUpdatedV2(&beacon.ForkchoiceStateV1{ + HeadBlockHash: sidechain[sidechainHeight].BlockHash, + }, &pAttributes) + r.ExpectPayloadStatus(test.Valid) + time.Sleep(time.Second) + p := secondaryEngineTest.TestEngineGetPayloadV2(r.Response.PayloadID) + p.ExpectNoError() + s := secondaryEngineTest.TestEngineNewPayloadV2(&p.Payload) + s.ExpectStatus(test.Valid) + q := secondaryEngineTest.TestEngineForkchoiceUpdatedV2( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: p.Payload.BlockHash, + }, + nil, + ) + q.ExpectPayloadStatus(test.Valid) + sidechainHeight++ + sidechain[sidechainHeight] = &p.Payload + } + } + + // Check the withdrawals on the latest + ws.WithdrawalsHistory.VerifyWithdrawals( + sidechainHeight, + nil, + t.TestEngine, + ) + + if ws.ReOrgViaSync { + // Send latest sidechain payload as NewPayload + FCU and wait for sync + loop: + for { + r := t.TestEngine.TestEngineNewPayloadV2(sidechain[sidechainHeight]) + r.ExpectNoError() + p := t.TestEngine.TestEngineForkchoiceUpdatedV2( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: sidechain[sidechainHeight].BlockHash, + }, + nil, + ) + p.ExpectNoError() + if p.Response.PayloadStatus.Status == test.Invalid { + t.Fatalf("FAIL (%s): Primary client invalidated side chain", t.TestName) + } + select { + case <-t.TimeoutContext.Done(): + t.Fatalf("FAIL (%s): Timeout waiting for sync", t.TestName) + case <-time.After(time.Second): + b := t.TestEngine.TestBlockByNumber(nil) + if b.Block.Hash() == sidechain[sidechainHeight].BlockHash { + // sync successful + break loop + } + } + } + } else { + // Send all payloads one by one to the primary client + for payloadNumber := ws.GetSidechainSplitHeight(); payloadNumber <= sidechainHeight; payloadNumber++ { + payload, ok := sidechain[payloadNumber] + if !ok { + t.Fatalf("FAIL (%s): Invalid payload %d requested.", t.TestName, payloadNumber) + } + var version int + if payloadNumber >= ws.GetSidechainWithdrawalsForkHeight() { + version = 2 + } else { + version = 1 + } + t.Logf("INFO (%s): Sending sidechain payload %d, hash=%s, parent=%s", t.TestName, payloadNumber, payload.BlockHash, payload.ParentHash) + r := t.TestEngine.TestEngineNewPayload(payload, version) + r.ExpectStatusEither(test.Valid, test.Accepted) + p := t.TestEngine.TestEngineForkchoiceUpdated( + &beacon.ForkchoiceStateV1{ + HeadBlockHash: payload.BlockHash, + }, + nil, + version, + ) + p.ExpectPayloadStatus(test.Valid) + } + } + + // Verify withdrawals changed + sidechainWithdrawalsHistory.VerifyWithdrawals( + sidechainHeight, + nil, + t.TestEngine, + ) + // Verify all balances of accounts in the original chain didn't increase + // after the fork. + // We are using different accounts credited between the canonical chain + // and the fork. + // We check on `latest`. + ws.WithdrawalsHistory.VerifyWithdrawals( + ws.WithdrawalsForkHeight-1, + nil, + t.TestEngine, + ) + + // Re-Org back to the canonical chain + r := t.TestEngine.TestEngineForkchoiceUpdatedV2(&beacon.ForkchoiceStateV1{ + HeadBlockHash: t.CLMock.LatestPayloadBuilt.BlockHash, + }, nil) + r.ExpectPayloadStatus(test.Valid) +} + +// EIP-3860 Shanghai Tests: +// Send transactions overflowing the MAX_INITCODE_SIZE +// limit set in EIP-3860, before and after the Shanghai +// fork. +type MaxInitcodeSizeSpec struct { + *WithdrawalsBaseSpec + OverflowMaxInitcodeTxCountBeforeFork uint64 + OverflowMaxInitcodeTxCountAfterFork uint64 +} + +func (s *MaxInitcodeSizeSpec) Execute(t *test.Env) { + t.CLMock.WaitForTTD() + + invalidTxCreator := &helper.BigInitcodeTransactionCreator{ + InitcodeLength: MAX_INITCODE_SIZE + 1, + BaseTransactionCreator: helper.BaseTransactionCreator{ + GasLimit: 2000000, + }, + } + validTxCreator := &helper.BigInitcodeTransactionCreator{ + InitcodeLength: MAX_INITCODE_SIZE, + BaseTransactionCreator: helper.BaseTransactionCreator{ + GasLimit: 2000000, + }, + } + + if s.OverflowMaxInitcodeTxCountBeforeFork > 0 { + if s.GetPreWithdrawalsBlockCount() == 0 { + panic("invalid test configuration") + } + + for i := uint64(0); i < s.OverflowMaxInitcodeTxCountBeforeFork; i++ { + tx, err := invalidTxCreator.MakeTransaction(i) + if err != nil { + t.Fatalf("FAIL: Error creating max initcode transaction: %v", err) + } + err = t.Engine.SendTransaction(t.TestContext, tx) + if err != nil { + t.Fatalf("FAIL: Error sending max initcode transaction before Shanghai: %v", err) + } + } + } + + // Produce all blocks needed to reach Shanghai + t.Logf("INFO: Blocks until Shanghai=%d", s.GetPreWithdrawalsBlockCount()) + txIncluded := uint64(0) + t.CLMock.ProduceBlocks(int(s.GetPreWithdrawalsBlockCount()), clmock.BlockProcessCallbacks{ + OnGetPayload: func() { + t.Logf("INFO: Got Pre-Shanghai block=%d", t.CLMock.LatestPayloadBuilt.Number) + txIncluded += uint64(len(t.CLMock.LatestPayloadBuilt.Transactions)) + }, + }) + + // Check how many transactions were included + if txIncluded == 0 && s.OverflowMaxInitcodeTxCountBeforeFork > 0 { + t.Fatalf("FAIL: No max initcode txs included before Shanghai. Txs must have been included before the MAX_INITCODE_SIZE limit was enabled") + } + + // Create a payload, no txs should be included + t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ + OnGetPayload: func() { + if len(t.CLMock.LatestPayloadBuilt.Transactions) > 0 { + t.Fatalf("FAIL: Client included tx exceeding the MAX_INITCODE_SIZE in payload") + } + }, + }) + + // Send transactions after the fork + for i := txIncluded; i < (txIncluded + s.OverflowMaxInitcodeTxCountAfterFork); i++ { + tx, err := invalidTxCreator.MakeTransaction(i) + if err != nil { + t.Fatalf("FAIL: Error creating max initcode transaction: %v", err) + } + err = t.Engine.SendTransaction(t.TestContext, tx) + if err == nil { + t.Fatalf("FAIL: Client accepted tx exceeding the MAX_INITCODE_SIZE: %v", tx) + } + txBack, isPending, err := t.Engine.TransactionByHash(t.TestContext, tx.Hash()) + if txBack != nil || isPending || err == nil { + t.Fatalf("FAIL: Invalid tx was not unknown to the client: txBack=%v, isPending=%t, err=%v", txBack, isPending, err) + } + } + + // Try to include an invalid tx in new payload + var ( + validTx, _ = validTxCreator.MakeTransaction(txIncluded) + invalidTx, _ = invalidTxCreator.MakeTransaction(txIncluded) + ) + t.CLMock.ProduceSingleBlock(clmock.BlockProcessCallbacks{ + OnPayloadProducerSelected: func() { + t.Engine.SendTransaction(t.TestContext, validTx) + }, + OnGetPayload: func() { + validTxBytes, err := validTx.MarshalBinary() + if err != nil { + t.Fatalf("FAIL: Unable to marshal valid tx to binary: %v", err) + } + if len(t.CLMock.LatestPayloadBuilt.Transactions) != 1 || !bytes.Equal(validTxBytes, t.CLMock.LatestPayloadBuilt.Transactions[0]) { + t.Fatalf("FAIL: Client did not include valid tx with MAX_INITCODE_SIZE") + } + // Customize the payload to include a tx with an invalid initcode + customPayload, err := helper.CustomizePayloadTransactions(&t.CLMock.LatestPayloadBuilt, types.Transactions{invalidTx}) + if err != nil { + t.Fatalf("FAIL: Unable to customize payload: %v", err) + } + + r := t.TestEngine.TestEngineNewPayloadV2(customPayload) + r.ExpectStatus(test.Invalid) + r.ExpectLatestValidHash(&t.CLMock.LatestPayloadBuilt.ParentHash) + }, + }) +} diff --git a/simulators/ethereum/engine/test/env.go b/simulators/ethereum/engine/test/env.go index de4251f532..00a1f1fe83 100644 --- a/simulators/ethereum/engine/test/env.go +++ b/simulators/ethereum/engine/test/env.go @@ -13,6 +13,7 @@ import ( "github.com/ethereum/hive/simulators/ethereum/engine/helper" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/hive/hivesim" ) @@ -38,6 +39,7 @@ type Env struct { CLMock *clmock.CLMocker // Client parameters used to launch the default client + Genesis *core.Genesis ClientParams hivesim.Params ClientFiles hivesim.Params @@ -45,9 +47,19 @@ type Env struct { TestTransactionType helper.TestTransactionType } -func Run(testName string, ttd *big.Int, slotsToSafe *big.Int, slotsToFinalized *big.Int, timeout time.Duration, t *hivesim.T, c *hivesim.Client, fn func(*Env), cParams hivesim.Params, cFiles hivesim.Params, testTransactionType helper.TestTransactionType, safeSlotsToImportOptimistically int64) { +func Run(testSpec SpecInterface, ttd *big.Int, timeout time.Duration, t *hivesim.T, c *hivesim.Client, genesis *core.Genesis, cParams hivesim.Params, cFiles hivesim.Params) { // Setup the CL Mocker for this test - clMocker := clmock.NewCLMocker(t, slotsToSafe, slotsToFinalized, big.NewInt(safeSlotsToImportOptimistically)) + consensusConfig := testSpec.GetConsensusConfig() + clMocker := clmock.NewCLMocker( + t, + consensusConfig.SlotsToSafe, + consensusConfig.SlotsToFinalized, + big.NewInt(consensusConfig.SafeSlotsToImportOptimistically), + testSpec.GetForkConfig().ShanghaiTimestamp) + + // Send the CLMocker for configuration by the spec, if any. + testSpec.ConfigureCLMock(clMocker) + // Defer closing all clients defer func() { clMocker.CloseClients() @@ -55,9 +67,9 @@ func Run(testName string, ttd *big.Int, slotsToSafe *big.Int, slotsToFinalized * // Create Engine client from main hivesim.Client to be used by tests ec := hive_rpc.NewHiveRPCEngineClient(c, globals.EnginePortHTTP, globals.EthPortHTTP, globals.DefaultJwtTokenSecretBytes, ttd, &helper.LoggingRoundTrip{ - T: t, - Hc: c, - Inner: http.DefaultTransport, + Logger: t, + ID: c.Container, + Inner: http.DefaultTransport, }) defer ec.Close() @@ -66,15 +78,16 @@ func Run(testName string, ttd *big.Int, slotsToSafe *big.Int, slotsToFinalized * env := &Env{ T: t, - TestName: testName, + TestName: testSpec.GetName(), Client: c, Engine: ec, Eth: ec, HiveEngine: ec, CLMock: clMocker, + Genesis: genesis, ClientParams: cParams, ClientFiles: cFiles, - TestTransactionType: testTransactionType, + TestTransactionType: testSpec.GetTestTransactionType(), } // Before running the test, make sure Eth and Engine ports are open for the client @@ -106,7 +119,7 @@ func Run(testName string, ttd *big.Int, slotsToSafe *big.Int, slotsToFinalized * }() // Run the test - fn(env) + testSpec.Execute(env) } func (t *Env) MainTTD() *big.Int { diff --git a/simulators/ethereum/engine/test/expect.go b/simulators/ethereum/engine/test/expect.go index ebfa4d75a8..b063db4c40 100644 --- a/simulators/ethereum/engine/test/expect.go +++ b/simulators/ethereum/engine/test/expect.go @@ -2,6 +2,7 @@ package test import ( "context" + "encoding/json" "fmt" "math/big" "runtime" @@ -10,6 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" api "github.com/ethereum/go-ethereum/core/beacon" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/hive/simulators/ethereum/engine/client" "github.com/ethereum/hive/simulators/ethereum/engine/globals" ) @@ -52,10 +54,14 @@ func StatusesToString(statuses []PayloadStatus) []string { // Test Engine API Helper Structs type ExpectEnv struct { *Env + ExpectationDescription string } func (e ExpectEnv) Fatalf(format string, values ...interface{}) { PrintExpectStack(e.Env) + if e.ExpectationDescription != "" { + e.Env.Logf("DEBUG (%s): %s", e.TestName, e.ExpectationDescription) + } e.Env.Fatalf(format, values...) } @@ -66,59 +72,89 @@ type TestEngineClient struct { func NewTestEngineClient(t *Env, ec client.EngineClient) *TestEngineClient { return &TestEngineClient{ - ExpectEnv: &ExpectEnv{t}, + ExpectEnv: &ExpectEnv{Env: t}, Engine: ec, } } -// ForkchoiceUpdatedV1 +// ForkchoiceUpdated type ForkchoiceResponseExpectObject struct { *ExpectEnv - Response api.ForkChoiceResponse - Error error + Response api.ForkChoiceResponse + Version int + Error error + ErrorCode int } -func (tec *TestEngineClient) TestEngineForkchoiceUpdatedV1(fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributesV1) *ForkchoiceResponseExpectObject { +func (tec *TestEngineClient) TestEngineForkchoiceUpdatedV1(fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) *ForkchoiceResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() resp, err := tec.Engine.ForkchoiceUpdatedV1(ctx, fcState, pAttributes) - return &ForkchoiceResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &ForkchoiceResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, + Response: resp, + Version: 1, + Error: err, + } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (tec *TestEngineClient) TestEngineForkchoiceUpdatedV2(fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes) *ForkchoiceResponseExpectObject { + ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) + defer cancel() + resp, err := tec.Engine.ForkchoiceUpdatedV2(ctx, fcState, pAttributes) + ret := &ForkchoiceResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Response: resp, + Version: 2, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (tec *TestEngineClient) TestEngineForkchoiceUpdated(fcState *api.ForkchoiceStateV1, pAttributes *api.PayloadAttributes, version int) *ForkchoiceResponseExpectObject { + if version == 2 { + return tec.TestEngineForkchoiceUpdatedV2(fcState, pAttributes) + } + return tec.TestEngineForkchoiceUpdatedV1(fcState, pAttributes) } func (exp *ForkchoiceResponseExpectObject) ExpectNoError() { if exp.Error != nil { - exp.Fatalf("FAIL (%s): Unexpected error on EngineForkchoiceUpdatedV1: %v, expected=", exp.TestName, exp.Error) + exp.Fatalf("FAIL (%s): Unexpected error on EngineForkchoiceUpdatedV%d: %v, expected=", exp.TestName, exp.Version, exp.Error) } } func (exp *ForkchoiceResponseExpectObject) ExpectNoValidationError() { if exp.Response.PayloadStatus.ValidationError != nil { - exp.Fatalf("FAIL (%s): Unexpected validation error on EngineForkchoiceUpdatedV1: %v, expected=", exp.TestName, exp.Response.PayloadStatus.ValidationError) + exp.Fatalf("FAIL (%s): Unexpected validation error on EngineForkchoiceUpdatedV%d: %v, expected=", exp.TestName, exp.Version, exp.Response.PayloadStatus.ValidationError) } } func (exp *ForkchoiceResponseExpectObject) ExpectError() { if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on EngineForkchoiceUpdatedV1: response=%v", exp.TestName, exp.Response) + exp.Fatalf("FAIL (%s): Expected error on EngineForkchoiceUpdatedV%d: response=%v", exp.TestName, exp.Version, exp.Response) } } func (exp *ForkchoiceResponseExpectObject) ExpectErrorCode(code int) { - // TODO: Actually check error code - if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on EngineForkchoiceUpdatedV1: response=%v", exp.TestName, exp.Response) + exp.ExpectError() + if exp.ErrorCode != code { + exp.Fatalf("FAIL (%s): Expected error code on EngineForkchoiceUpdatedV%d: want=%d, got=%d", exp.TestName, exp.Version, code, exp.ErrorCode) } } func (exp *ForkchoiceResponseExpectObject) ExpectPayloadStatus(ps PayloadStatus) { exp.ExpectNoError() if PayloadStatus(exp.Response.PayloadStatus.Status) != ps { - exp.Fatalf("FAIL (%s): Unexpected status response on EngineForkchoiceUpdatedV1: %v, expected=%v", exp.TestName, exp.Response.PayloadStatus.Status, ps) + exp.Fatalf("FAIL (%s): Unexpected status response on EngineForkchoiceUpdatedV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Response.PayloadStatus.Status, ps) } } @@ -129,14 +165,14 @@ func (exp *ForkchoiceResponseExpectObject) ExpectAnyPayloadStatus(statuses ...Pa return } } - exp.Fatalf("FAIL (%s): Unexpected status response on EngineForkchoiceUpdatedV1: %v, expected=%v", exp.TestName, exp.Response.PayloadStatus.Status, StatusesToString(statuses)) + exp.Fatalf("FAIL (%s): Unexpected status response on EngineForkchoiceUpdatedV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Response.PayloadStatus.Status, StatusesToString(statuses)) } func (exp *ForkchoiceResponseExpectObject) ExpectLatestValidHash(lvh *common.Hash) { exp.ExpectNoError() if ((lvh == nil || exp.Response.PayloadStatus.LatestValidHash == nil) && exp.Response.PayloadStatus.LatestValidHash != lvh) || (lvh != nil && exp.Response.PayloadStatus.LatestValidHash != nil && *exp.Response.PayloadStatus.LatestValidHash != *lvh) { - exp.Fatalf("FAIL (%v): Unexpected LatestValidHash on EngineForkchoiceUpdatedV1: %v, expected=%v", exp.TestName, exp.Response.PayloadStatus.LatestValidHash, lvh) + exp.Fatalf("FAIL (%v): Unexpected LatestValidHash on EngineForkchoiceUpdatedV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Response.PayloadStatus.LatestValidHash, lvh) } } @@ -144,58 +180,96 @@ func (exp *ForkchoiceResponseExpectObject) ExpectPayloadID(pid *api.PayloadID) { exp.ExpectNoError() if ((exp.Response.PayloadID == nil || pid == nil) && exp.Response.PayloadID != pid) || (exp.Response.PayloadID != nil && pid != nil && *exp.Response.PayloadID != *pid) { - exp.Fatalf("FAIL (%v): Unexpected PayloadID on EngineForkchoiceUpdatedV1: %v, expected=%v", exp.TestName, exp.Response.PayloadID, pid) + exp.Fatalf("FAIL (%v): Unexpected PayloadID on EngineForkchoiceUpdatedV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Response.PayloadID, pid) } } -// NewPayloadV1 +// NewPayload type NewPayloadResponseExpectObject struct { *ExpectEnv - Status api.PayloadStatusV1 - Error error + Payload *api.ExecutableData + Status api.PayloadStatusV1 + Version int + Error error + ErrorCode int } -func (tec *TestEngineClient) TestEngineNewPayloadV1(payload *api.ExecutableDataV1) *NewPayloadResponseExpectObject { +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) - return &NewPayloadResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &NewPayloadResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, + Payload: payload, Status: status, + Version: 1, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (tec *TestEngineClient) TestEngineNewPayloadV2(payload *api.ExecutableData) *NewPayloadResponseExpectObject { + ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) + defer cancel() + status, err := tec.Engine.NewPayloadV2(ctx, payload) + ret := &NewPayloadResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, + Payload: payload, + Status: status, + Version: 2, + Error: err, + } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (tec *TestEngineClient) TestEngineNewPayload(payload *api.ExecutableData, version int) *NewPayloadResponseExpectObject { + if version == 2 { + return tec.TestEngineNewPayloadV2(payload) + } + return tec.TestEngineNewPayloadV1(payload) +} + +func (exp *NewPayloadResponseExpectObject) PayloadJson() string { + jsonPayload, _ := json.MarshalIndent(exp.Payload, "", " ") + return string(jsonPayload) } func (exp *NewPayloadResponseExpectObject) ExpectNoError() { if exp.Error != nil { - exp.Fatalf("FAIL (%s): Expected no error on EngineNewPayloadV1: error=%v", exp.TestName, exp.Error) + exp.Fatalf("FAIL (%s): Expected no error on EngineNewPayloadV%d: error=%v, payload=%s", exp.TestName, exp.Version, exp.Error, exp.PayloadJson()) } } func (exp *NewPayloadResponseExpectObject) ExpectError() { if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on EngineNewPayloadV1: status=%v", exp.TestName, exp.Status) + exp.Fatalf("FAIL (%s): Expected error on EngineNewPayloadV%d: status=%v, payload=%s", exp.TestName, exp.Version, exp.Status, exp.PayloadJson()) } } func (exp *NewPayloadResponseExpectObject) ExpectErrorCode(code int) { - // TODO: Actually check error code - if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on EngineNewPayloadV1: status=%v", exp.TestName, exp.Status) + exp.ExpectError() + if exp.ErrorCode != code { + exp.Fatalf("FAIL (%s): Expected error code on EngineNewPayloadV%d: want=%d, got=%d", exp.TestName, exp.Version, code, exp.ErrorCode) } } func (exp *NewPayloadResponseExpectObject) ExpectNoValidationError() { if exp.Status.ValidationError != nil { - exp.Fatalf("FAIL (%s): Unexpected validation error on EngineNewPayloadV1: %v, expected=", exp.TestName, exp.Status.ValidationError) + exp.Fatalf("FAIL (%s): Unexpected validation error on EngineNewPayloadV%d: %v, expected=", exp.TestName, exp.Version, exp.Status.ValidationError) } } func (exp *NewPayloadResponseExpectObject) ExpectStatus(ps PayloadStatus) { exp.ExpectNoError() if PayloadStatus(exp.Status.Status) != ps { - exp.Fatalf("FAIL (%s): Unexpected status response on EngineNewPayloadV1: %v, expected=%v", exp.TestName, exp.Status.Status, ps) + exp.Fatalf("FAIL (%s): Unexpected status response on EngineNewPayloadV%d: %v, expected=%v, payload=%s", exp.TestName, exp.Version, exp.Status.Status, ps, exp.PayloadJson()) } } @@ -207,100 +281,138 @@ func (exp *NewPayloadResponseExpectObject) ExpectStatusEither(statuses ...Payloa } } - exp.Fatalf("FAIL (%s): Unexpected status response on EngineNewPayloadV1: %v, expected=%v", exp.TestName, exp.Status.Status, strings.Join(StatusesToString(statuses), ",")) + exp.Fatalf("FAIL (%s): Unexpected status response on EngineNewPayloadV%d: %v, expected=%v, payload=%s", exp.TestName, exp.Version, exp.Status.Status, strings.Join(StatusesToString(statuses), ","), exp.PayloadJson()) } func (exp *NewPayloadResponseExpectObject) ExpectLatestValidHash(lvh *common.Hash) { exp.ExpectNoError() if ((lvh == nil || exp.Status.LatestValidHash == nil) && exp.Status.LatestValidHash != lvh) || (lvh != nil && exp.Status.LatestValidHash != nil && *exp.Status.LatestValidHash != *lvh) { - exp.Fatalf("FAIL (%v): Unexpected LatestValidHash on EngineNewPayloadV1: %v, expected=%v", exp.TestName, exp.Status.LatestValidHash, lvh) + exp.Fatalf("FAIL (%v): Unexpected LatestValidHash on EngineNewPayloadV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Status.LatestValidHash, lvh) } } // GetPayloadV1 type GetPayloadResponseExpectObject struct { *ExpectEnv - Payload api.ExecutableDataV1 - Error error + Payload api.ExecutableData + BlockValue *big.Int + Version int + Error error + ErrorCode int } func (tec *TestEngineClient) TestEngineGetPayloadV1(payloadID *api.PayloadID) *GetPayloadResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() payload, err := tec.Engine.GetPayloadV1(ctx, payloadID) - return &GetPayloadResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, - Payload: payload, - Error: err, + ret := &GetPayloadResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, + Payload: payload, + Version: 1, + BlockValue: nil, + Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (tec *TestEngineClient) TestEngineGetPayloadV2(payloadID *api.PayloadID) *GetPayloadResponseExpectObject { + ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) + defer cancel() + payload, blockValue, err := tec.Engine.GetPayloadV2(ctx, payloadID) + ret := &GetPayloadResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, + Payload: payload, + Version: 2, + BlockValue: blockValue, + Error: err, + } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret +} + +func (tec *TestEngineClient) TestEngineGetPayload(payloadID *api.PayloadID, version int) *GetPayloadResponseExpectObject { + if version == 2 { + return tec.TestEngineGetPayloadV2(payloadID) + } + return tec.TestEngineGetPayloadV1(payloadID) } func (exp *GetPayloadResponseExpectObject) ExpectNoError() { if exp.Error != nil { - exp.Fatalf("FAIL (%s): Expected no error on EngineGetPayloadV1: error=%v", exp.TestName, exp.Error) + exp.Fatalf("FAIL (%s): Expected no error on EngineGetPayloadV%d: error=%v", exp.TestName, exp.Version, exp.Error) } } func (exp *GetPayloadResponseExpectObject) ExpectError() { if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on EngineGetPayloadV1: payload=%v", exp.TestName, exp.Payload) + exp.Fatalf("FAIL (%s): Expected error on EngineGetPayloadV%d: payload=%v", exp.TestName, exp.Version, exp.Payload) } } func (exp *GetPayloadResponseExpectObject) ExpectErrorCode(code int) { - // TODO: Actually check error code - if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on EngineGetPayloadV1: payload=%v", exp.TestName, exp.Payload) + exp.ExpectError() + if exp.ErrorCode != code { + exp.Fatalf("FAIL (%s): Expected error code on EngineGetPayloadV%d: want=%d, got=%d", exp.TestName, exp.Version, code, exp.ErrorCode) } } func (exp *GetPayloadResponseExpectObject) ExpectPayloadParentHash(expectedParentHash common.Hash) { exp.ExpectNoError() if exp.Payload.ParentHash != expectedParentHash { - exp.Fatalf("FAIL (%s): Unexpected parent hash for payload on EngineGetPayloadV1: %v, expected=%v", exp.TestName, exp.Payload.ParentHash, expectedParentHash) + exp.Fatalf("FAIL (%s): Unexpected parent hash for payload on EngineGetPayloadV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Payload.ParentHash, expectedParentHash) } } func (exp *GetPayloadResponseExpectObject) ExpectPayloadBlockNumber(expectedBlockNumber uint64) { exp.ExpectNoError() if exp.Payload.Number != expectedBlockNumber { - exp.Fatalf("FAIL (%s): Unexpected block number for payload on EngineGetPayloadV1: %v, expected=%v", exp.TestName, exp.Payload.Number, expectedBlockNumber) + exp.Fatalf("FAIL (%s): Unexpected block number for payload on EngineGetPayloadV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Payload.Number, expectedBlockNumber) } } func (exp *GetPayloadResponseExpectObject) ExpectPrevRandao(expectedPrevRandao common.Hash) { exp.ExpectNoError() if exp.Payload.Random != expectedPrevRandao { - exp.Fatalf("FAIL (%s): Unexpected prevRandao for payload on EngineGetPayloadV1: %v, expected=%v", exp.TestName, exp.Payload.Random, expectedPrevRandao) + exp.Fatalf("FAIL (%s): Unexpected prevRandao for payload on EngineGetPayloadV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Payload.Random, expectedPrevRandao) } } func (exp *GetPayloadResponseExpectObject) ExpectTimestamp(expectedTimestamp uint64) { exp.ExpectNoError() if exp.Payload.Timestamp != expectedTimestamp { - exp.Fatalf("FAIL (%s): Unexpected timestamp for payload on EngineGetPayloadV1: %v, expected=%v", exp.TestName, exp.Payload.Timestamp, expectedTimestamp) + exp.Fatalf("FAIL (%s): Unexpected timestamp for payload on EngineGetPayloadV%d: %v, expected=%v", exp.TestName, exp.Version, exp.Payload.Timestamp, expectedTimestamp) } } // BlockNumber type BlockNumberResponseExpectObject struct { *ExpectEnv - Call string - Number uint64 - Error error + Call string + Number uint64 + Error error + ErrorCode int } func (tec *TestEngineClient) TestBlockNumber() *BlockNumberResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() number, err := tec.Engine.BlockNumber(ctx) - return &BlockNumberResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &BlockNumberResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "BlockNumber", Number: number, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret } func (exp *BlockNumberResponseExpectObject) ExpectNoError() { @@ -320,21 +432,26 @@ func (exp *BlockNumberResponseExpectObject) ExpectNumber(number uint64) { type HeaderResponseExpectObject struct { *ExpectEnv - Call string - Header *types.Header - Error error + Call string + Header *types.Header + Error error + ErrorCode int } func (tec *TestEngineClient) TestHeaderByNumber(number *big.Int) *HeaderResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() header, err := tec.Engine.HeaderByNumber(ctx, number) - return &HeaderResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &HeaderResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "HeaderByNumber", Header: header, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret } func (exp *HeaderResponseExpectObject) ExpectNoError() { @@ -350,9 +467,9 @@ func (exp *HeaderResponseExpectObject) ExpectError() { } func (exp *HeaderResponseExpectObject) ExpectErrorCode(code int) { - // TODO: Actually check error code - if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on %s: header=%v", exp.TestName, exp.Call, exp.Header) + exp.ExpectError() + if exp.ErrorCode != code { + exp.Fatalf("FAIL (%s): Expected error code on %s: want=%d, got=%d", exp.TestName, exp.Call, code, exp.ErrorCode) } } @@ -367,33 +484,42 @@ func (exp *HeaderResponseExpectObject) ExpectHash(expHash common.Hash) { type BlockResponseExpectObject struct { *ExpectEnv - Call string - Block *types.Block - Error error + Call string + Block *types.Block + Error error + ErrorCode int } func (tec *TestEngineClient) TestBlockByNumber(number *big.Int) *BlockResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() block, err := tec.Engine.BlockByNumber(ctx, number) - return &BlockResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &BlockResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "BlockByNumber", Block: block, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret } func (tec *TestEngineClient) TestBlockByHash(hash common.Hash) *BlockResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() block, err := tec.Engine.BlockByHash(ctx, hash) - return &BlockResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &BlockResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "BlockByHash", Block: block, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret } func (exp *BlockResponseExpectObject) ExpectError() { @@ -403,9 +529,9 @@ func (exp *BlockResponseExpectObject) ExpectError() { } func (exp *BlockResponseExpectObject) ExpectErrorCode(code int) { - // TODO: Actually check error code - if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on %s: block=%v", exp.TestName, exp.Call, exp.Block) + exp.ExpectError() + if exp.ErrorCode != code { + exp.Fatalf("FAIL (%s): Expected error code on %s: want=%d, got=%d", exp.TestName, exp.Call, code, exp.ErrorCode) } } @@ -422,6 +548,15 @@ func (exp *BlockResponseExpectObject) ExpectHash(expHash common.Hash) { } } +func (exp *BlockResponseExpectObject) ExpectWithdrawalsRoot(expectedRoot *common.Hash) { + exp.ExpectNoError() + actualWithdrawalsRoot := exp.Block.Header().WithdrawalsHash + if ((expectedRoot == nil || actualWithdrawalsRoot == nil) && actualWithdrawalsRoot != expectedRoot) || + (expectedRoot != nil && actualWithdrawalsRoot != nil && *actualWithdrawalsRoot != *expectedRoot) { + exp.Fatalf("FAIL (%s): Unexpected WithdrawalsRoot on %s: %v, expected=%v", exp.TestName, exp.Call, actualWithdrawalsRoot, expectedRoot) + } +} + func (exp *BlockResponseExpectObject) ExpectTransactionCountEqual(expTxCount int) { exp.ExpectNoError() if len(exp.Block.Transactions()) != expTxCount { @@ -447,21 +582,30 @@ func (exp *BlockResponseExpectObject) ExpectCoinbase(expCoinbase common.Address) type BalanceResponseExpectObject struct { *ExpectEnv - Call string - Balance *big.Int - Error error + Call string + Account common.Address + Block *big.Int + Balance *big.Int + Error error + ErrorCode int } func (tec *TestEngineClient) TestBalanceAt(account common.Address, number *big.Int) *BalanceResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() balance, err := tec.Engine.BalanceAt(ctx, account, number) - return &BalanceResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &BalanceResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "BalanceAt", + Account: account, Balance: balance, + Block: number, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret } func (exp *BalanceResponseExpectObject) ExpectNoError() { @@ -471,10 +615,17 @@ func (exp *BalanceResponseExpectObject) ExpectNoError() { } func (exp *BalanceResponseExpectObject) ExpectBalanceEqual(expBalance *big.Int) { + exp.Logf("INFO (%s): Testing balance for account %s on block %d: actual=%d, expected=%d", + exp.TestName, + exp.Account, + exp.Block, + exp.Balance, + expBalance, + ) exp.ExpectNoError() if ((expBalance == nil || exp.Balance == nil) && expBalance != exp.Balance) || (expBalance != nil && exp.Balance != nil && expBalance.Cmp(exp.Balance) != 0) { - exp.Fatalf("FAIL (%s): Unexpected balance on %s: %v, expected=%v", exp.TestName, exp.Call, exp.Balance, expBalance) + exp.Fatalf("FAIL (%s): Unexpected balance on %s, for account %s at block %v: %v, expected=%v", exp.TestName, exp.Call, exp.Account, exp.Block, exp.Balance, expBalance) } } @@ -482,21 +633,26 @@ func (exp *BalanceResponseExpectObject) ExpectBalanceEqual(expBalance *big.Int) type StorageResponseExpectObject struct { *ExpectEnv - Call string - Storage []byte - Error error + Call string + Storage []byte + Error error + ErrorCode int } func (tec *TestEngineClient) TestStorageAt(account common.Address, key common.Hash, number *big.Int) *StorageResponseExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() storage, err := tec.Engine.StorageAt(ctx, account, key, number) - return &StorageResponseExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &StorageResponseExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "StorageAt", Storage: storage, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret } func (exp *StorageResponseExpectObject) ExpectNoError() { @@ -525,21 +681,26 @@ func (exp *StorageResponseExpectObject) ExpectStorageEqual(expStorage common.Has // Transaction Receipt type TransactionReceiptExpectObject struct { *ExpectEnv - Call string - Receipt *types.Receipt - Error error + Call string + Receipt *types.Receipt + Error error + ErrorCode int } func (tec *TestEngineClient) TestTransactionReceipt(txHash common.Hash) *TransactionReceiptExpectObject { ctx, cancel := context.WithTimeout(tec.TestContext, globals.RPCTimeout) defer cancel() receipt, err := tec.Engine.TransactionReceipt(ctx, txHash) - return &TransactionReceiptExpectObject{ - ExpectEnv: &ExpectEnv{tec.Env}, + ret := &TransactionReceiptExpectObject{ + ExpectEnv: &ExpectEnv{Env: tec.Env}, Call: "TransactionReceipt", Receipt: receipt, Error: err, } + if err, ok := err.(rpc.Error); ok { + ret.ErrorCode = err.ErrorCode() + } + return ret } func (exp *TransactionReceiptExpectObject) ExpectError() { @@ -549,9 +710,9 @@ func (exp *TransactionReceiptExpectObject) ExpectError() { } func (exp *TransactionReceiptExpectObject) ExpectErrorCode(code int) { - // TODO: Actually check error code - if exp.Error == nil { - exp.Fatalf("FAIL (%s): Expected error on %s: block=%v", exp.TestName, exp.Call, exp.Receipt) + exp.ExpectError() + if exp.ErrorCode != code { + exp.Fatalf("FAIL (%s): Expected error code on %s: want=%d, got=%d", exp.TestName, exp.Call, code, exp.ErrorCode) } } diff --git a/simulators/ethereum/engine/test/spec.go b/simulators/ethereum/engine/test/spec.go index af5583ba08..0d8c906133 100644 --- a/simulators/ethereum/engine/test/spec.go +++ b/simulators/ethereum/engine/test/spec.go @@ -1,11 +1,40 @@ package test import ( + "fmt" "math/big" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/hive/simulators/ethereum/engine/clmock" "github.com/ethereum/hive/simulators/ethereum/engine/helper" ) +type ForkConfig struct { + // Shanghai Fork Timestamp + ShanghaiTimestamp *big.Int +} + +type ConsensusConfig struct { + SlotsToSafe *big.Int + SlotsToFinalized *big.Int + SafeSlotsToImportOptimistically int64 +} + +type SpecInterface interface { + Execute(*Env) + ConfigureCLMock(*clmock.CLMocker) + GetAbout() string + GetConsensusConfig() ConsensusConfig + GetChainFile() string + GetForkConfig() ForkConfig + GetGenesis() *core.Genesis + GetName() string + GetTestTransactionType() helper.TestTransactionType + GetTimeout() int + GetTTD() int64 + IsMiningDisabled() bool +} + type Spec struct { // Name of the test Name string @@ -46,4 +75,63 @@ type Spec struct { // Transaction type to use throughout the test TestTransactionType helper.TestTransactionType + + // Fork Config + ForkConfig +} + +func (s Spec) Execute(env *Env) { + s.Run(env) +} + +func (s Spec) ConfigureCLMock(*clmock.CLMocker) { + // No-op +} + +func (s Spec) GetAbout() string { + return s.About +} + +func (s Spec) GetConsensusConfig() ConsensusConfig { + return ConsensusConfig{ + SlotsToSafe: s.SlotsToSafe, + SlotsToFinalized: s.SlotsToFinalized, + SafeSlotsToImportOptimistically: s.SafeSlotsToImportOptimistically, + } +} +func (s Spec) GetChainFile() string { + return s.ChainFile +} + +func (s Spec) GetForkConfig() ForkConfig { + return s.ForkConfig +} + +func (s Spec) GetGenesis() *core.Genesis { + genesisPath := "./init/genesis.json" + if s.GenesisFile != "" { + genesisPath = fmt.Sprintf("./init/%s", s.GenesisFile) + } + genesis := helper.LoadGenesis(genesisPath) + return &genesis +} + +func (s Spec) GetName() string { + return s.Name +} + +func (s Spec) GetTestTransactionType() helper.TestTransactionType { + return s.TestTransactionType +} + +func (s Spec) GetTimeout() int { + return s.TimeoutSeconds +} + +func (s Spec) GetTTD() int64 { + return s.TTD +} + +func (s Spec) IsMiningDisabled() bool { + return s.DisableMining }