Skip to content

Commit 73fec32

Browse files
feat: add DA visualization HTTP endpoints
Implements comprehensive DA layer visualization with: - Real-time tracking of blob submissions from sequencer - HTML dashboard with auto-refresh showing submission status - JSON API endpoints for programmatic access - Individual blob inspection with hex content preview - Integration with existing submission flow Addresses issue #2104 by providing HTTP endpoints to inspect blobs submitted from sequencer nodes to the DA layer. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Marko <tac0turtle@users.noreply.github.com>
1 parent 265ff50 commit 73fec32

5 files changed

Lines changed: 679 additions & 0 deletions

File tree

block/manager.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/evstack/ev-node/pkg/cache"
2626
"github.com/evstack/ev-node/pkg/config"
2727
"github.com/evstack/ev-node/pkg/genesis"
28+
"github.com/evstack/ev-node/pkg/rpc/server"
2829
"github.com/evstack/ev-node/pkg/signer"
2930
storepkg "github.com/evstack/ev-node/pkg/store"
3031
"github.com/evstack/ev-node/types"
@@ -428,6 +429,10 @@ func NewManager(
428429
return nil, fmt.Errorf("failed to load cache: %w", err)
429430
}
430431

432+
// Initialize DA visualization server
433+
daVisualizationServer := server.NewDAVisualizationServer(da, logger.With().Str("module", "da_visualization").Logger())
434+
server.SetDAVisualizationServer(daVisualizationServer)
435+
431436
return m, nil
432437
}
433438

block/submitter.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
coreda "github.com/evstack/ev-node/core/da"
9+
"github.com/evstack/ev-node/pkg/rpc/server"
910
"github.com/evstack/ev-node/types"
1011
"google.golang.org/protobuf/proto"
1112
)
@@ -121,6 +122,11 @@ func submitToDA[T any](
121122
res := types.SubmitWithHelpers(submitctx, m.da, m.logger, currMarshaled, gasPrice, namespace, nil)
122123
submitCtxCancel()
123124

125+
// Record submission in DA visualization server
126+
if daVisualizationServer := server.GetDAVisualizationServer(); daVisualizationServer != nil {
127+
daVisualizationServer.RecordSubmission(&res, gasPrice, len(currMarshaled))
128+
}
129+
124130
switch res.Code {
125131
case coreda.StatusSuccess:
126132
// Record successful DA submission

pkg/rpc/server/da_visualization.go

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"encoding/json"
7+
"fmt"
8+
"html/template"
9+
"net/http"
10+
"strconv"
11+
"sync"
12+
"time"
13+
14+
coreda "github.com/evstack/ev-node/core/da"
15+
"github.com/rs/zerolog"
16+
)
17+
18+
// DASubmissionInfo represents information about a DA submission
19+
type DASubmissionInfo struct {
20+
ID string `json:"id"`
21+
Height uint64 `json:"height"`
22+
BlobSize uint64 `json:"blob_size"`
23+
Timestamp time.Time `json:"timestamp"`
24+
GasPrice float64 `json:"gas_price"`
25+
StatusCode string `json:"status_code"`
26+
Message string `json:"message,omitempty"`
27+
NumBlobs int `json:"num_blobs"`
28+
BlobIDs []string `json:"blob_ids,omitempty"`
29+
}
30+
31+
// DAVisualizationServer provides DA layer visualization endpoints
32+
type DAVisualizationServer struct {
33+
da coreda.DA
34+
logger zerolog.Logger
35+
submissions []DASubmissionInfo
36+
mutex sync.RWMutex
37+
}
38+
39+
// NewDAVisualizationServer creates a new DA visualization server
40+
func NewDAVisualizationServer(da coreda.DA, logger zerolog.Logger) *DAVisualizationServer {
41+
return &DAVisualizationServer{
42+
da: da,
43+
logger: logger,
44+
submissions: make([]DASubmissionInfo, 0),
45+
}
46+
}
47+
48+
// RecordSubmission records a DA submission for visualization
49+
func (s *DAVisualizationServer) RecordSubmission(result *coreda.ResultSubmit, gasPrice float64, numBlobs int) {
50+
s.mutex.Lock()
51+
defer s.mutex.Unlock()
52+
53+
statusCode := s.getStatusCodeString(result.Code)
54+
blobIDs := make([]string, len(result.IDs))
55+
for i, id := range result.IDs {
56+
blobIDs[i] = hex.EncodeToString(id)
57+
}
58+
59+
submission := DASubmissionInfo{
60+
ID: fmt.Sprintf("submission_%d_%d", result.Height, time.Now().Unix()),
61+
Height: result.Height,
62+
BlobSize: result.BlobSize,
63+
Timestamp: result.Timestamp,
64+
GasPrice: gasPrice,
65+
StatusCode: statusCode,
66+
Message: result.Message,
67+
NumBlobs: numBlobs,
68+
BlobIDs: blobIDs,
69+
}
70+
71+
// Keep only the last 100 submissions to avoid memory growth
72+
s.submissions = append(s.submissions, submission)
73+
if len(s.submissions) > 100 {
74+
s.submissions = s.submissions[1:]
75+
}
76+
}
77+
78+
// getStatusCodeString converts status code to human-readable string
79+
func (s *DAVisualizationServer) getStatusCodeString(code coreda.StatusCode) string {
80+
switch code {
81+
case coreda.StatusSuccess:
82+
return "Success"
83+
case coreda.StatusNotFound:
84+
return "Not Found"
85+
case coreda.StatusNotIncludedInBlock:
86+
return "Not Included In Block"
87+
case coreda.StatusAlreadyInMempool:
88+
return "Already In Mempool"
89+
case coreda.StatusTooBig:
90+
return "Too Big"
91+
case coreda.StatusContextDeadline:
92+
return "Context Deadline"
93+
case coreda.StatusError:
94+
return "Error"
95+
case coreda.StatusIncorrectAccountSequence:
96+
return "Incorrect Account Sequence"
97+
case coreda.StatusContextCanceled:
98+
return "Context Canceled"
99+
case coreda.StatusHeightFromFuture:
100+
return "Height From Future"
101+
default:
102+
return "Unknown"
103+
}
104+
}
105+
106+
// handleDASubmissions returns JSON list of recent DA submissions
107+
func (s *DAVisualizationServer) handleDASubmissions(w http.ResponseWriter, r *http.Request) {
108+
s.mutex.RLock()
109+
defer s.mutex.RUnlock()
110+
111+
w.Header().Set("Content-Type", "application/json")
112+
if err := json.NewEncoder(w).Encode(map[string]interface{}{
113+
"submissions": s.submissions,
114+
"total": len(s.submissions),
115+
}); err != nil {
116+
s.logger.Error().Err(err).Msg("Failed to encode DA submissions response")
117+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
118+
return
119+
}
120+
}
121+
122+
// handleDABlobDetails returns details about a specific blob
123+
func (s *DAVisualizationServer) handleDABlobDetails(w http.ResponseWriter, r *http.Request) {
124+
blobID := r.URL.Query().Get("id")
125+
if blobID == "" {
126+
http.Error(w, "Missing blob ID parameter", http.StatusBadRequest)
127+
return
128+
}
129+
130+
// Decode the hex blob ID
131+
id, err := hex.DecodeString(blobID)
132+
if err != nil {
133+
http.Error(w, "Invalid blob ID format", http.StatusBadRequest)
134+
return
135+
}
136+
137+
// Try to retrieve blob from DA layer
138+
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
139+
defer cancel()
140+
141+
// Extract namespace - using empty namespace for now, could be parameterized
142+
namespace := []byte{}
143+
blobs, err := s.da.Get(ctx, []coreda.ID{id}, namespace)
144+
if err != nil {
145+
s.logger.Error().Err(err).Str("blob_id", blobID).Msg("Failed to retrieve blob from DA")
146+
http.Error(w, fmt.Sprintf("Failed to retrieve blob: %v", err), http.StatusInternalServerError)
147+
return
148+
}
149+
150+
if len(blobs) == 0 {
151+
http.Error(w, "Blob not found", http.StatusNotFound)
152+
return
153+
}
154+
155+
// Parse the blob ID to extract height and commitment
156+
height, commitment, err := coreda.SplitID(id)
157+
if err != nil {
158+
s.logger.Error().Err(err).Str("blob_id", blobID).Msg("Failed to split blob ID")
159+
}
160+
161+
blob := blobs[0]
162+
response := map[string]interface{}{
163+
"id": blobID,
164+
"height": height,
165+
"commitment": hex.EncodeToString(commitment),
166+
"size": len(blob),
167+
"content": hex.EncodeToString(blob),
168+
"content_preview": string(blob[:min(len(blob), 200)]), // First 200 bytes as string
169+
}
170+
171+
w.Header().Set("Content-Type", "application/json")
172+
if err := json.NewEncoder(w).Encode(response); err != nil {
173+
s.logger.Error().Err(err).Msg("Failed to encode blob details response")
174+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
175+
}
176+
}
177+
178+
// handleDAVisualizationHTML returns HTML visualization page
179+
func (s *DAVisualizationServer) handleDAVisualizationHTML(w http.ResponseWriter, r *http.Request) {
180+
s.mutex.RLock()
181+
submissions := make([]DASubmissionInfo, len(s.submissions))
182+
copy(submissions, s.submissions)
183+
s.mutex.RUnlock()
184+
185+
// Reverse the slice to show newest first
186+
for i, j := 0, len(submissions)-1; i < j; i, j = i+1, j-1 {
187+
submissions[i], submissions[j] = submissions[j], submissions[i]
188+
}
189+
190+
tmpl := `
191+
<!DOCTYPE html>
192+
<html>
193+
<head>
194+
<title>DA Layer Visualization</title>
195+
<style>
196+
body { font-family: Arial, sans-serif; margin: 20px; }
197+
.header { background-color: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
198+
.submission { border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 5px; }
199+
.success { border-left: 4px solid #4CAF50; }
200+
.error { border-left: 4px solid #f44336; }
201+
.pending { border-left: 4px solid #ff9800; }
202+
.blob-ids { margin-top: 10px; }
203+
.blob-id { background-color: #f0f0f0; padding: 2px 6px; margin: 2px; border-radius: 3px; font-family: monospace; font-size: 12px; }
204+
.meta { color: #666; font-size: 14px; }
205+
table { border-collapse: collapse; width: 100%; }
206+
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
207+
th { background-color: #f2f2f2; }
208+
.blob-link { color: #007cba; text-decoration: none; }
209+
.blob-link:hover { text-decoration: underline; }
210+
</style>
211+
</head>
212+
<body>
213+
<div class="header">
214+
<h1>DA Layer Visualization</h1>
215+
<p>Real-time view of blob submissions from the sequencer node to the Data Availability layer.</p>
216+
<p><strong>Total Submissions:</strong> {{len .}} | <strong>Last Update:</strong> {{.LastUpdate}}</p>
217+
</div>
218+
219+
<h2>Recent Submissions</h2>
220+
{{if .}}
221+
<table>
222+
<tr>
223+
<th>Timestamp</th>
224+
<th>Height</th>
225+
<th>Status</th>
226+
<th>Blobs</th>
227+
<th>Size (bytes)</th>
228+
<th>Gas Price</th>
229+
<th>Message</th>
230+
</tr>
231+
{{range .}}
232+
<tr class="{{if eq .StatusCode "Success"}}success{{else if eq .StatusCode "Error"}}error{{else}}pending{{end}}">
233+
<td>{{.Timestamp.Format "15:04:05"}}</td>
234+
<td>{{.Height}}</td>
235+
<td>{{.StatusCode}}</td>
236+
<td>
237+
{{.NumBlobs}}
238+
{{if .BlobIDs}}
239+
<div class="blob-ids">
240+
{{range .BlobIDs}}
241+
<a href="/da/blob?id={{.}}" class="blob-link blob-id">{{slice . 0 8}}...</a>
242+
{{end}}
243+
</div>
244+
{{end}}
245+
</td>
246+
<td>{{.BlobSize}}</td>
247+
<td>{{printf "%.6f" .GasPrice}}</td>
248+
<td>{{.Message}}</td>
249+
</tr>
250+
{{end}}
251+
</table>
252+
{{else}}
253+
<p>No submissions recorded yet.</p>
254+
{{end}}
255+
256+
<div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666;">
257+
<p><strong>API Endpoints:</strong></p>
258+
<ul>
259+
<li><code>GET /da/submissions</code> - JSON list of submissions</li>
260+
<li><code>GET /da/blob?id=&lt;blob_id&gt;</code> - Detailed blob information</li>
261+
</ul>
262+
<p><em>Auto-refresh: <span id="countdown">30</span>s</em></p>
263+
</div>
264+
265+
<script>
266+
// Auto-refresh page every 30 seconds
267+
let countdown = 30;
268+
const countdownEl = document.getElementById('countdown');
269+
setInterval(() => {
270+
countdown--;
271+
countdownEl.textContent = countdown;
272+
if (countdown <= 0) {
273+
location.reload();
274+
}
275+
}, 1000);
276+
</script>
277+
</body>
278+
</html>
279+
`
280+
281+
t, err := template.New("da").Funcs(template.FuncMap{
282+
"slice": func(s string, start, end int) string {
283+
if end > len(s) {
284+
end = len(s)
285+
}
286+
return s[start:end]
287+
},
288+
"len": func(s []DASubmissionInfo) int {
289+
return len(s)
290+
},
291+
}).Parse(tmpl)
292+
293+
if err != nil {
294+
s.logger.Error().Err(err).Msg("Failed to parse template")
295+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
296+
return
297+
}
298+
299+
// Create template data with LastUpdate
300+
data := struct {
301+
Submissions []DASubmissionInfo
302+
LastUpdate string
303+
}{
304+
Submissions: submissions,
305+
LastUpdate: time.Now().Format("15:04:05"),
306+
}
307+
308+
w.Header().Set("Content-Type", "text/html")
309+
if err := t.Execute(w, data.Submissions); err != nil {
310+
s.logger.Error().Err(err).Msg("Failed to execute template")
311+
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
312+
}
313+
}
314+
315+
// Global DA visualization server instance
316+
var daVisualizationServer *DAVisualizationServer
317+
var daVisualizationMutex sync.Mutex
318+
319+
// SetDAVisualizationServer sets the global DA visualization server instance
320+
func SetDAVisualizationServer(server *DAVisualizationServer) {
321+
daVisualizationMutex.Lock()
322+
defer daVisualizationMutex.Unlock()
323+
daVisualizationServer = server
324+
}
325+
326+
// GetDAVisualizationServer returns the global DA visualization server instance
327+
func GetDAVisualizationServer() *DAVisualizationServer {
328+
daVisualizationMutex.Lock()
329+
defer daVisualizationMutex.Unlock()
330+
return daVisualizationServer
331+
}

0 commit comments

Comments
 (0)