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

feat: Add light_client routes #29

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
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
Loading