Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

44 changes: 44 additions & 0 deletions clients/consensus/rpc/beaconapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions clients/consensus/rpc/types.go
Original file line number Diff line number Diff line change
@@ -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"`
}
70 changes: 70 additions & 0 deletions handlers/slot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
77 changes: 77 additions & 0 deletions templates/slot/proofs.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{{ define "block_proofs" }}
{{ if gt (len .Block.ExecutionProofs) 0 }}
{{ range $i, $proof := .Block.ExecutionProofs }}
<div class="card my-2">
<div class="card-body px-0 py-1">
<div class="row border-bottom p-1 mx-0">
<div class="col-md-12 text-center"><b>Execution Proof {{ $proof.ProofId }}</b></div>
</div>
<div class="row border-bottom p-1 mx-0">
<div class="col-md-2"><span data-bs-toggle="tooltip" data-bs-placement="top" title="Proof Type ID">Proof ID:</span></div>
<div class="col-md-10">
{{ $proof.ProofId }}
<span class="text-muted ms-2">
{{ if eq $proof.ProofId 0 }}(SP1+Reth)
{{ else if eq $proof.ProofId 1 }}(Risc0+Geth)
{{ else if eq $proof.ProofId 2 }}(SP1+Geth)
Comment on lines +14 to +16
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These labels are hardcoded, but will be changed

{{ else }}(zkVM+EL #{{ $proof.ProofId }})
{{ end }}
</span>
</div>
</div>
<div class="row border-bottom p-1 mx-0">
<div class="col-md-2"><span data-bs-toggle="tooltip" data-bs-placement="top" title="Slot Number">Slot:</span></div>
<div class="col-md-10">
{{ $proof.Slot }}
</div>
</div>
<div class="row border-bottom p-1 mx-0">
<div class="col-md-2"><span data-bs-toggle="tooltip" data-bs-placement="top" title="Execution Block Hash">Block Hash:</span></div>
<div class="col-md-10 text-monospace">
0x{{ printf "%x" $proof.BlockHash }}
<i class="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" title="Copy to clipboard" data-clipboard-text="0x{{ printf "%x" $proof.BlockHash }}"></i>
</div>
</div>
<div class="row border-bottom p-1 mx-0">
<div class="col-md-2"><span data-bs-toggle="tooltip" data-bs-placement="top" title="Beacon Block Root">Block Root:</span></div>
<div class="col-md-10 text-monospace">
0x{{ printf "%x" $proof.BlockRoot }}
<i class="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" title="Copy to clipboard" data-clipboard-text="0x{{ printf "%x" $proof.BlockRoot }}"></i>
</div>
</div>
<div class="row border-bottom p-1 mx-0">
<div class="col-md-2"><span data-bs-toggle="tooltip" data-bs-placement="top" title="Proof Data Size">Proof Size:</span></div>
<div class="col-md-10">
{{ len $proof.ProofData }} bytes
{{ if gt (len $proof.ProofData) 1024 }}
({{ printf "%.2f" (div (len $proof.ProofData) 1024.0) }} KB)
{{ end }}
</div>
</div>
<div class="row border-bottom p-1 mx-0">
<div class="col-md-2"><span data-bs-toggle="tooltip" data-bs-placement="top" title="Proof Data (truncated)">Proof Data:</span></div>
<div class="col-md-10 text-monospace" style="word-break: break-all;">
{{ if gt (len $proof.ProofData) 128 }}
0x{{ printf "%x" (slice $proof.ProofData 0 128) }}...
{{ else }}
0x{{ printf "%x" $proof.ProofData }}
{{ end }}
<i class="fa fa-copy text-muted p-1" role="button" data-bs-toggle="tooltip" title="Copy to clipboard" data-clipboard-text="0x{{ printf "%x" $proof.ProofData }}"></i>
</div>
</div>
</div>
</div>
{{ end }}
{{ else }}
<div class="card my-2">
<div class="card-body px-0 py-1">
<div class="row border-bottom p-1 mx-0">
<div class="col-md-12 text-center">
<p class="text-muted my-3">No execution proofs available for this block.</p>
<p class="text-muted small">Execution proofs are cryptographic proofs that validate the execution payload was computed correctly by a specific zkVM+EL combination.</p>
</div>
</div>
</div>
</div>
{{ end }}
{{ end }}
13 changes: 13 additions & 0 deletions templates/slot/slot.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ <h1 class="h4 my-2 mb-md-0 h1-pager">
<li class="nav-item">
<a class="nav-link" id="attestations-tab" data-bs-toggle="tab" href="#attestations" role="tab" aria-controls="attestations" aria-selected="false">Attestations <span class="badge bg-secondary text-white">{{ .Block.AttestationsCount }}</span></a>
</li>
<li class="nav-item">
<a class="nav-link" id="proofs-tab" data-bs-toggle="tab" href="#proofs" role="tab" aria-controls="proofs" aria-selected="false">Proofs</a>
</li>
{{ if gt .Block.DepositsCount 0 }}
<li class="nav-item">
<a class="nav-link" id="deposits-tab" data-bs-toggle="tab" href="#deposits" role="tab" aria-controls="deposits" aria-selected="false">Deposits <span class="badge bg-secondary text-white">{{ .Block.DepositsCount }}</span></a>
Expand Down Expand Up @@ -138,6 +141,16 @@ <h3 class="h5 col-12 col-md-4 text-center">
</div>
{{ template "block_attestations" . }}
</div>
<div class="tab-pane fade show active" id="proofs" role="tabpanel" aria-labelledby="proofs-tab">
<div class="card block-card">
<div style="margin-bottom: -.25rem;" class="card-body px-0 py-1">
<div class="row p-1 mx-0">
<h3 class="h5 col-md-12 text-center"><b>Proofs</b></h3>
</div>
</div>
</div>
{{ template "block_proofs" . }}
</div>
{{ if gt .Block.DepositsCount 0 }}
<div class="tab-pane fade show active" id="deposits" role="tabpanel" aria-labelledby="deposits-tab">
<div class="card block-card">
Expand Down
1 change: 1 addition & 0 deletions types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type Config struct {
Endpoint string `yaml:"endpoint" envconfig:"BEACONAPI_ENDPOINT"`
Endpoints []EndpointConfig `yaml:"endpoints"`
EndpointsURL string `yaml:"endpointsUrl" envconfig:"BEACONAPI_ENDPOINTS_URL"`
ClientIndex int `yaml:"clientIndex" envconfig:"BEACONAPI_CLIENT_INDEX"`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hack that we use to fix the node we want to always connect to.

Note: The alternative here would be to not put Dora in the kurtosis config and just running it separately connecting to the specific node we care about.


LocalCacheSize int `yaml:"localCacheSize" envconfig:"BEACONAPI_LOCAL_CACHE_SIZE"`
SkipFinalAssignments bool `yaml:"skipFinalAssignments" envconfig:"BEACONAPI_SKIP_FINAL_ASSIGNMENTS"`
Expand Down
10 changes: 10 additions & 0 deletions types/models/slot.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type SlotPageBlockData struct {
VoluntaryExitsCount uint64 `json:"voluntaryexits_count"`
SlashingsCount uint64 `json:"slashings_count"`
BlobsCount uint64 `json:"blobs_count"`
ExecutionProofsCount uint64 `json:"execution_proofs_count"`
TransactionsCount uint64 `json:"transactions_count"`
DepositRequestsCount uint64 `json:"deposit_receipts_count"`
WithdrawalRequestsCount uint64 `json:"withdrawal_requests_count"`
Expand All @@ -78,6 +79,7 @@ type SlotPageBlockData struct {
BLSChanges []*SlotPageBLSChange `json:"bls_changes"` // BLSChanges included in this block
Withdrawals []*SlotPageWithdrawal `json:"withdrawals"` // Withdrawals included in this block
Blobs []*SlotPageBlob `json:"blobs"` // Blob sidecars included in this block
ExecutionProofs []*SlotPageExecutionProof `json:"execution_proofs"` // Execution proofs included in this block
Transactions []*SlotPageTransaction `json:"transactions"` // Transactions included in this block
DepositRequests []*SlotPageDepositRequest `json:"deposit_receipts"` // DepositRequests included in this block
WithdrawalRequests []*SlotPageWithdrawalRequest `json:"withdrawal_requests"` // WithdrawalRequests included in this block
Expand Down Expand Up @@ -260,3 +262,11 @@ type SlotPageConsolidationRequest struct {
TargetName string `db:"target_name"`
Epoch uint64 `db:"epoch"`
}

type SlotPageExecutionProof struct {
ProofId uint8 `json:"proof_id"`
Slot uint64 `json:"slot"`
BlockHash []byte `json:"block_hash"`
BlockRoot []byte `json:"block_root"`
ProofData []byte `json:"proof_data"`
}
7 changes: 7 additions & 0 deletions utils/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ func ReadConfig(cfg *types.Config, path string) error {
},
}
}

// ClientIndex selects a specific endpoint by index (0-based)
if cfg.BeaconApi.ClientIndex >= 0 && cfg.BeaconApi.ClientIndex < len(cfg.BeaconApi.Endpoints) {
selectedEndpoint := cfg.BeaconApi.Endpoints[cfg.BeaconApi.ClientIndex]
cfg.BeaconApi.Endpoints = []types.EndpointConfig{selectedEndpoint}
}

for idx, endpoint := range cfg.BeaconApi.Endpoints {
if endpoint.Name == "" {
url, _ := url.Parse(endpoint.Url)
Expand Down