Skip to content

Commit e95fc6e

Browse files
authored
Merge pull request #559 from ethpandaops/pk910/tracoor-integration
add tracoor download links to dora
2 parents 2d74fa0 + adacf22 commit e95fc6e

File tree

9 files changed

+494
-3
lines changed

9 files changed

+494
-3
lines changed

cmd/dora-explorer/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ func startFrontend(router *mux.Router) {
196196
router.HandleFunc("/slots", handlers.Slots).Methods("GET")
197197
router.HandleFunc("/slots/filtered", handlers.SlotsFiltered).Methods("GET")
198198
router.HandleFunc("/slot/{slotOrHash}", handlers.Slot).Methods("GET")
199+
router.HandleFunc("/slot/{slotOrHash}/tracoor", handlers.SlotTracoor).Methods("GET")
199200
router.HandleFunc("/slot/{root}/blob/{index}", handlers.SlotBlob).Methods("GET")
200201
router.HandleFunc("/blocks", handlers.Blocks).Methods("GET")
201202
router.HandleFunc("/block/{numberOrHash}", handlers.Block).Methods("GET")

config/default.config.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ frontend:
6363
# DAS Guardian configuration
6464
#disableDasGuardianCheck: false # Disable DAS Guardian completely (default: false - enabled)
6565
#enableDasGuardianMassScan: false # Enable mass DAS Guardian scanning (default: false - disabled)
66+
67+
# Tracoor cross references
68+
#tracoorUrl: "https://tracoor.sepolia.ethpandaops.io/"
69+
#tracoorNetwork: "sepolia"
6670

6771
beaconapi:
6872
# beacon node rpc endpoints

handlers/slot.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ func buildSlotPageData(ctx context.Context, blockSlot int64, blockRoot []byte) (
233233
Future: slot >= currentSlot,
234234
EpochFinalized: finalizedEpoch >= chainState.EpochOfSlot(slot),
235235
Badges: []*models.SlotPageBlockBadge{},
236+
TracoorUrl: utils.Config.Frontend.TracoorUrl,
236237
}
237238

238239
var epochStatsValues *beacon.EpochStatsValues

handlers/slot_tracoor.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
package handlers
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
"sync"
11+
"time"
12+
13+
"github.com/gorilla/mux"
14+
"github.com/sirupsen/logrus"
15+
16+
"github.com/ethpandaops/dora/types/models"
17+
"github.com/ethpandaops/dora/utils"
18+
)
19+
20+
// SlotTracoor handles the Tracoor data request for a slot
21+
func SlotTracoor(w http.ResponseWriter, r *http.Request) {
22+
w.Header().Set("Content-Type", "application/json")
23+
24+
tracoorUrl := utils.Config.Frontend.TracoorUrl
25+
if tracoorUrl == "" {
26+
http.Error(w, `{"error": "Tracoor not configured"}`, http.StatusNotFound)
27+
return
28+
}
29+
30+
// Remove trailing slash from tracoorUrl
31+
tracoorUrl = strings.TrimSuffix(tracoorUrl, "/")
32+
33+
tracoorNetwork := utils.Config.Frontend.TracoorNetwork
34+
if tracoorNetwork == "" {
35+
tracoorNetwork = "mainnet"
36+
}
37+
38+
// Get block root from URL path
39+
vars := mux.Vars(r)
40+
blockRoot := vars["slotOrHash"]
41+
42+
// Validate block root - must be a 0x-prefixed hex string (66 chars for 32 bytes)
43+
if !strings.HasPrefix(blockRoot, "0x") || len(blockRoot) != 66 {
44+
http.Error(w, `{"error": "block root required (not slot number)"}`, http.StatusBadRequest)
45+
return
46+
}
47+
48+
// Get state root and execution block hash from query string
49+
query := r.URL.Query()
50+
stateRoot := query.Get("stateRoot")
51+
executionBlockHash := query.Get("executionBlockHash")
52+
53+
result := models.SlotTracoorData{
54+
TracoorUrl: tracoorUrl,
55+
BeaconBlocks: []models.TracoorBeaconBlock{},
56+
BeaconStates: []models.TracoorBeaconState{},
57+
ExecutionBlockTraces: []models.TracoorExecutionBlockTrace{},
58+
}
59+
60+
client := &http.Client{
61+
Timeout: 10 * time.Second,
62+
}
63+
64+
var wg sync.WaitGroup
65+
var mu sync.Mutex
66+
67+
// Fetch beacon block traces if blockRoot is provided
68+
if blockRoot != "" {
69+
wg.Add(1)
70+
go func() {
71+
defer wg.Done()
72+
blockResp, err := fetchTracoorBeaconBlocks(client, tracoorUrl, tracoorNetwork, blockRoot)
73+
mu.Lock()
74+
defer mu.Unlock()
75+
if err != nil {
76+
result.BeaconBlocksError = err.Error()
77+
logrus.WithError(err).WithField("blockRoot", blockRoot).Debug("error fetching Tracoor beacon blocks")
78+
} else {
79+
result.BeaconBlocks = blockResp.BeaconBlocks
80+
}
81+
}()
82+
}
83+
84+
// Fetch beacon state traces if stateRoot is provided
85+
if stateRoot != "" {
86+
wg.Add(1)
87+
go func() {
88+
defer wg.Done()
89+
stateResp, err := fetchTracoorBeaconStates(client, tracoorUrl, tracoorNetwork, stateRoot)
90+
mu.Lock()
91+
defer mu.Unlock()
92+
if err != nil {
93+
result.BeaconStatesError = err.Error()
94+
logrus.WithError(err).WithField("stateRoot", stateRoot).Debug("error fetching Tracoor beacon states")
95+
} else {
96+
result.BeaconStates = stateResp.BeaconStates
97+
}
98+
}()
99+
}
100+
101+
// Fetch execution block traces if executionBlockHash is provided
102+
if executionBlockHash != "" {
103+
wg.Add(1)
104+
go func() {
105+
defer wg.Done()
106+
execResp, err := fetchTracoorExecutionBlockTraces(client, tracoorUrl, tracoorNetwork, executionBlockHash)
107+
mu.Lock()
108+
defer mu.Unlock()
109+
if err != nil {
110+
result.ExecutionBlockTraceErr = err.Error()
111+
logrus.WithError(err).WithField("executionBlockHash", executionBlockHash).Debug("error fetching Tracoor execution block traces")
112+
} else {
113+
result.ExecutionBlockTraces = execResp.ExecutionBlockTraces
114+
}
115+
}()
116+
}
117+
118+
wg.Wait()
119+
120+
if err := json.NewEncoder(w).Encode(result); err != nil {
121+
logrus.WithError(err).Error("error encoding Tracoor response")
122+
http.Error(w, `{"error": "internal server error"}`, http.StatusInternalServerError)
123+
}
124+
}
125+
126+
func fetchTracoorBeaconBlocks(client *http.Client, tracoorUrl, network, blockRoot string) (*models.TracoorBeaconBlockResponse, error) {
127+
reqBody := map[string]any{
128+
"network": network,
129+
"block_root": blockRoot,
130+
"pagination": map[string]any{
131+
"limit": 100,
132+
"offset": 0,
133+
"order_by": "fetched_at DESC",
134+
},
135+
}
136+
137+
bodyBytes, err := json.Marshal(reqBody)
138+
if err != nil {
139+
return nil, fmt.Errorf("failed to marshal request: %w", err)
140+
}
141+
142+
req, err := http.NewRequest("POST", tracoorUrl+"/v1/api/list-beacon-block", bytes.NewReader(bodyBytes))
143+
if err != nil {
144+
return nil, fmt.Errorf("failed to create request: %w", err)
145+
}
146+
req.Header.Set("Content-Type", "application/json")
147+
148+
resp, err := client.Do(req)
149+
if err != nil {
150+
return nil, fmt.Errorf("failed to fetch: %w", err)
151+
}
152+
defer resp.Body.Close()
153+
154+
if resp.StatusCode != http.StatusOK {
155+
body, _ := io.ReadAll(resp.Body)
156+
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
157+
}
158+
159+
var result models.TracoorBeaconBlockResponse
160+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
161+
return nil, fmt.Errorf("failed to decode response: %w", err)
162+
}
163+
164+
return &result, nil
165+
}
166+
167+
func fetchTracoorBeaconStates(client *http.Client, tracoorUrl, network, stateRoot string) (*models.TracoorBeaconStateResponse, error) {
168+
reqBody := map[string]any{
169+
"network": network,
170+
"state_root": stateRoot,
171+
"pagination": map[string]any{
172+
"limit": 100,
173+
"offset": 0,
174+
"order_by": "fetched_at DESC",
175+
},
176+
}
177+
178+
bodyBytes, err := json.Marshal(reqBody)
179+
if err != nil {
180+
return nil, fmt.Errorf("failed to marshal request: %w", err)
181+
}
182+
183+
req, err := http.NewRequest("POST", tracoorUrl+"/v1/api/list-beacon-state", bytes.NewReader(bodyBytes))
184+
if err != nil {
185+
return nil, fmt.Errorf("failed to create request: %w", err)
186+
}
187+
req.Header.Set("Content-Type", "application/json")
188+
189+
resp, err := client.Do(req)
190+
if err != nil {
191+
return nil, fmt.Errorf("failed to fetch: %w", err)
192+
}
193+
defer resp.Body.Close()
194+
195+
if resp.StatusCode != http.StatusOK {
196+
body, _ := io.ReadAll(resp.Body)
197+
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
198+
}
199+
200+
var result models.TracoorBeaconStateResponse
201+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
202+
return nil, fmt.Errorf("failed to decode response: %w", err)
203+
}
204+
205+
return &result, nil
206+
}
207+
208+
func fetchTracoorExecutionBlockTraces(client *http.Client, tracoorUrl, network, blockHash string) (*models.TracoorExecutionBlockTraceResponse, error) {
209+
reqBody := map[string]any{
210+
"network": network,
211+
"block_hash": blockHash,
212+
"pagination": map[string]any{
213+
"limit": 100,
214+
"offset": 0,
215+
"order_by": "fetched_at DESC",
216+
},
217+
}
218+
219+
bodyBytes, err := json.Marshal(reqBody)
220+
if err != nil {
221+
return nil, fmt.Errorf("failed to marshal request: %w", err)
222+
}
223+
224+
req, err := http.NewRequest("POST", tracoorUrl+"/v1/api/list-execution-block-trace", bytes.NewReader(bodyBytes))
225+
if err != nil {
226+
return nil, fmt.Errorf("failed to create request: %w", err)
227+
}
228+
req.Header.Set("Content-Type", "application/json")
229+
230+
resp, err := client.Do(req)
231+
if err != nil {
232+
return nil, fmt.Errorf("failed to fetch: %w", err)
233+
}
234+
defer resp.Body.Close()
235+
236+
if resp.StatusCode != http.StatusOK {
237+
body, _ := io.ReadAll(resp.Body)
238+
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
239+
}
240+
241+
var result models.TracoorExecutionBlockTraceResponse
242+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
243+
return nil, fmt.Errorf("failed to decode response: %w", err)
244+
}
245+
246+
return &result, nil
247+
}

0 commit comments

Comments
 (0)