11package main
22
33import (
4+ "context"
5+ "encoding/hex"
46 "encoding/json"
57 "fmt"
68 "math/big"
9+ "net"
10+ "net/http"
11+ "time"
712
813 "github.com/ethereum/go-ethereum/common"
914 "github.com/ethereum/go-ethereum/common/hexutil"
15+ "github.com/ethereum/go-ethereum/core/types"
16+ "github.com/ethereum/go-ethereum/ethclient"
17+ "github.com/ethereum/go-ethereum/rpc"
18+ "github.com/ethereum/hive/hivesim"
19+ "github.com/golang-jwt/jwt/v4"
1020)
1121
22+ // https://github.com/ethereum/execution-apis/blob/v1.0.0-alpha.7/src/engine/specification.md
23+ var EthPortHTTP = 8545
24+ var EnginePortHTTP = 8551
25+
26+ // EngineClient wrapper for Ethereum Engine RPC for testing purposes.
27+ type EngineClient struct {
28+ * hivesim.T
29+ * hivesim.Client
30+ c * rpc.Client
31+ Eth * ethclient.Client
32+ cEth * rpc.Client
33+ IP net.IP
34+ TerminalTotalDifficulty * big.Int
35+
36+ // This holds most recent context created by the Ctx method.
37+ // Every time Ctx is called, it creates a new context with the default
38+ // timeout and cancels the previous one.
39+ lastCtx context.Context
40+ lastCancel context.CancelFunc
41+ }
42+
43+ // NewClient creates a engine client that uses the given RPC client.
44+ func NewEngineClient (t * hivesim.T , userRPCAddress string , engineRPCAddress string , ttd * big.Int ) * EngineClient {
45+ client := & http.Client {}
46+ // Prepare HTTP Client
47+ rpcHttpClient , _ := rpc .DialHTTPWithClient (engineRPCAddress , client )
48+
49+ // Prepare ETH Client
50+ client = & http.Client {}
51+
52+ rpcClient , _ := rpc .DialHTTPWithClient (userRPCAddress , client )
53+ eth := ethclient .NewClient (rpcClient )
54+ return & EngineClient {
55+ T : t ,
56+ c : rpcHttpClient ,
57+ Eth : eth ,
58+ cEth : rpcClient ,
59+ TerminalTotalDifficulty : ttd ,
60+ lastCtx : nil ,
61+ lastCancel : nil ,
62+ }
63+ }
64+
65+ func (ec * EngineClient ) Close () {
66+ ec .c .Close ()
67+ ec .Eth .Close ()
68+ if ec .lastCtx != nil {
69+ ec .lastCancel ()
70+ }
71+ }
72+
73+ func (ec * EngineClient ) Ctx () context.Context {
74+ if ec .lastCtx != nil {
75+ ec .lastCancel ()
76+ }
77+ ec .lastCtx , ec .lastCancel = context .WithTimeout (context .Background (), 10 * time .Second )
78+ return ec .lastCtx
79+ }
80+
81+ // Helper structs to fetch the TotalDifficulty
82+ type TD struct {
83+ TotalDifficulty * hexutil.Big `json:"totalDifficulty"`
84+ }
85+ type TotalDifficultyHeader struct {
86+ types.Header
87+ TD
88+ }
89+
90+ func (tdh * TotalDifficultyHeader ) UnmarshalJSON (data []byte ) error {
91+ if err := json .Unmarshal (data , & tdh .Header ); err != nil {
92+ return err
93+ }
94+ if err := json .Unmarshal (data , & tdh .TD ); err != nil {
95+ return err
96+ }
97+ return nil
98+ }
99+
100+ func (ec * EngineClient ) checkTTD () (* types.Header , bool ) {
101+ var td * TotalDifficultyHeader
102+ if err := ec .cEth .CallContext (ec .Ctx (), & td , "eth_getBlockByNumber" , "latest" , false ); err != nil {
103+ panic (err )
104+ }
105+ if td .TotalDifficulty .ToInt ().Cmp (ec .TerminalTotalDifficulty ) >= 0 {
106+ return & td .Header , true
107+ }
108+ return nil , false
109+ }
110+
111+ // Engine API Types
112+ type PayloadStatusV1 struct {
113+ Status PayloadStatus `json:"status"`
114+ LatestValidHash * common.Hash `json:"latestValidHash"`
115+ ValidationError * string `json:"validationError"`
116+ }
117+ type ForkChoiceResponse struct {
118+ PayloadStatus PayloadStatusV1 `json:"payloadStatus"`
119+ PayloadID * PayloadID `json:"payloadId"`
120+ }
121+
122+ type PayloadStatus int
123+
124+ const (
125+ Unknown PayloadStatus = iota
126+ Valid
127+ Invalid
128+ Accepted
129+ Syncing
130+ InvalidBlockHash
131+ )
132+
133+ var PayloadStatuses = map [PayloadStatus ]string {
134+ Valid : "VALID" ,
135+ Invalid : "INVALID" ,
136+ Accepted : "ACCEPTED" ,
137+ Syncing : "SYNCING" ,
138+ InvalidBlockHash : "INVALID_BLOCK_HASH" ,
139+ }
140+
141+ func (b PayloadStatus ) String () string {
142+ str , ok := PayloadStatuses [b ]
143+ if ! ok {
144+ return "UNKNOWN"
145+ }
146+ return str
147+ }
148+
149+ func (b PayloadStatus ) MarshalText () ([]byte , error ) {
150+ str , ok := PayloadStatuses [b ]
151+ if ! ok {
152+ return nil , fmt .Errorf ("invalid payload status" )
153+ }
154+ return []byte (str ), nil
155+ }
156+
157+ func (b * PayloadStatus ) UnmarshalText (input []byte ) error {
158+ s := string (input )
159+ for p , status := range PayloadStatuses {
160+ if status == s {
161+ * b = p
162+ return nil
163+ }
164+ }
165+ return fmt .Errorf ("invalid payload status: %s" , s )
166+ }
167+
168+ type PayloadID [8 ]byte
169+
170+ func (b PayloadID ) MarshalText () ([]byte , error ) {
171+ return hexutil .Bytes (b [:]).MarshalText ()
172+ }
173+ func (b * PayloadID ) UnmarshalText (input []byte ) error {
174+ err := hexutil .UnmarshalFixedText ("PayloadID" , input , b [:])
175+ if err != nil {
176+ return fmt .Errorf ("invalid payload id %q: %w" , input , err )
177+ }
178+ return nil
179+ }
180+
181+ type ForkchoiceStateV1 struct {
182+ HeadBlockHash common.Hash `json:"headBlockHash"`
183+ SafeBlockHash common.Hash `json:"safeBlockHash"`
184+ FinalizedBlockHash common.Hash `json:"finalizedBlockHash"`
185+ }
186+
12187//go:generate go run github.com/fjl/gencodec -type ExecutableDataV1 -field-override executableDataMarshaling -out gen_ed.go
13188// ExecutableDataV1 structure described at https://github.com/ethereum/execution-apis/src/engine/specification.md
14189type ExecutableDataV1 struct {
@@ -40,6 +215,74 @@ type executableDataMarshaling struct {
40215 Transactions []hexutil.Bytes
41216}
42217
218+ //go:generate go run github.com/fjl/gencodec -type PayloadAttributesV1 -field-override payloadAttributesMarshaling -out gen_blockparams.go
219+ // PayloadAttributesV1 structure described at https://github.com/ethereum/execution-apis/pull/74
220+ type PayloadAttributesV1 struct {
221+ Timestamp uint64 `json:"timestamp" gencodec:"required"`
222+ PrevRandao common.Hash `json:"prevRandao" gencodec:"required"`
223+ SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"`
224+ }
225+
226+ // JSON type overrides for PayloadAttributesV1.
227+ type payloadAttributesMarshaling struct {
228+ Timestamp hexutil.Uint64
229+ }
230+
231+ // JWT Tokens
232+ func GetNewToken (jwtSecretBytes []byte , iat time.Time ) (string , error ) {
233+ newToken := jwt .NewWithClaims (jwt .SigningMethodHS256 , jwt.MapClaims {
234+ "iat" : iat .Unix (),
235+ })
236+ tokenString , err := newToken .SignedString (jwtSecretBytes )
237+ if err != nil {
238+ return "" , err
239+ }
240+ return tokenString , nil
241+ }
242+
243+ func (ec * EngineClient ) PrepareAuthCallToken (jwtSecretBytes []byte , iat time.Time ) error {
244+ newTokenString , err := GetNewToken (jwtSecretBytes , iat )
245+ if err != nil {
246+ return err
247+ }
248+ ec .c .SetHeader ("Authorization" , fmt .Sprintf ("Bearer %s" , newTokenString ))
249+ return nil
250+ }
251+
252+ func (ec * EngineClient ) PrepareDefaultAuthCallToken () error {
253+ secret , _ := hex .DecodeString ("7365637265747365637265747365637265747365637265747365637265747365" )
254+ ec .PrepareAuthCallToken (secret , time .Now ())
255+ return nil
256+ }
257+
258+ // Engine API Call Methods
259+ func (ec * EngineClient ) EngineForkchoiceUpdatedV1 (fcState * ForkchoiceStateV1 , pAttributes * PayloadAttributesV1 ) (ForkChoiceResponse , error ) {
260+ var result ForkChoiceResponse
261+ if err := ec .PrepareDefaultAuthCallToken (); err != nil {
262+ return result , err
263+ }
264+ err := ec .c .CallContext (ec .Ctx (), & result , "engine_forkchoiceUpdatedV1" , fcState , pAttributes )
265+ return result , err
266+ }
267+
268+ func (ec * EngineClient ) EngineGetPayloadV1 (payloadId * PayloadID ) (ExecutableDataV1 , error ) {
269+ var result ExecutableDataV1
270+ if err := ec .PrepareDefaultAuthCallToken (); err != nil {
271+ return result , err
272+ }
273+ err := ec .c .CallContext (ec .Ctx (), & result , "engine_getPayloadV1" , payloadId )
274+ return result , err
275+ }
276+
277+ func (ec * EngineClient ) EngineNewPayloadV1 (payload * ExecutableDataV1 ) (PayloadStatusV1 , error ) {
278+ var result PayloadStatusV1
279+ if err := ec .PrepareDefaultAuthCallToken (); err != nil {
280+ return result , err
281+ }
282+ err := ec .c .CallContext (ec .Ctx (), & result , "engine_newPayloadV1" , payload )
283+ return result , err
284+ }
285+
43286// Json helpers
44287// A value of this type can a JSON-RPC request, notification, successful response or
45288// error response. Which one it is depends on the fields.
@@ -65,7 +308,7 @@ type jsonrpcMessage struct {
65308 Result json.RawMessage `json:"result,omitempty"`
66309}
67310
68- func UnmarshalFromJsonRPC (b []byte , result interface {}) error {
311+ func UnmarshalFromJsonRPCResponse (b []byte , result interface {}) error {
69312 var rpcMessage jsonrpcMessage
70313 err := json .Unmarshal (b , & rpcMessage )
71314 if err != nil {
@@ -77,3 +320,16 @@ func UnmarshalFromJsonRPC(b []byte, result interface{}) error {
77320 err = json .Unmarshal (rpcMessage .Result , & result )
78321 return nil
79322}
323+
324+ func UnmarshalFromJsonRPCRequest (b []byte , params ... interface {}) error {
325+ var rpcMessage jsonrpcMessage
326+ err := json .Unmarshal (b , & rpcMessage )
327+ if err != nil {
328+ return err
329+ }
330+ if rpcMessage .Error != nil {
331+ return rpcMessage .Error
332+ }
333+ err = json .Unmarshal (rpcMessage .Params , & params )
334+ return nil
335+ }
0 commit comments