From 80e67a75d166078dfc6d84bc6700d7d6b743054c Mon Sep 17 00:00:00 2001 From: healthykim Date: Wed, 15 Apr 2026 17:20:55 +0200 Subject: [PATCH] cmd/devp2p: build forkchoice update in code --- cmd/devp2p/internal/ethtest/engine.go | 70 ++++++++++++++++++--------- cmd/devp2p/internal/ethtest/suite.go | 5 +- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/cmd/devp2p/internal/ethtest/engine.go b/cmd/devp2p/internal/ethtest/engine.go index 0e94efa5bd..8c28199906 100644 --- a/cmd/devp2p/internal/ethtest/engine.go +++ b/cmd/devp2p/internal/ethtest/engine.go @@ -18,31 +18,32 @@ package ethtest import ( "bytes" + "encoding/json" "fmt" - "io" "net/http" - "os" - "path/filepath" "time" + "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/golang-jwt/jwt/v4" ) // EngineClient is a wrapper around engine-related data. type EngineClient struct { - url string - jwt [32]byte - headfcu []byte + url string + jwt [32]byte + chain *Chain + http *http.Client } // NewEngineClient creates a new engine client. -func NewEngineClient(dir, url, jwt string) (*EngineClient, error) { - headfcu, err := os.ReadFile(filepath.Join(dir, "headfcu.json")) - if err != nil { - return nil, fmt.Errorf("failed to read headfcu: %w", err) +func NewEngineClient(url, jwtSecret string, chain *Chain) *EngineClient { + return &EngineClient{ + url: url, + jwt: common.HexToHash(jwtSecret), + chain: chain, + http: &http.Client{Timeout: 10 * time.Second}, } - return &EngineClient{url, common.HexToHash(jwt), headfcu}, nil } // token returns the jwt claim token for authorization. @@ -52,18 +53,41 @@ func (ec *EngineClient) token() string { return token } -// sendForkchoiceUpdated sends an fcu for the head of the generated chain. -func (ec *EngineClient) sendForkchoiceUpdated() error { - var ( - req, _ = http.NewRequest(http.MethodPost, ec.url, io.NopCloser(bytes.NewReader(ec.headfcu))) - header = make(http.Header) - ) - // Set header - header.Set("accept", "application/json") - header.Set("content-type", "application/json") - header.Set("Authorization", fmt.Sprintf("Bearer %v", ec.token())) - req.Header = header +// rpcRequest marshals a JSON-RPC 2.0 request body for the given method and params. +func rpcRequest(method string, params ...any) ([]byte, error) { + p, err := json.Marshal(params) + if err != nil { + return nil, err + } + return fmt.Appendf(nil, `{"jsonrpc":"2.0","id":1,"method":%q,"params":%s}`, method, p), nil +} - _, err := new(http.Client).Do(req) +// call sends an authenticated Engine API JSON-RPC request. Response body is +// not inspected — only transport errors are returned. +func (ec *EngineClient) call(method string, params ...any) error { + body, err := rpcRequest(method, params...) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, ec.url, bytes.NewReader(body)) + if err != nil { + return err + } + req.Header.Set("accept", "application/json") + req.Header.Set("content-type", "application/json") + req.Header.Set("Authorization", "Bearer "+ec.token()) + + _, err = ec.http.Do(req) return err } + +// sendForkchoiceUpdated sends an fcu for the head of the generated chain. +func (ec *EngineClient) sendForkchoiceUpdated() error { + head := ec.chain.Head().Hash() + state := engine.ForkchoiceStateV1{ + HeadBlockHash: head, + SafeBlockHash: head, + FinalizedBlockHash: head, + } + return ec.call("engine_forkchoiceUpdatedV3", state, nil) +} diff --git a/cmd/devp2p/internal/ethtest/suite.go b/cmd/devp2p/internal/ethtest/suite.go index 043e20f124..05b2cc7315 100644 --- a/cmd/devp2p/internal/ethtest/suite.go +++ b/cmd/devp2p/internal/ethtest/suite.go @@ -56,10 +56,7 @@ func NewSuite(dest *enode.Node, chainDir, engineURL, jwt string) (*Suite, error) if err != nil { return nil, err } - engine, err := NewEngineClient(chainDir, engineURL, jwt) - if err != nil { - return nil, err - } + engine := NewEngineClient(engineURL, jwt, chain) return &Suite{ Dest: dest,