diff --git a/README.md b/README.md index 94a4b423..b13104bb 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,4 @@ This explorer is heavily based on the code from [gobitfly/eth2-beaconchain-explo # License [![License: GPL-3.0](https://img.shields.io/badge/license-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) + diff --git a/clients/consensus/rpc/beaconapi.go b/clients/consensus/rpc/beaconapi.go index 7abf3175..42417460 100644 --- a/clients/consensus/rpc/beaconapi.go +++ b/clients/consensus/rpc/beaconapi.go @@ -439,6 +439,50 @@ func (bc *BeaconClient) GetBlobSidecarsByBlockroot(ctx context.Context, blockroo return result.Data, nil } +func (bc *BeaconClient) GetExecutionProofsByBlockroot(ctx context.Context, blockroot []byte) (*ExecutionProofsResponse, error) { + // Make a direct HTTP request since this is a custom Lighthouse endpoint + // not part of the standard eth2 API + url := fmt.Sprintf("%s/eth/v1/beacon/execution_proofs/0x%x", bc.endpoint, blockroot) + + req, err := nethttp.NewRequestWithContext(ctx, "GET", url, nethttp.NoBody) + if err != nil { + return nil, err + } + + // Add custom headers + for headerKey, headerVal := range bc.headers { + req.Header.Set(headerKey, headerVal) + } + req.Header.Set("Accept", "application/json") + + client := &nethttp.Client{Timeout: time.Second * 300} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != nethttp.StatusOK { + // If endpoint doesn't exist or block has no proofs, return empty response + if resp.StatusCode == nethttp.StatusNotFound { + return &ExecutionProofsResponse{ + Data: []ExecutionProof{}, + ExecutionOptimistic: false, + Finalized: false, + }, nil + } + data, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("url: %v, status: %d, error: %s", url, resp.StatusCode, data) + } + + var response ExecutionProofsResponse + if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { + return nil, fmt.Errorf("error parsing json response: %v", err) + } + + return &response, nil +} + func (bc *BeaconClient) GetForkState(ctx context.Context, stateRef string) (*phase0.Fork, error) { provider, isProvider := bc.clientSvc.(eth2client.ForkProvider) if !isProvider { diff --git a/clients/consensus/rpc/types.go b/clients/consensus/rpc/types.go new file mode 100644 index 00000000..9dc234c9 --- /dev/null +++ b/clients/consensus/rpc/types.go @@ -0,0 +1,27 @@ +package rpc + +// ExecutionProof represents a cryptographic proof of execution that +// an execution payload is valid. +type ExecutionProof struct { + // Which proof type (zkVM+EL combination) this proof belongs to + ProofId uint8 `json:"proof_id"` + + // The slot of the beacon block this proof validates + Slot string `json:"slot"` + + // The block hash of the execution payload this proof validates + BlockHash string `json:"block_hash"` + + // The beacon block root corresponding to the beacon block + BlockRoot string `json:"block_root"` + + // The actual proof data (byte array) + ProofData []byte `json:"proof_data"` +} + +// ExecutionProofsResponse represents the API response for execution proofs +type ExecutionProofsResponse struct { + Data []ExecutionProof `json:"data"` + ExecutionOptimistic bool `json:"execution_optimistic"` + Finalized bool `json:"finalized"` +} diff --git a/handlers/slot.go b/handlers/slot.go index bc8df344..9d364503 100644 --- a/handlers/slot.go +++ b/handlers/slot.go @@ -39,6 +39,7 @@ func Slot(w http.ResponseWriter, r *http.Request) { "slot/overview.html", "slot/transactions.html", "slot/attestations.html", + "slot/proofs.html", "slot/deposits.html", "slot/withdrawals.html", "slot/voluntary_exits.html", @@ -760,6 +761,9 @@ func getSlotPageBlockData(blockData *services.CombinedBlockResponse, epochStatsV } } + // Fetch execution proofs for this block + getSlotPageExecutionProofs(pageData, blockData.Root) + if requests, err := blockData.Block.ExecutionRequests(); err == nil && requests != nil { getSlotPageDepositRequests(pageData, requests.Deposits) getSlotPageWithdrawalRequests(pageData, requests.Withdrawals) @@ -926,6 +930,72 @@ func getSlotPageConsolidationRequests(pageData *models.SlotPageBlockData, consol pageData.ConsolidationRequestsCount = uint64(len(pageData.ConsolidationRequests)) } +func getSlotPageExecutionProofs(pageData *models.SlotPageBlockData, blockRoot phase0.Root) { + // Get a ready beacon client to fetch execution proofs + beaconIndexer := services.GlobalBeaconService.GetBeaconIndexer() + client := beaconIndexer.GetReadyClient(false) + if client == nil { + // No client available, return empty proofs + pageData.ExecutionProofsCount = 0 + pageData.ExecutionProofs = []*models.SlotPageExecutionProof{} + return + } + + // Fetch execution proofs from the beacon node + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + proofsResponse, err := client.GetClient().GetRPCClient().GetExecutionProofsByBlockroot(ctx, blockRoot[:]) + if err != nil { + // Error fetching proofs, return empty list + logrus.WithError(err).WithField("blockRoot", fmt.Sprintf("0x%x", blockRoot)).Info("Error fetching execution proofs") + pageData.ExecutionProofsCount = 0 + pageData.ExecutionProofs = []*models.SlotPageExecutionProof{} + return + } + + // Log successful fetch + logrus.WithFields(logrus.Fields{ + "blockRoot": fmt.Sprintf("0x%x", blockRoot), + "proofCount": len(proofsResponse.Data), + }).Info("Successfully fetched execution proofs") + + // Convert RPC proofs to page data model + pageData.ExecutionProofsCount = uint64(len(proofsResponse.Data)) + pageData.ExecutionProofs = make([]*models.SlotPageExecutionProof, pageData.ExecutionProofsCount) + for i, proof := range proofsResponse.Data { + // Parse slot from string + slot, err := strconv.ParseUint(proof.Slot, 10, 64) + if err != nil { + logrus.WithError(err).WithField("slot", proof.Slot).Warn("Error parsing proof slot") + continue + } + + // Decode hex strings + blockHash, err := hex.DecodeString(strings.TrimPrefix(proof.BlockHash, "0x")) + if err != nil { + logrus.WithError(err).WithField("blockHash", proof.BlockHash).Warn("Error decoding block hash") + continue + } + + blockRootBytes, err := hex.DecodeString(strings.TrimPrefix(proof.BlockRoot, "0x")) + if err != nil { + logrus.WithError(err).WithField("blockRoot", proof.BlockRoot).Warn("Error decoding block root") + continue + } + + // proof.ProofData is already a byte array from JSON, no need to decode + + pageData.ExecutionProofs[i] = &models.SlotPageExecutionProof{ + ProofId: proof.ProofId, + Slot: slot, + BlockHash: blockHash, + BlockRoot: blockRootBytes, + ProofData: proof.ProofData, + } + } +} + func handleSlotDownload(ctx context.Context, w http.ResponseWriter, blockSlot int64, blockRoot []byte, downloadType string) error { chainState := services.GlobalBeaconService.GetChainState() currentSlot := chainState.CurrentSlot() diff --git a/templates/slot/proofs.html b/templates/slot/proofs.html new file mode 100644 index 00000000..b43361bf --- /dev/null +++ b/templates/slot/proofs.html @@ -0,0 +1,77 @@ +{{ define "block_proofs" }} + {{ if gt (len .Block.ExecutionProofs) 0 }} + {{ range $i, $proof := .Block.ExecutionProofs }} +
+
+
+
Execution Proof {{ $proof.ProofId }}
+
+
+
Proof ID:
+
+ {{ $proof.ProofId }} + + {{ if eq $proof.ProofId 0 }}(SP1+Reth) + {{ else if eq $proof.ProofId 1 }}(Risc0+Geth) + {{ else if eq $proof.ProofId 2 }}(SP1+Geth) + {{ else }}(zkVM+EL #{{ $proof.ProofId }}) + {{ end }} + +
+
+
+
Slot:
+
+ {{ $proof.Slot }} +
+
+
+
Block Hash:
+
+ 0x{{ printf "%x" $proof.BlockHash }} + +
+
+
+
Block Root:
+
+ 0x{{ printf "%x" $proof.BlockRoot }} + +
+
+
+
Proof Size:
+
+ {{ len $proof.ProofData }} bytes + {{ if gt (len $proof.ProofData) 1024 }} + ({{ printf "%.2f" (div (len $proof.ProofData) 1024.0) }} KB) + {{ end }} +
+
+
+
Proof Data:
+
+ {{ if gt (len $proof.ProofData) 128 }} + 0x{{ printf "%x" (slice $proof.ProofData 0 128) }}... + {{ else }} + 0x{{ printf "%x" $proof.ProofData }} + {{ end }} + +
+
+
+
+ {{ end }} + {{ else }} +
+
+
+
+

No execution proofs available for this block.

+

Execution proofs are cryptographic proofs that validate the execution payload was computed correctly by a specific zkVM+EL combination.

+
+
+
+
+ {{ end }} +{{ end }} diff --git a/templates/slot/slot.html b/templates/slot/slot.html index f14d0f20..f986ecd6 100644 --- a/templates/slot/slot.html +++ b/templates/slot/slot.html @@ -46,6 +46,9 @@

+ {{ if gt .Block.DepositsCount 0 }}