package debug import ( "context" "encoding/json" "fmt" "math" "net/http" "net/url" "strconv" "strings" "github.com/OffchainLabs/prysm/v7/api" "github.com/OffchainLabs/prysm/v7/api/server/structs" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/core" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/helpers" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/shared" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/consensus-types/blocks" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" "github.com/OffchainLabs/prysm/v7/network/httputil" ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/runtime/version" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" ) const ( errMsgStateFromConsensus = "Could not convert consensus state to response" ) // GetBeaconStateV2 returns the full beacon state for a given state ID. func (s *Server) GetBeaconStateV2(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "debug.GetBeaconStateV2") defer span.End() stateId := r.PathValue("state_id") if stateId == "" { httputil.HandleError(w, "state_id is required in URL params", http.StatusBadRequest) return } if httputil.RespondWithSsz(r) { s.getBeaconStateSSZV2(ctx, w, []byte(stateId)) } else { s.getBeaconStateV2(ctx, w, []byte(stateId)) } } // getBeaconStateV2 returns the JSON-serialized version of the full beacon state object for given state ID. func (s *Server) getBeaconStateV2(ctx context.Context, w http.ResponseWriter, id []byte) { st, err := s.Stater.State(ctx, id) if err != nil { shared.WriteStateFetchError(w, err) return } isOptimistic, err := helpers.IsOptimistic(ctx, id, s.OptimisticModeFetcher, s.Stater, s.ChainInfoFetcher, s.BeaconDB) if err != nil { helpers.HandleIsOptimisticError(w, err) return } blockRoot, err := st.LatestBlockHeader().HashTreeRoot() if err != nil { httputil.HandleError(w, "Could not calculate root of latest block header: "+err.Error(), http.StatusInternalServerError) return } isFinalized := s.FinalizationFetcher.IsFinalized(ctx, blockRoot) var respSt any switch st.Version() { case version.Phase0: respSt, err = structs.BeaconStateFromConsensus(st) if err != nil { httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } case version.Altair: respSt, err = structs.BeaconStateAltairFromConsensus(st) if err != nil { httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } case version.Bellatrix: respSt, err = structs.BeaconStateBellatrixFromConsensus(st) if err != nil { httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } case version.Capella: respSt, err = structs.BeaconStateCapellaFromConsensus(st) if err != nil { httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } case version.Deneb: respSt, err = structs.BeaconStateDenebFromConsensus(st) if err != nil { httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } case version.Electra: respSt, err = structs.BeaconStateElectraFromConsensus(st) if err != nil { httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } case version.Fulu: respSt, err = structs.BeaconStateFuluFromConsensus(st) if err != nil { httputil.HandleError(w, errMsgStateFromConsensus+": "+err.Error(), http.StatusInternalServerError) return } default: httputil.HandleError(w, "Unsupported state version", http.StatusInternalServerError) return } jsonBytes, err := json.Marshal(respSt) if err != nil { httputil.HandleError(w, "Could not marshal state into JSON: "+err.Error(), http.StatusInternalServerError) return } ver := version.String(st.Version()) resp := &structs.GetBeaconStateV2Response{ Version: ver, ExecutionOptimistic: isOptimistic, Finalized: isFinalized, Data: jsonBytes, } w.Header().Set(api.VersionHeader, ver) httputil.WriteJson(w, resp) } // getBeaconStateSSZV2 returns the SSZ-serialized version of the full beacon state object for given state ID. func (s *Server) getBeaconStateSSZV2(ctx context.Context, w http.ResponseWriter, id []byte) { st, err := s.Stater.State(ctx, id) if err != nil { shared.WriteStateFetchError(w, err) return } sszState, err := st.MarshalSSZ() if err != nil { httputil.HandleError(w, "Could not marshal state into SSZ: "+err.Error(), http.StatusInternalServerError) return } w.Header().Set(api.VersionHeader, version.String(st.Version())) httputil.WriteSsz(w, sszState) } // GetForkChoiceHeadsV2 retrieves the leaves of the current fork choice tree. func (s *Server) GetForkChoiceHeadsV2(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "debug.GetForkChoiceHeadsV2") defer span.End() headRoots, headSlots := s.HeadFetcher.ChainHeads() resp := &structs.GetForkChoiceHeadsV2Response{ Data: make([]*structs.ForkChoiceHead, len(headRoots)), } for i := range headRoots { isOptimistic, err := s.OptimisticModeFetcher.IsOptimisticForRoot(ctx, headRoots[i]) if err != nil { httputil.HandleError(w, "Could not check if head is optimistic: "+err.Error(), http.StatusInternalServerError) return } resp.Data[i] = &structs.ForkChoiceHead{ Root: hexutil.Encode(headRoots[i][:]), Slot: fmt.Sprintf("%d", headSlots[i]), ExecutionOptimistic: isOptimistic, } } httputil.WriteJson(w, resp) } // GetForkChoice returns a dump fork choice store. func (s *Server) GetForkChoice(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "debug.GetForkChoice") defer span.End() dump, err := s.ForkchoiceFetcher.ForkChoiceDump(ctx) if err != nil { httputil.HandleError(w, "Could not get forkchoice dump: "+err.Error(), http.StatusInternalServerError) return } nodes := make([]*structs.ForkChoiceNode, len(dump.ForkChoiceNodes)) for i, n := range dump.ForkChoiceNodes { nodes[i] = &structs.ForkChoiceNode{ Slot: fmt.Sprintf("%d", n.Slot), BlockRoot: hexutil.Encode(n.BlockRoot), ParentRoot: hexutil.Encode(n.ParentRoot), JustifiedEpoch: fmt.Sprintf("%d", n.JustifiedEpoch), FinalizedEpoch: fmt.Sprintf("%d", n.FinalizedEpoch), Weight: fmt.Sprintf("%d", n.Weight), ExecutionBlockHash: hexutil.Encode(n.ExecutionBlockHash), Validity: n.Validity.String(), ExtraData: &structs.ForkChoiceNodeExtraData{ UnrealizedJustifiedEpoch: fmt.Sprintf("%d", n.UnrealizedJustifiedEpoch), UnrealizedFinalizedEpoch: fmt.Sprintf("%d", n.UnrealizedFinalizedEpoch), Balance: fmt.Sprintf("%d", n.Balance), ExecutionOptimistic: n.ExecutionOptimistic, TimeStamp: n.Timestamp.String(), Target: fmt.Sprintf("%#x", n.Target), }, } } resp := &structs.GetForkChoiceDumpResponse{ JustifiedCheckpoint: structs.CheckpointFromConsensus(dump.JustifiedCheckpoint), FinalizedCheckpoint: structs.CheckpointFromConsensus(dump.FinalizedCheckpoint), ForkChoiceNodes: nodes, ExtraData: &structs.ForkChoiceDumpExtraData{ UnrealizedJustifiedCheckpoint: structs.CheckpointFromConsensus(dump.UnrealizedJustifiedCheckpoint), UnrealizedFinalizedCheckpoint: structs.CheckpointFromConsensus(dump.UnrealizedFinalizedCheckpoint), ProposerBoostRoot: hexutil.Encode(dump.ProposerBoostRoot), PreviousProposerBoostRoot: hexutil.Encode(dump.PreviousProposerBoostRoot), HeadRoot: hexutil.Encode(dump.HeadRoot), }, } httputil.WriteJson(w, resp) } // DataColumnSidecars retrieves data column sidecars for a given block id. func (s *Server) DataColumnSidecars(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "debug.DataColumnSidecars") defer span.End() // Check if we're before Fulu fork - data columns are only available from Fulu onwards fuluForkEpoch := params.BeaconConfig().FuluForkEpoch if fuluForkEpoch == math.MaxUint64 { httputil.HandleError(w, "Data columns are not supported - Fulu fork not configured", http.StatusBadRequest) return } // Check if we're before Fulu fork based on current slot currentSlot := s.GenesisTimeFetcher.CurrentSlot() currentEpoch := primitives.Epoch(currentSlot / params.BeaconConfig().SlotsPerEpoch) if currentEpoch < fuluForkEpoch { httputil.HandleError(w, "Data columns are not supported - before Fulu fork", http.StatusBadRequest) return } indices, err := parseDataColumnIndices(r.URL) if err != nil { httputil.HandleError(w, err.Error(), http.StatusBadRequest) return } segments := strings.Split(r.URL.Path, "/") blockId := segments[len(segments)-1] verifiedDataColumns, rpcErr := s.Blocker.DataColumns(ctx, blockId, indices) if rpcErr != nil { code := core.ErrorReasonToHTTP(rpcErr.Reason) switch code { case http.StatusBadRequest: httputil.HandleError(w, "Bad request: "+rpcErr.Err.Error(), code) return case http.StatusNotFound: httputil.HandleError(w, "Not found: "+rpcErr.Err.Error(), code) return case http.StatusInternalServerError: httputil.HandleError(w, "Internal server error: "+rpcErr.Err.Error(), code) return default: httputil.HandleError(w, rpcErr.Err.Error(), code) return } } blk, err := s.Blocker.Block(ctx, []byte(blockId)) if !shared.WriteBlockFetchError(w, blk, err) { return } if httputil.RespondWithSsz(r) { sszResp, err := buildDataColumnSidecarsSSZResponse(verifiedDataColumns) if err != nil { httputil.HandleError(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set(api.VersionHeader, version.String(blk.Version())) httputil.WriteSsz(w, sszResp) return } blkRoot, err := blk.Block().HashTreeRoot() if err != nil { httputil.HandleError(w, "Could not hash block: "+err.Error(), http.StatusInternalServerError) return } isOptimistic, err := s.OptimisticModeFetcher.IsOptimisticForRoot(ctx, blkRoot) if err != nil { httputil.HandleError(w, "Could not check if block is optimistic: "+err.Error(), http.StatusInternalServerError) return } data := buildDataColumnSidecarsJsonResponse(verifiedDataColumns) resp := &structs.GetDebugDataColumnSidecarsResponse{ Version: version.String(blk.Version()), Data: data, ExecutionOptimistic: isOptimistic, Finalized: s.FinalizationFetcher.IsFinalized(ctx, blkRoot), } w.Header().Set(api.VersionHeader, version.String(blk.Version())) httputil.WriteJson(w, resp) } // parseDataColumnIndices filters out invalid and duplicate data column indices func parseDataColumnIndices(url *url.URL) ([]int, error) { const numberOfColumns = fieldparams.NumberOfColumns rawIndices := url.Query()["indices"] indices := make([]int, 0, numberOfColumns) invalidIndices := make([]string, 0) loop: for _, raw := range rawIndices { ix, err := strconv.Atoi(raw) if err != nil { invalidIndices = append(invalidIndices, raw) continue } if !(0 <= ix && uint64(ix) < numberOfColumns) { invalidIndices = append(invalidIndices, raw) continue } for i := range indices { if ix == indices[i] { continue loop } } indices = append(indices, ix) } if len(invalidIndices) > 0 { return nil, fmt.Errorf("requested data column indices %v are invalid", invalidIndices) } return indices, nil } func buildDataColumnSidecarsJsonResponse(verifiedDataColumns []blocks.VerifiedRODataColumn) []*structs.DataColumnSidecar { sidecars := make([]*structs.DataColumnSidecar, len(verifiedDataColumns)) for i, dc := range verifiedDataColumns { column := make([]string, len(dc.Column)) for j, cell := range dc.Column { column[j] = hexutil.Encode(cell) } kzgCommitments := make([]string, len(dc.KzgCommitments)) for j, commitment := range dc.KzgCommitments { kzgCommitments[j] = hexutil.Encode(commitment) } kzgProofs := make([]string, len(dc.KzgProofs)) for j, proof := range dc.KzgProofs { kzgProofs[j] = hexutil.Encode(proof) } kzgCommitmentsInclusionProof := make([]string, len(dc.KzgCommitmentsInclusionProof)) for j, proof := range dc.KzgCommitmentsInclusionProof { kzgCommitmentsInclusionProof[j] = hexutil.Encode(proof) } sidecars[i] = &structs.DataColumnSidecar{ Index: strconv.FormatUint(dc.Index, 10), Column: column, KzgCommitments: kzgCommitments, KzgProofs: kzgProofs, SignedBeaconBlockHeader: structs.SignedBeaconBlockHeaderFromConsensus(dc.SignedBlockHeader), KzgCommitmentsInclusionProof: kzgCommitmentsInclusionProof, } } return sidecars } // buildDataColumnSidecarsSSZResponse builds SSZ response for data column sidecars func buildDataColumnSidecarsSSZResponse(verifiedDataColumns []blocks.VerifiedRODataColumn) ([]byte, error) { if len(verifiedDataColumns) == 0 { return []byte{}, nil } // Pre-allocate buffer for all sidecars using the known SSZ size sizePerSidecar := (ðpb.DataColumnSidecar{}).SizeSSZ() ssz := make([]byte, 0, sizePerSidecar*len(verifiedDataColumns)) // Marshal and append each sidecar for i, sidecar := range verifiedDataColumns { sszrep, err := sidecar.MarshalSSZ() if err != nil { return nil, errors.Wrapf(err, "failed to marshal data column sidecar at index %d", i) } ssz = append(ssz, sszrep...) } return ssz, nil }