Skip to content

Commit

Permalink
Added Heads method to Beacon API (#9135)
Browse files Browse the repository at this point in the history
  • Loading branch information
Giulio2002 authored Jan 6, 2024
1 parent 98cc1ee commit e958d35
Show file tree
Hide file tree
Showing 16 changed files with 392 additions and 35 deletions.
39 changes: 39 additions & 0 deletions cl/beacon/handler/forkchoice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package handler

import (
"encoding/json"
"net/http"

"github.com/ledgerwatch/erigon/cl/beacon/beaconhttp"
)

func (a *ApiHandler) GetEthV2DebugBeaconHeads(w http.ResponseWriter, r *http.Request) (*beaconResponse, error) {
if a.syncedData.Syncing() {
return nil, beaconhttp.NewEndpointError(http.StatusServiceUnavailable, "beacon node is syncing")
}
hash, slotNumber, err := a.forkchoiceStore.GetHead()
if err != nil {
return nil, err
}
return newBeaconResponse(
[]interface{}{
map[string]interface{}{
"slot": slotNumber,
"root": hash,
"execution_optimistic": false,
},
}), nil
}

func (a *ApiHandler) GetEthV1DebugBeaconForkChoice(w http.ResponseWriter, r *http.Request) {
justifiedCheckpoint := a.forkchoiceStore.JustifiedCheckpoint()
finalizedCheckpoint := a.forkchoiceStore.FinalizedCheckpoint()
forkNodes := a.forkchoiceStore.ForkNodes()
if err := json.NewEncoder(w).Encode(map[string]interface{}{
"justified_checkpoint": justifiedCheckpoint,
"finalized_checkpoint": finalizedCheckpoint,
"fork_choice_nodes": forkNodes,
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
80 changes: 80 additions & 0 deletions cl/beacon/handler/forkchoice_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package handler

import (
"io"
"net/http/httptest"
"testing"

libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon/cl/clparams"
"github.com/ledgerwatch/erigon/cl/cltypes/solid"
"github.com/ledgerwatch/erigon/cl/phase1/forkchoice"
"github.com/stretchr/testify/require"
)

func TestGetHeads(t *testing.T) {
// find server
_, _, _, _, p, handler, _, sm, fcu := setupTestingHandler(t, clparams.Phase0Version)
sm.OnHeadState(p)
s, cancel := sm.HeadState()
s.SetSlot(789274827847783)
cancel()

fcu.HeadSlotVal = 128
fcu.HeadVal = libcommon.Hash{1, 2, 3}
server := httptest.NewServer(handler.mux)
defer server.Close()

// get heads
resp, err := server.Client().Get(server.URL + "/eth/v2/debug/beacon/heads")
require.NoError(t, err)
defer resp.Body.Close()

require.Equal(t, 200, resp.StatusCode)
out, err := io.ReadAll(resp.Body)
require.NoError(t, err)

require.Equal(t, `{"data":[{"execution_optimistic":false,"root":"0x0102030000000000000000000000000000000000000000000000000000000000","slot":128}]}`+"\n", string(out))
}

func TestGetForkchoice(t *testing.T) {
// find server
_, _, _, _, p, handler, _, sm, fcu := setupTestingHandler(t, clparams.Phase0Version)
sm.OnHeadState(p)
s, cancel := sm.HeadState()
s.SetSlot(789274827847783)
cancel()

fcu.HeadSlotVal = 128
fcu.HeadVal = libcommon.Hash{1, 2, 3}
server := httptest.NewServer(handler.mux)
defer server.Close()

fcu.WeightsMock = []forkchoice.ForkNode{
{
BlockRoot: libcommon.Hash{1, 2, 3},
ParentRoot: libcommon.Hash{1, 2, 3},
Slot: 128,
Weight: 1,
},
{
BlockRoot: libcommon.Hash{1, 2, 2, 4, 5, 3},
ParentRoot: libcommon.Hash{1, 2, 5},
Slot: 128,
Weight: 2,
},
}

fcu.FinalizedCheckpointVal = solid.NewCheckpointFromParameters(libcommon.Hash{1, 2, 3}, 1)
fcu.JustifiedCheckpointVal = solid.NewCheckpointFromParameters(libcommon.Hash{1, 2, 3}, 2)

// get heads
resp, err := server.Client().Get(server.URL + "/eth/v1/debug/fork_choice")
require.NoError(t, err)
defer resp.Body.Close()

require.Equal(t, 200, resp.StatusCode)
out, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, `{"finalized_checkpoint":{"epoch":"1","root":"0x0102030000000000000000000000000000000000000000000000000000000000"},"fork_choice_nodes":[{"slot":"128","block_root":"0x0102030000000000000000000000000000000000000000000000000000000000","parent_root":"0x0102030000000000000000000000000000000000000000000000000000000000","justified_epoch":"0","finalized_epoch":"0","weight":"1","validity":"","execution_block_hash":"0x0000000000000000000000000000000000000000000000000000000000000000"},{"slot":"128","block_root":"0x0102020405030000000000000000000000000000000000000000000000000000","parent_root":"0x0102050000000000000000000000000000000000000000000000000000000000","justified_epoch":"0","finalized_epoch":"0","weight":"2","validity":"","execution_block_hash":"0x0000000000000000000000000000000000000000000000000000000000000000"}],"justified_checkpoint":{"epoch":"2","root":"0x0102030000000000000000000000000000000000000000000000000000000000"}}`+"\n", string(out))
}
6 changes: 5 additions & 1 deletion cl/beacon/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,11 @@ func (a *ApiHandler) init() {
// otterscn specific ones are commented as such
r.Route("/eth", func(r chi.Router) {
r.Route("/v1", func(r chi.Router) {

r.Get("/events", http.NotFound)
r.Route("/node", func(r chi.Router) {
r.Get("/health", a.GetEthV1NodeHealth)
})
r.Get("/debug/fork_choice", a.GetEthV1DebugBeaconForkChoice)
r.Route("/config", func(r chi.Router) {
r.Get("/spec", beaconhttp.HandleEndpointFunc(a.getSpec))
r.Get("/deposit_contract", beaconhttp.HandleEndpointFunc(a.getDepositContract))
Expand Down Expand Up @@ -125,6 +128,7 @@ func (a *ApiHandler) init() {
r.Route("/debug", func(r chi.Router) {
r.Route("/beacon", func(r chi.Router) {
r.Get("/states/{state_id}", beaconhttp.HandleEndpointFunc(a.getFullState))
r.Get("/heads", beaconhttp.HandleEndpointFunc(a.GetEthV2DebugBeaconHeads))
})
})
r.Route("/beacon", func(r chi.Router) {
Expand Down
20 changes: 20 additions & 0 deletions cl/beacon/handler/node.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package handler

import "net/http"

func (a *ApiHandler) GetEthV1NodeHealth(w http.ResponseWriter, r *http.Request) {
syncingStatus, err := uint64FromQueryParams(r, "syncing_status")
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
syncingCode := http.StatusOK
if syncingStatus != nil {
syncingCode = int(*syncingStatus)
}
if a.syncedData.Syncing() {
w.WriteHeader(syncingCode)
return
}
w.WriteHeader(http.StatusOK)
}
49 changes: 49 additions & 0 deletions cl/beacon/handler/node_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package handler

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/ledgerwatch/erigon/cl/clparams"
"github.com/stretchr/testify/require"
)

func TestNodeSyncing(t *testing.T) {
// i just want the correct schema to be generated
_, _, _, _, _, handler, _, _, _ := setupTestingHandler(t, clparams.Phase0Version)

// Call GET /eth/v1/node/health
server := httptest.NewServer(handler.mux)
defer server.Close()

req, err := http.NewRequest("GET", server.URL+"/eth/v1/node/health?syncing_status=666", nil)
require.NoError(t, err)

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 666, resp.StatusCode)
}

func TestNodeSyncingTip(t *testing.T) {
// i just want the correct schema to be generated
_, _, _, _, post, handler, _, sm, _ := setupTestingHandler(t, clparams.Phase0Version)

// Call GET /eth/v1/node/health
server := httptest.NewServer(handler.mux)
defer server.Close()

req, err := http.NewRequest("GET", server.URL+"/eth/v1/node/health?syncing_status=666", nil)
require.NoError(t, err)

require.NoError(t, sm.OnHeadState(post))
s, cancel := sm.HeadState()
s.SetSlot(999999999999999)
cancel()

resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
require.Equal(t, 200, resp.StatusCode)
}
5 changes: 4 additions & 1 deletion cl/beacon/synced_data/synced_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ func (s *SyncedDataManager) OnHeadState(newState *state.CachingBeaconState) (err
defer s.mu.Unlock()
if s.headState == nil {
s.headState, err = newState.Copy()
if err != nil {
return err
}
}
err = newState.CopyInto(s.headState)
if err != nil {
Expand All @@ -56,7 +59,7 @@ func (s *SyncedDataManager) Syncing() bool {
s.mu.RLock()
defer s.mu.RUnlock()
if s.headState == nil {
return false
return true
}

headEpoch := utils.GetCurrentEpoch(s.headState.GenesisTime(), s.cfg.SecondsPerSlot, s.cfg.SlotsPerEpoch)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,19 @@ func runTest(t *testing.T, blocks []*cltypes.SignedBeaconBlock, preState, postSt
}

func TestStateAntiquaryCapella(t *testing.T) {
//t.Skip()
t.Skip()
blocks, preState, postState := tests.GetCapellaRandom()
runTest(t, blocks, preState, postState)
}

func TestStateAntiquaryPhase0(t *testing.T) {
//t.Skip()
t.Skip()
blocks, preState, postState := tests.GetPhase0Random()
runTest(t, blocks, preState, postState)
}

func TestStateAntiquaryBellatrix(t *testing.T) {
//t.Skip()
t.Skip()
blocks, preState, postState := tests.GetBellatrixRandom()
runTest(t, blocks, preState, postState)
}
86 changes: 78 additions & 8 deletions cl/phase1/forkchoice/forkchoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package forkchoice

import (
"context"
"sort"
"sync"

"github.com/ledgerwatch/erigon/cl/clparams"
Expand All @@ -20,6 +21,31 @@ import (
"github.com/ledgerwatch/erigon-lib/common/length"
)

// Schema
/*
{
"slot": "1",
"block_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"justified_epoch": "1",
"finalized_epoch": "1",
"weight": "1",
"validity": "valid",
"execution_block_hash": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
"extra_data": {}
}
*/
type ForkNode struct {
Slot uint64 `json:"slot,string"`
BlockRoot libcommon.Hash `json:"block_root"`
ParentRoot libcommon.Hash `json:"parent_root"`
JustifiedEpoch uint64 `json:"justified_epoch,string"`
FinalizedEpoch uint64 `json:"finalized_epoch,string"`
Weight uint64 `json:"weight,string"`
Validity string `json:"validity"`
ExecutionBlock libcommon.Hash `json:"execution_block_hash"`
}

type checkpointComparable string

const (
Expand Down Expand Up @@ -53,17 +79,21 @@ type ForkChoiceStore struct {
unrealizedJustifiedCheckpoint solid.Checkpoint
unrealizedFinalizedCheckpoint solid.Checkpoint
proposerBoostRoot libcommon.Hash
headHash libcommon.Hash
headSlot uint64
genesisTime uint64
childrens map[libcommon.Hash]childrens
// head data
headHash libcommon.Hash
headSlot uint64
genesisTime uint64
weights map[libcommon.Hash]uint64
headSet map[libcommon.Hash]struct{}
// childrens
childrens map[libcommon.Hash]childrens

// Use go map because this is actually an unordered set
equivocatingIndicies map[uint64]struct{}
equivocatingIndicies []byte
forkGraph fork_graph.ForkGraph
// I use the cache due to the convenient auto-cleanup feauture.
checkpointStates map[checkpointComparable]*checkpointState // We keep ssz snappy of it as the full beacon state is full of rendundant data.
latestMessages map[uint64]*LatestMessage
latestMessages []LatestMessage
anchorPublicKeys []byte
// We keep track of them so that we can forkchoice with EL.
eth2Roots *lru.Cache[libcommon.Hash, libcommon.Hash] // ETH2 root -> ETH1 hash
Expand Down Expand Up @@ -163,6 +193,8 @@ func NewForkChoiceStore(ctx context.Context, anchorState *state2.CachingBeaconSt
r := solid.NewHashVector(int(anchorState.BeaconConfig().EpochsPerHistoricalVector))
anchorState.RandaoMixes().CopyTo(r)
randaoMixesLists.Add(anchorRoot, r)
headSet := make(map[libcommon.Hash]struct{})
headSet[anchorRoot] = struct{}{}
return &ForkChoiceStore{
ctx: ctx,
highestSeen: anchorState.Slot(),
Expand All @@ -172,8 +204,8 @@ func NewForkChoiceStore(ctx context.Context, anchorState *state2.CachingBeaconSt
unrealizedJustifiedCheckpoint: anchorCheckpoint.Copy(),
unrealizedFinalizedCheckpoint: anchorCheckpoint.Copy(),
forkGraph: forkGraph,
equivocatingIndicies: map[uint64]struct{}{},
latestMessages: map[uint64]*LatestMessage{},
equivocatingIndicies: make([]byte, anchorState.ValidatorLength(), anchorState.ValidatorLength()*2),
latestMessages: make([]LatestMessage, anchorState.ValidatorLength(), anchorState.ValidatorLength()*2),
checkpointStates: make(map[checkpointComparable]*checkpointState),
eth2Roots: eth2Roots,
engine: engine,
Expand All @@ -188,6 +220,8 @@ func NewForkChoiceStore(ctx context.Context, anchorState *state2.CachingBeaconSt
totalActiveBalances: totalActiveBalances,
randaoMixesLists: randaoMixesLists,
randaoDeltas: randaoDeltas,
headSet: headSet,
weights: make(map[libcommon.Hash]uint64),
participation: participation,
}, nil
}
Expand Down Expand Up @@ -399,3 +433,39 @@ func (f *ForkChoiceStore) RandaoMixes(blockRoot libcommon.Hash, out solid.HashLi
func (f *ForkChoiceStore) Partecipation(epoch uint64) (*solid.BitList, bool) {
return f.participation.Get(epoch)
}

func (f *ForkChoiceStore) ForkNodes() []ForkNode {
f.mu.Lock()
defer f.mu.Unlock()
forkNodes := make([]ForkNode, 0, len(f.weights))
for blockRoot, weight := range f.weights {
header, has := f.forkGraph.GetHeader(blockRoot)
if !has {
continue
}
justifiedCheckpoint, has := f.forkGraph.GetCurrentJustifiedCheckpoint(blockRoot)
if !has {
continue
}
finalizedCheckpoint, has := f.forkGraph.GetFinalizedCheckpoint(blockRoot)
if !has {
continue
}
blockHash, _ := f.eth2Roots.Get(blockRoot)

forkNodes = append(forkNodes, ForkNode{
Weight: weight,
BlockRoot: blockRoot,
ParentRoot: header.ParentRoot,
JustifiedEpoch: justifiedCheckpoint.Epoch(),
FinalizedEpoch: finalizedCheckpoint.Epoch(),
Slot: header.Slot,
Validity: "valid",
ExecutionBlock: blockHash,
})
}
sort.Slice(forkNodes, func(i, j int) bool {
return forkNodes[i].Slot < forkNodes[j].Slot
})
return forkNodes
}
Loading

0 comments on commit e958d35

Please sign in to comment.