Skip to content

Commit

Permalink
feat: Add light_client routes
Browse files Browse the repository at this point in the history
  • Loading branch information
samcm committed Oct 22, 2024
1 parent 5cc03d2 commit 30947fa
Show file tree
Hide file tree
Showing 18 changed files with 1,927 additions and 19 deletions.
70 changes: 70 additions & 0 deletions pkg/beacon/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"

"github.com/ethpandaops/beacon/pkg/beacon/api/types"
"github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient"
"github.com/sirupsen/logrus"
)

Expand All @@ -22,6 +24,10 @@ type ConsensusClient interface {
RawDebugBeaconState(ctx context.Context, stateID string, contentType string) ([]byte, error)
DepositSnapshot(ctx context.Context) (*types.DepositSnapshot, error)
NodeIdentity(ctx context.Context) (*types.Identity, error)
LightClientBootstrap(ctx context.Context, blockRoot string) (*lightclient.Bootstrap, error)
LightClientUpdate(ctx context.Context, startPeriod, count int) (*lightclient.Update, error)
LightClientFinalityUpdate(ctx context.Context) (*lightclient.FinalityUpdate, error)
LightClientOptimisticUpdate(ctx context.Context) (*lightclient.OptimisticUpdate, error)
}

type consensusClient struct {
Expand Down Expand Up @@ -250,3 +256,67 @@ func (c *consensusClient) NodeIdentity(ctx context.Context) (*types.Identity, er

return &rsp, nil
}

func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot string) (*lightclient.Bootstrap, error) {
data, err := c.get(ctx, fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/%s", blockRoot))
if err != nil {
return nil, err
}

rsp := lightclient.Bootstrap{}
if err := json.Unmarshal(data, &rsp); err != nil {
return nil, err
}

return &rsp, nil
}

func (c *consensusClient) LightClientUpdate(ctx context.Context, startPeriod, count int) (*lightclient.Update, error) {
if count == 0 {
return nil, errors.New("count must be greater than 0")
}

params := url.Values{}
params.Add("start_period", fmt.Sprintf("%d", startPeriod))
params.Add("count", fmt.Sprintf("%d", count))

data, err := c.get(ctx, "/eth/v1/beacon/light_client/updates?"+params.Encode())
if err != nil {
return nil, err
}

rsp := lightclient.Update{}
if err := json.Unmarshal(data, &rsp); err != nil {
return nil, err
}

return &rsp, nil
}

func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*lightclient.FinalityUpdate, error) {
data, err := c.get(ctx, "/eth/v1/beacon/light_client/finality_update")
if err != nil {
return nil, err
}

rsp := lightclient.FinalityUpdate{}
if err := json.Unmarshal(data, &rsp); err != nil {
return nil, err
}

return &rsp, nil
}

func (c *consensusClient) LightClientOptimisticUpdate(ctx context.Context) (*lightclient.OptimisticUpdate, error) {
data, err := c.get(ctx, "/eth/v1/beacon/light_client/optimistic_update")
if err != nil {
return nil, err
}

rsp := lightclient.OptimisticUpdate{}
if err := json.Unmarshal(data, &rsp); err != nil {
return nil, err
}

return &rsp, nil
}
85 changes: 85 additions & 0 deletions pkg/beacon/api/types/lightclient/beacon_block_header.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package lightclient

import (
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
"strings"

"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
)

// BeaconBlockHeader represents a beacon block header.
type BeaconBlockHeader struct {
Slot phase0.Slot `json:"slot"`
ProposerIndex phase0.ValidatorIndex `json:"proposer_index"`
ParentRoot phase0.Root `json:"parent_root"`
StateRoot phase0.Root `json:"state_root"`
BodyRoot phase0.Root `json:"body_root"`
}

type beaconBlockHeaderJSON struct {
Slot string `json:"slot"`
ProposerIndex string `json:"proposer_index"`
ParentRoot string `json:"parent_root"`
StateRoot string `json:"state_root"`
BodyRoot string `json:"body_root"`
}

func (h *BeaconBlockHeader) ToJSON() beaconBlockHeaderJSON {
return beaconBlockHeaderJSON{
Slot: fmt.Sprintf("%d", h.Slot),
ProposerIndex: fmt.Sprintf("%d", h.ProposerIndex),
ParentRoot: h.ParentRoot.String(),
StateRoot: h.StateRoot.String(),
BodyRoot: h.BodyRoot.String(),
}
}

func (h *BeaconBlockHeader) FromJSON(data beaconBlockHeaderJSON) error {
slot, err := strconv.ParseUint(data.Slot, 10, 64)
if err != nil {
return errors.Wrap(err, "invalid slot")
}
h.Slot = phase0.Slot(slot)

proposerIndex, err := strconv.ParseUint(data.ProposerIndex, 10, 64)
if err != nil {
return errors.Wrap(err, "invalid proposer index")
}
h.ProposerIndex = phase0.ValidatorIndex(proposerIndex)

parentRoot, err := hex.DecodeString(strings.TrimPrefix(data.ParentRoot, "0x"))
if err != nil {
return errors.Wrap(err, "invalid parent root")
}
h.ParentRoot = phase0.Root(parentRoot)

stateRoot, err := hex.DecodeString(strings.TrimPrefix(data.StateRoot, "0x"))
if err != nil {
return errors.Wrap(err, "invalid state root")
}
h.StateRoot = phase0.Root(stateRoot)

bodyRoot, err := hex.DecodeString(strings.TrimPrefix(data.BodyRoot, "0x"))
if err != nil {
return errors.Wrap(err, "invalid body root")
}
h.BodyRoot = phase0.Root(bodyRoot)

return nil
}

func (h BeaconBlockHeader) MarshalJSON() ([]byte, error) {
return json.Marshal(h.ToJSON())
}

func (h *BeaconBlockHeader) UnmarshalJSON(data []byte) error {
var jsonData beaconBlockHeaderJSON
if err := json.Unmarshal(data, &jsonData); err != nil {
return err
}
return h.FromJSON(jsonData)
}
84 changes: 84 additions & 0 deletions pkg/beacon/api/types/lightclient/beacon_block_header_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package lightclient_test

import (
"encoding/json"
"fmt"
"reflect"
"testing"

"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestBeaconBlockHeaderMarshalUnmarshal(t *testing.T) {
testCases := []struct {
name string
header lightclient.BeaconBlockHeader
}{
{
name: "Basic BeaconBlockHeader",
header: lightclient.BeaconBlockHeader{
Slot: 1234,
ProposerIndex: 5678,
ParentRoot: phase0.Root{0x01, 0x02, 0x03},
StateRoot: phase0.Root{0x04, 0x05, 0x06},
BodyRoot: phase0.Root{0x07, 0x08, 0x09},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
marshaled, err := json.Marshal(tc.header)
if err != nil {
t.Fatalf("Failed to marshal LightClientHeader: %v", err)
}

var unmarshaled lightclient.LightClientHeader
err = json.Unmarshal(marshaled, &unmarshaled)
if err != nil {
t.Fatalf("Failed to unmarshal LightClientHeader: %v", err)
}

if !reflect.DeepEqual(tc.header, unmarshaled) {
t.Errorf("Unmarshaled LightClientHeader does not match original. Got %+v, want %+v", unmarshaled, tc.header)
}
})
}
}

func TestBeaconBlockHeaderUnmarshalJSON(t *testing.T) {
expectedSlot := "1"
expectedProposerIndex := "1"
expectedParentRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"
expectedStateRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"
expectedBodyRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"

jsonStr := `{
"slot": "` + expectedSlot + `",
"proposer_index": "` + expectedProposerIndex + `",
"parent_root": "` + expectedParentRoot + `",
"state_root": "` + expectedStateRoot + `",
"body_root": "` + expectedBodyRoot + `"
}`

var header lightclient.BeaconBlockHeader
err := json.Unmarshal([]byte(jsonStr), &header)
require.NoError(t, err)

assert.Equal(t, expectedSlot, fmt.Sprintf("%d", header.Slot))
assert.Equal(t, expectedProposerIndex, fmt.Sprintf("%d", header.ProposerIndex))
assert.Equal(t, expectedParentRoot, header.ParentRoot.String())
assert.Equal(t, expectedStateRoot, header.StateRoot.String())
assert.Equal(t, expectedBodyRoot, header.BodyRoot.String())

// Test marshalling back to JSON
marshaled, err := json.Marshal(header)
require.NoError(t, err)

var unmarshaled lightclient.BeaconBlockHeader
err = json.Unmarshal(marshaled, &unmarshaled)
require.NoError(t, err)
}
Loading

0 comments on commit 30947fa

Please sign in to comment.