@@ -5,6 +5,7 @@ package test
55import (
66 "context"
77 "encoding/hex"
8+ "encoding/json"
89 "fmt"
910 mathrand "math/rand"
1011 "net/http"
@@ -79,51 +80,115 @@ func SetupTestRethNode(t testing.TB, client types.TastoraDockerClient, networkID
7980// waitForRethContainer waits for the Reth container to be ready by polling the provided endpoints with JWT authentication.
8081func waitForRethContainer (t testing.TB , jwtSecret , ethURL , engineURL string ) error {
8182 t .Helper ()
82- client := & http.Client {Timeout : 100 * time .Millisecond }
83+
84+ secret , err := decodeSecret (jwtSecret )
85+ if err != nil {
86+ return err
87+ }
88+ authToken , err := getAuthToken (secret )
89+ if err != nil {
90+ return err
91+ }
92+
93+ client := & http.Client {Timeout : 500 * time .Millisecond }
8394 timer := time .NewTimer (30 * time .Second )
8495 defer timer .Stop ()
96+ var lastErr error
8597 for {
8698 select {
8799 case <- timer .C :
100+ if lastErr != nil {
101+ return fmt .Errorf ("timeout waiting for reth container to be ready: %w" , lastErr )
102+ }
88103 return fmt .Errorf ("timeout waiting for reth container to be ready" )
89104 default :
90- rpcReq := strings .NewReader (`{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}` )
91- resp , err := client .Post (ethURL , "application/json" , rpcReq )
105+ genesisHash , err := getGenesisHash (client , ethURL )
106+ if err == nil {
107+ err = waitForEngineForkchoice (client , engineURL , authToken , genesisHash )
108+ }
92109 if err == nil {
93- if err := resp .Body .Close (); err != nil {
94- return fmt .Errorf ("failed to close response body: %w" , err )
95- }
96- if resp .StatusCode == http .StatusOK {
97- req , err := http .NewRequest ("POST" , engineURL , strings .NewReader (`{"jsonrpc":"2.0","method":"engine_getClientVersionV1","params":[],"id":1}` ))
98- if err != nil {
99- return err
100- }
101- req .Header .Set ("Content-Type" , "application/json" )
102- secret , err := decodeSecret (jwtSecret )
103- if err != nil {
104- return err
105- }
106- authToken , err := getAuthToken (secret )
107- if err != nil {
108- return err
109- }
110- req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , authToken ))
111- resp , err := client .Do (req )
112- if err == nil {
113- if err := resp .Body .Close (); err != nil {
114- return fmt .Errorf ("failed to close response body: %w" , err )
115- }
116- if resp .StatusCode == http .StatusOK {
117- return nil
118- }
119- }
120- }
110+ return nil
121111 }
112+ lastErr = err
122113 time .Sleep (100 * time .Millisecond )
123114 }
124115 }
125116}
126117
118+ type jsonRPCError struct {
119+ Code int `json:"code"`
120+ Message string `json:"message"`
121+ }
122+
123+ func getGenesisHash (client * http.Client , ethURL string ) (string , error ) {
124+ rpcReq := strings .NewReader (`{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["0x0",false],"id":1}` )
125+ resp , err := client .Post (ethURL , "application/json" , rpcReq )
126+ if err != nil {
127+ return "" , err
128+ }
129+ defer func () { _ = resp .Body .Close () }()
130+
131+ if resp .StatusCode != http .StatusOK {
132+ return "" , fmt .Errorf ("eth endpoint returned status %d" , resp .StatusCode )
133+ }
134+
135+ var rpcResp struct {
136+ Result struct {
137+ Hash string `json:"hash"`
138+ } `json:"result"`
139+ Error * jsonRPCError `json:"error,omitempty"`
140+ }
141+ if err := json .NewDecoder (resp .Body ).Decode (& rpcResp ); err != nil {
142+ return "" , fmt .Errorf ("decode genesis block response: %w" , err )
143+ }
144+ if rpcResp .Error != nil {
145+ return "" , fmt .Errorf ("eth_getBlockByNumber failed: %s" , rpcResp .Error .Message )
146+ }
147+ if rpcResp .Result .Hash == "" {
148+ return "" , fmt .Errorf ("eth_getBlockByNumber returned empty genesis hash" )
149+ }
150+ return rpcResp .Result .Hash , nil
151+ }
152+
153+ func waitForEngineForkchoice (client * http.Client , engineURL , authToken , genesisHash string ) error {
154+ body := fmt .Sprintf (`{"jsonrpc":"2.0","method":"engine_forkchoiceUpdatedV3","params":[{"headBlockHash":%q,"safeBlockHash":%q,"finalizedBlockHash":%q},null],"id":1}` , genesisHash , genesisHash , genesisHash )
155+ req , err := http .NewRequest ("POST" , engineURL , strings .NewReader (body ))
156+ if err != nil {
157+ return err
158+ }
159+ req .Header .Set ("Content-Type" , "application/json" )
160+ req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , authToken ))
161+
162+ resp , err := client .Do (req )
163+ if err != nil {
164+ return err
165+ }
166+ defer func () { _ = resp .Body .Close () }()
167+
168+ if resp .StatusCode != http .StatusOK {
169+ return fmt .Errorf ("engine endpoint returned status %d" , resp .StatusCode )
170+ }
171+
172+ var rpcResp struct {
173+ Result struct {
174+ PayloadStatus struct {
175+ Status string `json:"status"`
176+ } `json:"payloadStatus"`
177+ } `json:"result"`
178+ Error * jsonRPCError `json:"error,omitempty"`
179+ }
180+ if err := json .NewDecoder (resp .Body ).Decode (& rpcResp ); err != nil {
181+ return fmt .Errorf ("decode engine forkchoice response: %w" , err )
182+ }
183+ if rpcResp .Error != nil {
184+ return fmt .Errorf ("engine_forkchoiceUpdatedV3 failed: %s" , rpcResp .Error .Message )
185+ }
186+ if rpcResp .Result .PayloadStatus .Status != "VALID" {
187+ return fmt .Errorf ("engine forkchoice status %s" , rpcResp .Result .PayloadStatus .Status )
188+ }
189+ return nil
190+ }
191+
127192// decodeSecret decodes a hex-encoded JWT secret string into a byte slice.
128193func decodeSecret (jwtSecret string ) ([]byte , error ) {
129194 secret , err := hex .DecodeString (strings .TrimPrefix (jwtSecret , "0x" ))
0 commit comments