diff --git a/docs/guides/da/blob-decoder.md b/docs/guides/da/blob-decoder.md new file mode 100644 index 0000000000..b37d963711 --- /dev/null +++ b/docs/guides/da/blob-decoder.md @@ -0,0 +1,152 @@ +# Blob Decoder Tool + +The blob decoder is a utility tool for decoding and inspecting blobs from Celestia (DA) layers. It provides both a web interface and API for decoding blob data into human-readable format. + +## Overview + +The blob decoder helps developers and operators inspect the contents of blobs submitted to DA layers. It can decode: +- Raw blob data (hex or base64 encoded) +- Block data structures +- Transaction payloads +- Protobuf-encoded messages + +## Usage + +### Starting the Server + +```bash +# Run with default port (8080) +go run tools/blob-decoder/main.go +``` + +The server will start and display: +- Web interface URL: `http://localhost:8080` +- API endpoint: `http://localhost:8080/api/decode` + +### Web Interface + +1. Open your browser to `http://localhost:8080` +2. Paste your blob data in the input field +3. Select the encoding format (hex or base64) +4. Click "Decode" to see the parsed output + +### API Usage + +The decoder provides a REST API for programmatic access: + +```bash +# Decode hex-encoded blob +curl -X POST http://localhost:8080/api/decode \ + -H "Content-Type: application/json" \ + -d '{ + "data": "0x1234abcd...", + "encoding": "hex" + }' + +# Decode base64-encoded blob +curl -X POST http://localhost:8080/api/decode \ + -H "Content-Type: application/json" \ + -d '{ + "data": "SGVsbG8gV29ybGQ=", + "encoding": "base64" + }' +``` + +#### API Request Format + +```json +{ + "data": "string", // The encoded blob data + "encoding": "string" // Either "hex" or "base64" +} +``` + +#### API Response Format + +```json +{ + "success": true, + "decoded": { + // Decoded data structure + }, + "error": "string" // Only present if success is false +} +``` + +## Supported Data Types + +### Block Data + +The decoder can parse ev-node block structures: +- Block height +- Timestamp +- Parent hash +- Transaction list +- Validator information +- Data commitments + +### Transaction Data + +Decodes individual transactions including: +- Transaction type +- Sender/receiver addresses +- Value/amount +- Gas parameters +- Payload data + +### Protobuf Messages + +Automatically detects and decodes protobuf-encoded messages used in ev-node: +- Block headers +- Transaction batches +- State updates +- DA commitments + +## Examples + +### Decoding a Block Blob + +```bash +# Example block blob (hex encoded) +curl -X POST http://localhost:8080/api/decode \ + -H "Content-Type: application/json" \ + -d '{ + "data": "0a2408011220...", + "encoding": "hex" + }' +``` + +Response: +```json +{ + "success": true, + "decoded": { + "height": 100, + "timestamp": "2024-01-15T10:30:00Z", + "parentHash": "0xabc123...", + "transactions": [ + { + "type": "transfer", + "from": "0x123...", + "to": "0x456...", + "value": "1000000000000000000" + } + ] + } +} +``` + +### Decoding DA Commitment + +```bash +curl -X POST http://localhost:8080/api/decode \ + -H "Content-Type: application/json" \ + -d '{ + "data": "eyJjb21taXRtZW50IjogIi4uLiJ9", + "encoding": "base64" + }' +``` + +### Celestia + +For Celestia blobs, you can decode namespace data and payment information from [celenium](https://celenium.io/namespaces). diff --git a/tools/blob-decoder/main.go b/tools/blob-decoder/main.go new file mode 100644 index 0000000000..4e59182d2a --- /dev/null +++ b/tools/blob-decoder/main.go @@ -0,0 +1,285 @@ +package main + +import ( + "embed" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + "os" + "strconv" + "strings" + "time" + + "google.golang.org/protobuf/proto" + + "github.com/evstack/ev-node/types" + pb "github.com/evstack/ev-node/types/pb/evnode/v1" +) + +//go:embed templates/* +var templatesFS embed.FS + +// DecodedBlob represents the result of decoding a blob +type DecodedBlob struct { + Type string `json:"type"` + Data interface{} `json:"data"` + RawHex string `json:"rawHex"` + Size int `json:"size"` + Timestamp time.Time `json:"timestamp"` + Error string `json:"error,omitempty"` +} + +func main() { + mux := http.NewServeMux() + + // handlers + mux.HandleFunc("/", handleIndex) + mux.HandleFunc("/api/decode", handleDecode) + + // port configuration + port := "8090" + if len(os.Args[1:]) > 0 { + port = os.Args[1] + if _, err := strconv.Atoi(port); err != nil { + log.Fatal("Invalid port number") + } + } + + fmt.Printf(` +╔═══════════════════════════════════╗ +║ Evolve Blob Decoder ║ +║ by ev.xyz ║ +╚═══════════════════════════════════╝ + +🚀 Server running at: http://localhost:%s +⚡ Using native Evolve protobuf decoding + +Press Ctrl+C to stop the server +`, port) + + // Create server with proper timeout configuration + srv := &http.Server{ + Addr: ":" + port, + Handler: corsMiddleware(mux), + ReadTimeout: 15 * time.Second, + WriteTimeout: 15 * time.Second, + IdleTimeout: 60 * time.Second, + } + + if err := srv.ListenAndServe(); err != nil { + log.Fatal(err) + } +} + +func corsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + + next.ServeHTTP(w, r) + }) +} + +func handleIndex(w http.ResponseWriter, r *http.Request) { + tmpl := template.Must(template.ParseFS(templatesFS, "templates/index.html")) + if err := tmpl.Execute(w, nil); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +func handleDecode(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var request struct { + Data string `json:"data"` + Encoding string `json:"encoding"` // "hex" or "base64" + } + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + // Decode the input data + var blobData []byte + var err error + + switch request.Encoding { + case "hex": + cleanHex := strings.TrimPrefix(request.Data, "0x") + blobData, err = hex.DecodeString(cleanHex) + case "base64": + blobData, err = base64.StdEncoding.DecodeString(request.Data) + default: + http.Error(w, "Invalid encoding type", http.StatusBadRequest) + return + } + + if err != nil { + sendJSONResponse(w, DecodedBlob{ + Error: fmt.Sprintf("Failed to decode %s: %v", request.Encoding, err), + Timestamp: time.Now(), + }) + return + } + + // Try to decode the blob + result := decodeBlob(blobData) + result.RawHex = hex.EncodeToString(blobData) + result.Size = len(blobData) + result.Timestamp = time.Now() + + sendJSONResponse(w, result) +} + +func decodeBlob(data []byte) DecodedBlob { + // Try to decode as SignedHeader + if header := tryDecodeHeader(data); header != nil { + return DecodedBlob{ + Type: "SignedHeader", + Data: header, + } + } + + // Try to decode as SignedData + if signedData := tryDecodeSignedData(data); signedData != nil { + return DecodedBlob{ + Type: "SignedData", + Data: signedData, + } + } + + // Check if it's JSON + var jsonData interface{} + if err := json.Unmarshal(data, &jsonData); err == nil { + return DecodedBlob{ + Type: "JSON", + Data: jsonData, + } + } + + // Return as unknown binary + return DecodedBlob{ + Type: "Unknown", + Data: map[string]interface{}{ + "message": "Unable to decode blob format", + "preview": hex.EncodeToString(data[:min(100, len(data))]), + }, + } +} + +func tryDecodeHeader(data []byte) interface{} { + var headerPb pb.SignedHeader + if err := proto.Unmarshal(data, &headerPb); err != nil { + return nil + } + + var signedHeader types.SignedHeader + if err := signedHeader.FromProto(&headerPb); err != nil { + return nil + } + + // Basic validation + if err := signedHeader.Header.ValidateBasic(); err != nil { + return nil + } + + // Return a map with the actual header fields + return map[string]interface{}{ + // BaseHeader fields + "height": signedHeader.Height(), + "time": signedHeader.Time().Format(time.RFC3339Nano), + "chainId": signedHeader.ChainID(), + + // Version + "version": map[string]interface{}{ + "block": signedHeader.Version.Block, + "app": signedHeader.Version.App, + }, + + // Hash fields (convert [32]byte arrays to hex strings) + "lastHeaderHash": bytesToHex(signedHeader.LastHeaderHash[:]), + "lastCommitHash": bytesToHex(signedHeader.LastCommitHash[:]), + "dataHash": bytesToHex(signedHeader.DataHash[:]), + "consensusHash": bytesToHex(signedHeader.ConsensusHash[:]), + "appHash": bytesToHex(signedHeader.AppHash[:]), + "lastResultsHash": bytesToHex(signedHeader.LastResultsHash[:]), + "validatorHash": bytesToHex(signedHeader.ValidatorHash[:]), + + // Proposer + "proposerAddress": bytesToHex(signedHeader.ProposerAddress), + + // Signature fields + "signature": bytesToHex(signedHeader.Signature), + "signer": map[string]interface{}{ + "address": bytesToHex(signedHeader.Signer.Address), + "pubKey": "", + }, + } +} + +func tryDecodeSignedData(data []byte) interface{} { + var signedData types.SignedData + if err := signedData.UnmarshalBinary(data); err != nil { + return nil + } + + // Create transaction list + transactions := make([]map[string]interface{}, len(signedData.Txs)) + for i, tx := range signedData.Txs { + transactions[i] = map[string]interface{}{ + "index": i, + "size": len(tx), + "data": bytesToHex(tx), + } + } + + result := map[string]interface{}{ + "transactions": transactions, + "transactionCount": len(signedData.Txs), + "signature": bytesToHex(signedData.Signature), + } + + // Add Metadata fields if present + if signedData.Metadata != nil { + result["chainId"] = signedData.ChainID() + result["height"] = signedData.Height() + result["time"] = signedData.Time().Format(time.RFC3339Nano) + result["lastDataHash"] = bytesToHex(signedData.LastDataHash[:]) + } + + // Add DACommitment hash + dataHash := signedData.DACommitment() + result["dataHash"] = bytesToHex(dataHash[:]) + + return result +} + +func bytesToHex(b []byte) string { + if len(b) == 0 { + return "" + } + return hex.EncodeToString(b) +} + +func sendJSONResponse(w http.ResponseWriter, data interface{}) { + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(data); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/tools/blob-decoder/templates/index.html b/tools/blob-decoder/templates/index.html new file mode 100644 index 0000000000..cc289ea5c0 --- /dev/null +++ b/tools/blob-decoder/templates/index.html @@ -0,0 +1,874 @@ + + +
+ + +Own It. Shape It. Launch It.
++ Decode and inspect DA layer blobs from your Evolve rollup +
+