Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Heads method to Beacon API #9135

Merged
merged 8 commits into from
Jan 6, 2024
Merged
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
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
Loading