From 30947fadc9a1098ef1f04f1b29f26f51723efc81 Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Tue, 22 Oct 2024 12:27:48 +1000 Subject: [PATCH 1/3] feat: Add light_client routes --- pkg/beacon/api/api.go | 70 +++++ .../types/lightclient/beacon_block_header.go | 85 ++++++ .../lightclient/beacon_block_header_test.go | 84 ++++++ pkg/beacon/api/types/lightclient/bootstrap.go | 256 ++++++++++++++++ .../api/types/lightclient/bootstrap_test.go | 88 ++++++ .../api/types/lightclient/finality_update.go | 94 ++++++ .../types/lightclient/finality_update_test.go | 130 +++++++++ pkg/beacon/api/types/lightclient/header.go | 36 +++ .../api/types/lightclient/header_test.go | 80 +++++ .../types/lightclient/optimistic_update.go | 43 +++ .../lightclient/optimsitic_update_test.go | 114 ++++++++ .../api/types/lightclient/sync_aggregate.go | 68 +++++ .../types/lightclient/sync_aggregate_test.go | 80 +++++ .../api/types/lightclient/sync_committee.go | 69 +++++ .../types/lightclient/sync_committee_test.go | 86 ++++++ pkg/beacon/api/types/lightclient/update.go | 105 +++++++ .../api/types/lightclient/update_test.go | 184 ++++++++++++ pkg/beacon/fetch.go | 274 ++++++++++++++++-- 18 files changed, 1927 insertions(+), 19 deletions(-) create mode 100644 pkg/beacon/api/types/lightclient/beacon_block_header.go create mode 100644 pkg/beacon/api/types/lightclient/beacon_block_header_test.go create mode 100644 pkg/beacon/api/types/lightclient/bootstrap.go create mode 100644 pkg/beacon/api/types/lightclient/bootstrap_test.go create mode 100644 pkg/beacon/api/types/lightclient/finality_update.go create mode 100644 pkg/beacon/api/types/lightclient/finality_update_test.go create mode 100644 pkg/beacon/api/types/lightclient/header.go create mode 100644 pkg/beacon/api/types/lightclient/header_test.go create mode 100644 pkg/beacon/api/types/lightclient/optimistic_update.go create mode 100644 pkg/beacon/api/types/lightclient/optimsitic_update_test.go create mode 100644 pkg/beacon/api/types/lightclient/sync_aggregate.go create mode 100644 pkg/beacon/api/types/lightclient/sync_aggregate_test.go create mode 100644 pkg/beacon/api/types/lightclient/sync_committee.go create mode 100644 pkg/beacon/api/types/lightclient/sync_committee_test.go create mode 100644 pkg/beacon/api/types/lightclient/update.go create mode 100644 pkg/beacon/api/types/lightclient/update_test.go diff --git a/pkg/beacon/api/api.go b/pkg/beacon/api/api.go index f6f9e91..8a7d7d0 100644 --- a/pkg/beacon/api/api.go +++ b/pkg/beacon/api/api.go @@ -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" ) @@ -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 { @@ -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 +} diff --git a/pkg/beacon/api/types/lightclient/beacon_block_header.go b/pkg/beacon/api/types/lightclient/beacon_block_header.go new file mode 100644 index 0000000..e837098 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/beacon_block_header.go @@ -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) +} diff --git a/pkg/beacon/api/types/lightclient/beacon_block_header_test.go b/pkg/beacon/api/types/lightclient/beacon_block_header_test.go new file mode 100644 index 0000000..570ab6d --- /dev/null +++ b/pkg/beacon/api/types/lightclient/beacon_block_header_test.go @@ -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) +} diff --git a/pkg/beacon/api/types/lightclient/bootstrap.go b/pkg/beacon/api/types/lightclient/bootstrap.go new file mode 100644 index 0000000..acbd4b1 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/bootstrap.go @@ -0,0 +1,256 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// Bootstrap is a light client bootstrap. +type Bootstrap struct { + Header BootstrapHeader `json:"header"` + CurrentSyncCommittee BootstrapCurrentSyncCommittee `json:"current_sync_committee"` + CurrentSyncCommitteeBranch []phase0.Root `json:"current_sync_committee_branch"` +} + +// bootstrapJSON is the JSON representation of a bootstrap +type bootstrapJSON struct { + Header bootstrapHeaderJSON `json:"header"` + CurrentSyncCommittee bootstrapCurrentSyncCommitteeJSON `json:"current_sync_committee"` + CurrentSyncCommitteeBranch bootstrapCurrentSyncCommitteeBranchJSON `json:"current_sync_committee_branch"` +} + +// BootstrapHeader is the header of a light client bootstrap. +type BootstrapHeader struct { + Slot phase0.Slot + ProposerIndex phase0.ValidatorIndex + ParentRoot phase0.Root + StateRoot phase0.Root + BodyRoot phase0.Root +} + +// bootstrapHeaderJSON is the JSON representation of a bootstrap header. +type bootstrapHeaderJSON 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"` +} + +// BootstrapCurrentSyncCommittee is the current sync committee of a light client bootstrap. +type BootstrapCurrentSyncCommittee struct { + Pubkeys []phase0.BLSPubKey + AggregatePubkey phase0.BLSPubKey +} + +// bootstrapCurrentSyncCommitteeJSON is the JSON representation of a bootstrap current sync committee. +type bootstrapCurrentSyncCommitteeJSON struct { + Pubkeys []string `json:"pubkeys"` + AggregatePubkey string `json:"aggregate_pubkey"` +} + +// bootstrapCurrentSyncCommitteeBranchJSON is the JSON representation of a bootstrap current sync committee branch. +type bootstrapCurrentSyncCommitteeBranchJSON []string + +func (b Bootstrap) MarshalJSON() ([]byte, error) { + pubkeys := make([]string, len(b.CurrentSyncCommittee.Pubkeys)) + for i, pubkey := range b.CurrentSyncCommittee.Pubkeys { + pubkeys[i] = pubkey.String() + } + + branch := make([]string, len(b.CurrentSyncCommitteeBranch)) + for i, root := range b.CurrentSyncCommitteeBranch { + branch[i] = root.String() + } + + return json.Marshal(&bootstrapJSON{ + Header: bootstrapHeaderJSON{ + Slot: fmt.Sprintf("%d", b.Header.Slot), + ProposerIndex: fmt.Sprintf("%d", b.Header.ProposerIndex), + ParentRoot: b.Header.ParentRoot.String(), + StateRoot: b.Header.StateRoot.String(), + BodyRoot: b.Header.BodyRoot.String(), + }, + CurrentSyncCommittee: bootstrapCurrentSyncCommitteeJSON{ + Pubkeys: pubkeys, + AggregatePubkey: b.CurrentSyncCommittee.AggregatePubkey.String(), + }, + CurrentSyncCommitteeBranch: branch, + }) +} + +func (b *Bootstrap) UnmarshalJSON(input []byte) error { + var err error + + var jsonData bootstrapJSON + if err = json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if jsonData.Header.Slot == "" { + return errors.New("slot is required") + } + + slot, err := strconv.ParseUint(jsonData.Header.Slot, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid slot: %s", jsonData.Header.Slot)) + } + b.Header.Slot = phase0.Slot(slot) + + if jsonData.Header.ProposerIndex == "" { + return errors.New("proposer index is required") + } + + proposerIndex, err := strconv.ParseUint(jsonData.Header.ProposerIndex, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid proposer index: %s", jsonData.Header.ProposerIndex)) + } + b.Header.ProposerIndex = phase0.ValidatorIndex(proposerIndex) + + if jsonData.Header.ParentRoot == "" { + return errors.New("parent root is required") + } + + parentRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.ParentRoot, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid parent root: %s", jsonData.Header.ParentRoot)) + } + b.Header.ParentRoot = phase0.Root(parentRoot) + + if jsonData.Header.StateRoot == "" { + return errors.New("state root is required") + } + + stateRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.StateRoot, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid state root: %s", jsonData.Header.StateRoot)) + } + b.Header.StateRoot = phase0.Root(stateRoot) + + if jsonData.Header.BodyRoot == "" { + return errors.New("body root is required") + } + + bodyRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.BodyRoot, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid body root: %s", jsonData.Header.BodyRoot)) + } + b.Header.BodyRoot = phase0.Root(bodyRoot) + + if len(jsonData.CurrentSyncCommitteeBranch) == 0 { + return errors.New("current sync committee branch is required") + } + + if len(jsonData.CurrentSyncCommittee.Pubkeys) == 0 { + return errors.New("current sync committee pubkeys are required") + } + + pubkeys := make([]phase0.BLSPubKey, len(jsonData.CurrentSyncCommittee.Pubkeys)) + for i, pubkey := range jsonData.CurrentSyncCommittee.Pubkeys { + pubkeyBytes, err := hex.DecodeString(strings.TrimPrefix(pubkey, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid pubkey: %s", pubkey)) + } + + pubkeys[i] = phase0.BLSPubKey(pubkeyBytes) + } + b.CurrentSyncCommittee.Pubkeys = pubkeys + + if jsonData.CurrentSyncCommittee.AggregatePubkey == "" { + return errors.New("current sync committee aggregate pubkey is required") + } + + aggregatePubkeyBytes, err := hex.DecodeString(strings.TrimPrefix(jsonData.CurrentSyncCommittee.AggregatePubkey, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid aggregate pubkey: %s", jsonData.CurrentSyncCommittee.AggregatePubkey)) + } + b.CurrentSyncCommittee.AggregatePubkey = phase0.BLSPubKey(aggregatePubkeyBytes) + + branch := make([]phase0.Root, len(jsonData.CurrentSyncCommitteeBranch)) + for i, root := range jsonData.CurrentSyncCommitteeBranch { + r, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid root: %s", root)) + } + + branch[i] = phase0.Root(r) + } + + b.CurrentSyncCommitteeBranch = branch + + return nil +} + +func (b *BootstrapHeader) UnmarshalJSON(input []byte) error { + var err error + + var jsonData bootstrapHeaderJSON + if err = json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + slot, err := strconv.ParseUint(jsonData.Slot, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid slot: %s", jsonData.Slot)) + } + b.Slot = phase0.Slot(slot) + + proposerIndex, err := strconv.ParseUint(jsonData.ProposerIndex, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid proposer index: %s", jsonData.ProposerIndex)) + } + b.ProposerIndex = phase0.ValidatorIndex(proposerIndex) + + parentRoot, err := hex.DecodeString(jsonData.ParentRoot) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid parent root: %s", jsonData.ParentRoot)) + } + b.ParentRoot = phase0.Root(parentRoot) + + stateRoot, err := hex.DecodeString(jsonData.StateRoot) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid state root: %s", jsonData.StateRoot)) + } + b.StateRoot = phase0.Root(stateRoot) + + bodyRoot, err := hex.DecodeString(jsonData.BodyRoot) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid body root: %s", jsonData.BodyRoot)) + } + b.BodyRoot = phase0.Root(bodyRoot) + + return nil +} + +func (b *BootstrapCurrentSyncCommittee) UnmarshalJSON(input []byte) error { + var err error + + var jsonData bootstrapCurrentSyncCommitteeJSON + if err = json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + b.Pubkeys = make([]phase0.BLSPubKey, len(jsonData.Pubkeys)) + for i, pubkey := range jsonData.Pubkeys { + pubkeyBytes, err := hex.DecodeString(pubkey) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid pubkey: %s", pubkey)) + } + + b.Pubkeys[i] = phase0.BLSPubKey(pubkeyBytes) + } + + aggregatePubkeyBytes, err := hex.DecodeString(jsonData.AggregatePubkey) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid aggregate pubkey: %s", jsonData.AggregatePubkey)) + } + b.AggregatePubkey = phase0.BLSPubKey(aggregatePubkeyBytes) + + return nil +} diff --git a/pkg/beacon/api/types/lightclient/bootstrap_test.go b/pkg/beacon/api/types/lightclient/bootstrap_test.go new file mode 100644 index 0000000..ba899ff --- /dev/null +++ b/pkg/beacon/api/types/lightclient/bootstrap_test.go @@ -0,0 +1,88 @@ +package lightclient_test + +import ( + "encoding/json" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/stretchr/testify/require" +) + +func TestBootstrap_MarshalJSON(t *testing.T) { + bootstrap := &lightclient.Bootstrap{ + Header: lightclient.BootstrapHeader{ + Slot: 123, + ProposerIndex: 456, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, + CurrentSyncCommittee: lightclient.BootstrapCurrentSyncCommittee{ + Pubkeys: []phase0.BLSPubKey{{0x04}, {0x05}}, + AggregatePubkey: phase0.BLSPubKey{0x06}, + }, + CurrentSyncCommitteeBranch: []phase0.Root{{0x07}, {0x08}}, + } + + jsonData, err := json.Marshal(bootstrap) + require.NoError(t, err) + + expectedJSON := `{ + "header": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee": { + "pubkeys": [ + "0x040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0x050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "aggregate_pubkey": "0x060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee_branch": [ + "0x0700000000000000000000000000000000000000000000000000000000000000", + "0x0800000000000000000000000000000000000000000000000000000000000000" + ] + }` + require.JSONEq(t, expectedJSON, string(jsonData)) +} + +func TestBootstrap_UnmarshalJSON(t *testing.T) { + jsonData := []byte(`{ + "header": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee": { + "pubkeys": [ + "0x040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "0x050000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ], + "aggregate_pubkey": "0x060000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "current_sync_committee_branch": [ + "0x0700000000000000000000000000000000000000000000000000000000000000", + "0x0800000000000000000000000000000000000000000000000000000000000000" + ] + }`) + + var bootstrap lightclient.Bootstrap + err := json.Unmarshal(jsonData, &bootstrap) + require.NoError(t, err) + + require.Equal(t, phase0.Slot(123), bootstrap.Header.Slot) + require.Equal(t, phase0.ValidatorIndex(456), bootstrap.Header.ProposerIndex) + require.Equal(t, phase0.Root{0x01}, bootstrap.Header.ParentRoot) + require.Equal(t, phase0.Root{0x02}, bootstrap.Header.StateRoot) + require.Equal(t, phase0.Root{0x03}, bootstrap.Header.BodyRoot) + require.Equal(t, []phase0.BLSPubKey{{0x04}, {0x05}}, bootstrap.CurrentSyncCommittee.Pubkeys) + require.Equal(t, phase0.BLSPubKey{0x06}, bootstrap.CurrentSyncCommittee.AggregatePubkey) + require.Equal(t, []phase0.Root{{0x07}, {0x08}}, bootstrap.CurrentSyncCommitteeBranch) +} diff --git a/pkg/beacon/api/types/lightclient/finality_update.go b/pkg/beacon/api/types/lightclient/finality_update.go new file mode 100644 index 0000000..5e4d08d --- /dev/null +++ b/pkg/beacon/api/types/lightclient/finality_update.go @@ -0,0 +1,94 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// FinalityUpdate represents a finality update for light clients. +type FinalityUpdate struct { + AttestedHeader LightClientHeader + FinalizedHeader LightClientHeader + FinalityBranch []phase0.Root + SyncAggregate SyncAggregate + SignatureSlot phase0.Slot +} + +type finalityUpdateJSON struct { + AttestedHeader lightClientHeaderJSON `json:"attested_header"` + FinalizedHeader lightClientHeaderJSON `json:"finalized_header"` + FinalityBranch []string `json:"finality_branch"` + SyncAggregate syncAggregateJSON `json:"sync_aggregate"` + SignatureSlot string `json:"signature_slot"` +} + +func (f *FinalityUpdate) UnmarshalJSON(data []byte) error { + var jsonData finalityUpdateJSON + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return f.FromJSON(jsonData) +} + +func (f *FinalityUpdate) FromJSON(data finalityUpdateJSON) error { + attestedHeader := LightClientHeader{} + if err := attestedHeader.FromJSON(data.AttestedHeader); err != nil { + return errors.Wrap(err, "failed to unmarshal attested header") + } + f.AttestedHeader = attestedHeader + + finalizedHeader := LightClientHeader{} + if err := finalizedHeader.FromJSON(data.FinalizedHeader); err != nil { + return errors.Wrap(err, "failed to unmarshal finalized header") + } + f.FinalizedHeader = finalizedHeader + + finalityBranch := make([]phase0.Root, len(data.FinalityBranch)) + for i, root := range data.FinalityBranch { + decoded, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to unmarshal finality branch at index %d: %s", i, root)) + } + finalityBranch[i] = phase0.Root(decoded) + } + f.FinalityBranch = finalityBranch + + syncAggregate := SyncAggregate{} + if err := syncAggregate.FromJSON(data.SyncAggregate); err != nil { + return errors.Wrap(err, "failed to unmarshal sync aggregate") + } + f.SyncAggregate = syncAggregate + + signatureSlot, err := strconv.ParseUint(data.SignatureSlot, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("failed to unmarshal signature slot: %s", data.SignatureSlot)) + } + f.SignatureSlot = phase0.Slot(signatureSlot) + + return nil +} + +func (f FinalityUpdate) MarshalJSON() ([]byte, error) { + return json.Marshal(f.ToJSON()) +} + +func (f *FinalityUpdate) ToJSON() finalityUpdateJSON { + finalityBranch := make([]string, len(f.FinalityBranch)) + for i, root := range f.FinalityBranch { + finalityBranch[i] = fmt.Sprintf("%x", root) + } + + return finalityUpdateJSON{ + AttestedHeader: f.AttestedHeader.ToJSON(), + FinalizedHeader: f.FinalizedHeader.ToJSON(), + FinalityBranch: finalityBranch, + SyncAggregate: f.SyncAggregate.ToJSON(), + SignatureSlot: fmt.Sprintf("%d", f.SignatureSlot), + } +} diff --git a/pkg/beacon/api/types/lightclient/finality_update_test.go b/pkg/beacon/api/types/lightclient/finality_update_test.go new file mode 100644 index 0000000..51cc755 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/finality_update_test.go @@ -0,0 +1,130 @@ +package lightclient + +import ( + "encoding/json" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFinalityUpdateMarshalUnmarshal(t *testing.T) { + originalUpdate := &FinalityUpdate{ + AttestedHeader: LightClientHeader{ + Beacon: BeaconBlockHeader{ + Slot: 123, + ProposerIndex: 456, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, + }, + FinalizedHeader: LightClientHeader{ + Beacon: BeaconBlockHeader{ + Slot: 789, + ProposerIndex: 101, + ParentRoot: phase0.Root{0x04}, + StateRoot: phase0.Root{0x05}, + BodyRoot: phase0.Root{0x06}, + }, + }, + FinalityBranch: []phase0.Root{{01, 02}, {03, 04}}, + SyncAggregate: SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{1, 1, 1, 0, 0, 1}, + SyncCommitteeSignature: [96]byte{0x0a}, + }, + SignatureSlot: 1234, + } + + // Marshal to JSON + jsonData, err := json.Marshal(originalUpdate) + require.NoError(t, err) + + // Unmarshal from JSON + var unmarshaledUpdate FinalityUpdate + err = json.Unmarshal(jsonData, &unmarshaledUpdate) + require.NoError(t, err) + + // Compare original and unmarshaled data + assert.Equal(t, originalUpdate.AttestedHeader, unmarshaledUpdate.AttestedHeader) + assert.Equal(t, originalUpdate.FinalizedHeader, unmarshaledUpdate.FinalizedHeader) + assert.Equal(t, originalUpdate.FinalityBranch, unmarshaledUpdate.FinalityBranch) + assert.Equal(t, originalUpdate.SyncAggregate, unmarshaledUpdate.SyncAggregate) + assert.Equal(t, originalUpdate.SignatureSlot, unmarshaledUpdate.SignatureSlot) +} + +func TestFinalityUpdateUnmarshalPhase0(t *testing.T) { + expectedRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + + jsonData := []byte(` + { + "attested_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "finalized_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "finality_branch": [ + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `" + ], + "sync_aggregate": { + "sync_committee_bits": "0x01", + "sync_committee_signature": "` + expectedSignature + `" + }, + "signature_slot": "1" + }`) + + var update FinalityUpdate + err := json.Unmarshal(jsonData, &update) + require.NoError(t, err) + + assert.Equal(t, phase0.Slot(1), update.AttestedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.AttestedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.BodyRoot.String()) + + assert.Equal(t, phase0.Slot(1), update.FinalizedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.FinalizedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.BodyRoot.String()) + + assert.Len(t, update.FinalityBranch, 6) + for _, root := range update.FinalityBranch { + assert.Equal(t, expectedRoot, root.String()) + } + + assert.Equal(t, bitfield.Bitvector512{1}, update.SyncAggregate.SyncCommitteeBits) + assert.Equal(t, expectedSignature, update.SyncAggregate.SyncCommitteeSignature.String()) + + assert.Equal(t, phase0.Slot(1), update.SignatureSlot) + + // Test marshalling back to JSON + marshaled, err := json.Marshal(update) + require.NoError(t, err) + + var unmarshaled FinalityUpdate + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err) +} diff --git a/pkg/beacon/api/types/lightclient/header.go b/pkg/beacon/api/types/lightclient/header.go new file mode 100644 index 0000000..987914e --- /dev/null +++ b/pkg/beacon/api/types/lightclient/header.go @@ -0,0 +1,36 @@ +package lightclient + +import ( + "encoding/json" +) + +// LightClientHeader represents a light client header. +type LightClientHeader struct { + Beacon BeaconBlockHeader `json:"beacon"` +} + +type lightClientHeaderJSON struct { + Beacon beaconBlockHeaderJSON `json:"beacon"` +} + +func (h *LightClientHeader) ToJSON() lightClientHeaderJSON { + return lightClientHeaderJSON{ + Beacon: h.Beacon.ToJSON(), + } +} + +func (h *LightClientHeader) FromJSON(data lightClientHeaderJSON) error { + return h.Beacon.FromJSON(data.Beacon) +} + +func (h LightClientHeader) MarshalJSON() ([]byte, error) { + return json.Marshal(h.ToJSON()) +} + +func (h *LightClientHeader) UnmarshalJSON(data []byte) error { + var jsonData lightClientHeaderJSON + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return h.FromJSON(jsonData) +} diff --git a/pkg/beacon/api/types/lightclient/header_test.go b/pkg/beacon/api/types/lightclient/header_test.go new file mode 100644 index 0000000..73f1d69 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/header_test.go @@ -0,0 +1,80 @@ +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 TestLightClientHeaderMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + header lightclient.LightClientHeader + }{ + { + name: "Basic LightClientHeader", + header: lightclient.LightClientHeader{ + Beacon: 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 TestLightClientHeaderUnmarshalJSON(t *testing.T) { + expectedSlot := "1" + expectedProposerIndex := "1" + expectedParentRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedStateRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedBodyRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + + jsonStr := `{ + "beacon": { + "slot": "` + expectedSlot + `", + "proposer_index": "` + expectedProposerIndex + `", + "parent_root": "` + expectedParentRoot + `", + "state_root": "` + expectedStateRoot + `", + "body_root": "` + expectedBodyRoot + `" + } + }` + + var header lightclient.LightClientHeader + err := json.Unmarshal([]byte(jsonStr), &header) + require.NoError(t, err) + + assert.Equal(t, expectedSlot, fmt.Sprintf("%d", header.Beacon.Slot)) + assert.Equal(t, expectedProposerIndex, fmt.Sprintf("%d", header.Beacon.ProposerIndex)) + assert.Equal(t, expectedParentRoot, header.Beacon.ParentRoot.String()) + assert.Equal(t, expectedStateRoot, header.Beacon.StateRoot.String()) + assert.Equal(t, expectedBodyRoot, header.Beacon.BodyRoot.String()) +} diff --git a/pkg/beacon/api/types/lightclient/optimistic_update.go b/pkg/beacon/api/types/lightclient/optimistic_update.go new file mode 100644 index 0000000..2340b6b --- /dev/null +++ b/pkg/beacon/api/types/lightclient/optimistic_update.go @@ -0,0 +1,43 @@ +package lightclient + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// OptimisticUpdate represents a light client optimistic update. +type OptimisticUpdate struct { + AttestedHeader LightClientHeader `json:"attested_header"` + SyncAggregate SyncAggregate `json:"sync_aggregate"` +} + +// optimisticUpdateJSON is the JSON representation of an optimistic update +type optimisticUpdateJSON struct { + AttestedHeader lightClientHeaderJSON `json:"attested_header"` + SyncAggregate syncAggregateJSON `json:"sync_aggregate"` +} + +func (u OptimisticUpdate) MarshalJSON() ([]byte, error) { + return json.Marshal(&optimisticUpdateJSON{ + AttestedHeader: u.AttestedHeader.ToJSON(), + SyncAggregate: u.SyncAggregate.ToJSON(), + }) +} + +func (u *OptimisticUpdate) UnmarshalJSON(input []byte) error { + var jsonData optimisticUpdateJSON + if err := json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if err := u.AttestedHeader.FromJSON(jsonData.AttestedHeader); err != nil { + return errors.Wrap(err, "invalid attested header") + } + + if err := u.SyncAggregate.FromJSON(jsonData.SyncAggregate); err != nil { + return errors.Wrap(err, "invalid sync aggregate") + } + + return nil +} diff --git a/pkg/beacon/api/types/lightclient/optimsitic_update_test.go b/pkg/beacon/api/types/lightclient/optimsitic_update_test.go new file mode 100644 index 0000000..c1cf366 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/optimsitic_update_test.go @@ -0,0 +1,114 @@ +package lightclient_test + +import ( + "fmt" + "testing" + + "encoding/json" + "reflect" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestOptimisticUpdateMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + update lightclient.OptimisticUpdate + }{ + { + name: "Basic Update", + update: lightclient.OptimisticUpdate{ + AttestedHeader: lightclient.LightClientHeader{ + Beacon: lightclient.BeaconBlockHeader{ + Slot: 1234, + ProposerIndex: 5678, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, + }, + SyncAggregate: lightclient.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{0, 1}, + SyncCommitteeSignature: [96]byte{0x0c}, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + marshaled, err := json.Marshal(tc.update) + if err != nil { + t.Fatalf("Failed to marshal Update: %v", err) + } + + // Unmarshal + var unmarshaled lightclient.OptimisticUpdate + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal Update: %v", err) + } + + // Compare + if !reflect.DeepEqual(tc.update, unmarshaled) { + t.Errorf("Unmarshaled Update does not match original. Got %+v, want %+v", unmarshaled, tc.update) + } + }) + } +} + +func TestOptimisticUpdateUnmarshalJSON(t *testing.T) { + expectedRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + expectedBits := "0x01" + + jsonStr := `{ + "attested_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "sync_aggregate": { + "sync_committee_bits": "` + expectedBits + `", + "sync_committee_signature": "` + expectedSignature + `" + } + }` + + var update lightclient.OptimisticUpdate + err := json.Unmarshal([]byte(jsonStr), &update) + require.NoError(t, err) + + // Check all fields manually + assert.Equal(t, phase0.Slot(1), update.AttestedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.AttestedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.BodyRoot.String()) + + assert.Equal(t, expectedBits, fmt.Sprintf("%#x", update.SyncAggregate.SyncCommitteeBits.Bytes())) + + assert.Equal(t, expectedSignature, update.SyncAggregate.SyncCommitteeSignature.String()) + + // Marshal back to JSON + marshaledJSON, err := json.Marshal(update) + require.NoError(t, err) + + // Unmarshal both JSONs to interfaces for comparison + var originalData, remarshaledData interface{} + err = json.Unmarshal([]byte(jsonStr), &originalData) + require.NoError(t, err) + err = json.Unmarshal(marshaledJSON, &remarshaledData) + require.NoError(t, err) + + // Compare the unmarshaled data + assert.Equal(t, originalData, remarshaledData, "Remarshaled JSON does not match the original") +} diff --git a/pkg/beacon/api/types/lightclient/sync_aggregate.go b/pkg/beacon/api/types/lightclient/sync_aggregate.go new file mode 100644 index 0000000..b0af754 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_aggregate.go @@ -0,0 +1,68 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" + "github.com/prysmaticlabs/go-bitfield" +) + +// SyncAggregate represents a sync aggregate. +type SyncAggregate struct { + SyncCommitteeBits bitfield.Bitvector512 `json:"sync_committee_bits"` + SyncCommitteeSignature phase0.BLSSignature `json:"sync_committee_signature"` +} + +type syncAggregateJSON struct { + SyncCommitteeBits string `json:"sync_committee_bits"` + SyncCommitteeSignature string `json:"sync_committee_signature"` +} + +func (s *SyncAggregate) ToJSON() syncAggregateJSON { + return syncAggregateJSON{ + SyncCommitteeBits: fmt.Sprintf("%#x", s.SyncCommitteeBits.Bytes()), + SyncCommitteeSignature: fmt.Sprintf("%#x", s.SyncCommitteeSignature), + } +} + +func (s *SyncAggregate) FromJSON(data syncAggregateJSON) error { + if data.SyncCommitteeBits == "" { + return errors.New("sync committee bits are required") + } + + if data.SyncCommitteeSignature == "" { + return errors.New("sync committee signature is required") + } + + bits, err := hex.DecodeString(strings.TrimPrefix(data.SyncCommitteeBits, "0x")) + if err != nil { + return errors.Wrap(err, "invalid sync committee bits") + } + + s.SyncCommitteeBits = bitfield.Bitvector512(bits) + + signature, err := hex.DecodeString(strings.TrimPrefix(data.SyncCommitteeSignature, "0x")) + if err != nil { + return errors.Wrap(err, "invalid sync committee signature") + } + s.SyncCommitteeSignature = phase0.BLSSignature(signature) + + return nil +} + +func (s SyncAggregate) MarshalJSON() ([]byte, error) { + return json.Marshal(s.ToJSON()) +} + +func (s *SyncAggregate) UnmarshalJSON(input []byte) error { + var data syncAggregateJSON + if err := json.Unmarshal(input, &data); err != nil { + return errors.Wrap(err, "failed to unmarshal sync aggregate") + } + + return s.FromJSON(data) +} diff --git a/pkg/beacon/api/types/lightclient/sync_aggregate_test.go b/pkg/beacon/api/types/lightclient/sync_aggregate_test.go new file mode 100644 index 0000000..3d42128 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_aggregate_test.go @@ -0,0 +1,80 @@ +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/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSyncAggregateMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + syncAggregate lightclient.SyncAggregate + }{ + { + name: "Basic SyncAggregate", + syncAggregate: lightclient.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{0, 1, 0, 1, 0}, + SyncCommitteeSignature: phase0.BLSSignature{0x03, 0x04}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + marshaled, err := json.Marshal(tc.syncAggregate) + if err != nil { + t.Fatalf("Failed to marshal SyncAggregate: %v", err) + } + + // Unmarshal + var unmarshaled lightclient.SyncAggregate + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal SyncAggregate: %v", err) + } + + // Compare + if !reflect.DeepEqual(tc.syncAggregate, unmarshaled) { + t.Errorf("Unmarshaled SyncAggregate does not match original. Got %+v, want %+v", unmarshaled, tc.syncAggregate) + } + }) + } +} + +func TestSyncAggregateUnmarshalJSON(t *testing.T) { + expectedBits := "0x01" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + jsonStr := ` + { + "sync_committee_bits": "` + expectedBits + `", + "sync_committee_signature": "` + expectedSignature + `" + } + ` + + var syncAggregate lightclient.SyncAggregate + err := json.Unmarshal([]byte(jsonStr), &syncAggregate) + require.NoError(t, err) + + assert.Equal(t, expectedBits, fmt.Sprintf("%#x", syncAggregate.SyncCommitteeBits.Bytes())) + assert.Equal(t, expectedSignature, fmt.Sprintf("%#x", syncAggregate.SyncCommitteeSignature)) + + // Test marshalling back to JSON + marshaled, err := json.Marshal(syncAggregate) + require.NoError(t, err) + + var unmarshaled lightclient.SyncAggregate + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err) + + assert.Equal(t, expectedBits, fmt.Sprintf("%#x", unmarshaled.SyncCommitteeBits.Bytes())) + assert.Equal(t, expectedSignature, fmt.Sprintf("%#x", unmarshaled.SyncCommitteeSignature)) +} diff --git a/pkg/beacon/api/types/lightclient/sync_committee.go b/pkg/beacon/api/types/lightclient/sync_committee.go new file mode 100644 index 0000000..3ef9cd8 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_committee.go @@ -0,0 +1,69 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// SyncCommittee represents a sync committee. +type SyncCommittee struct { + Pubkeys []phase0.BLSPubKey `json:"pubkeys"` + AggregatePubkey phase0.BLSPubKey `json:"aggregate_pubkey"` +} + +// syncCommitteeJSON is the JSON representation of a sync committee. +type syncCommitteeJSON struct { + Pubkeys []string `json:"pubkeys"` + AggregatePubkey string `json:"aggregate_pubkey"` +} + +// ToJSON converts a SyncCommittee to its JSON representation. +func (s *SyncCommittee) ToJSON() syncCommitteeJSON { + pubkeys := make([]string, len(s.Pubkeys)) + for i, pubkey := range s.Pubkeys { + pubkeys[i] = fmt.Sprintf("%#x", pubkey) + } + return syncCommitteeJSON{ + Pubkeys: pubkeys, + AggregatePubkey: fmt.Sprintf("%#x", s.AggregatePubkey), + } +} + +// FromJSON converts a JSON representation of a SyncCommittee to a SyncCommittee. +func (s *SyncCommittee) FromJSON(data syncCommitteeJSON) error { + s.Pubkeys = make([]phase0.BLSPubKey, len(data.Pubkeys)) + for i, pubkey := range data.Pubkeys { + pk, err := hex.DecodeString(strings.TrimPrefix(pubkey, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid pubkey: %s", pubkey)) + } + copy(s.Pubkeys[i][:], pk) + } + + aggregatePubkey, err := hex.DecodeString(strings.TrimPrefix(data.AggregatePubkey, "0x")) + if err != nil { + return errors.Wrap(err, "invalid aggregate pubkey") + } + copy(s.AggregatePubkey[:], aggregatePubkey) + + return nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (s SyncCommittee) MarshalJSON() ([]byte, error) { + return json.Marshal(s.ToJSON()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface. +func (s *SyncCommittee) UnmarshalJSON(data []byte) error { + var jsonData syncCommitteeJSON + if err := json.Unmarshal(data, &jsonData); err != nil { + return err + } + return s.FromJSON(jsonData) +} diff --git a/pkg/beacon/api/types/lightclient/sync_committee_test.go b/pkg/beacon/api/types/lightclient/sync_committee_test.go new file mode 100644 index 0000000..21293c0 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/sync_committee_test.go @@ -0,0 +1,86 @@ +package lightclient + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSyncCommitteeMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + syncCommittee SyncCommittee + }{ + { + name: "Basic SyncCommittee", + syncCommittee: SyncCommittee{ + Pubkeys: []phase0.BLSPubKey{ + {0x01, 0x23, 0x45}, + {0x67, 0x89, 0xab}, + }, + AggregatePubkey: phase0.BLSPubKey{0xcd, 0xef, 0x01}, + }, + }, + { + name: "Empty SyncCommittee", + syncCommittee: SyncCommittee{ + Pubkeys: []phase0.BLSPubKey{}, + AggregatePubkey: phase0.BLSPubKey{}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + marshaled, err := json.Marshal(tc.syncCommittee) + if err != nil { + t.Fatalf("Failed to marshal SyncCommittee: %v", err) + } + + var unmarshaled SyncCommittee + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal SyncCommittee: %v", err) + } + + if !reflect.DeepEqual(tc.syncCommittee, unmarshaled) { + t.Errorf("Unmarshaled SyncCommittee does not match original. Got %+v, want %+v", unmarshaled, tc.syncCommittee) + } + }) + } +} + +func TestSyncCommitteeUnmarshalJSON(t *testing.T) { + expectedPubkey := "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + + jsonStr := ` + { + "pubkeys": [ + "` + expectedPubkey + `", + "` + expectedPubkey + `" + ], + "aggregate_pubkey": "` + expectedPubkey + `" + } + ` + + var syncCommittee SyncCommittee + err := json.Unmarshal([]byte(jsonStr), &syncCommittee) + require.NoError(t, err) + + assert.Equal(t, expectedPubkey, syncCommittee.AggregatePubkey.String()) + for _, pubkey := range syncCommittee.Pubkeys { + assert.Equal(t, expectedPubkey, pubkey.String()) + } + + // Test marshalling back to JSON + marshaled, err := json.Marshal(syncCommittee) + require.NoError(t, err) + + var unmarshaled SyncCommittee + err = json.Unmarshal(marshaled, &unmarshaled) + require.NoError(t, err) +} diff --git a/pkg/beacon/api/types/lightclient/update.go b/pkg/beacon/api/types/lightclient/update.go new file mode 100644 index 0000000..35b7753 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/update.go @@ -0,0 +1,105 @@ +package lightclient + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/pkg/errors" +) + +// Update represents a light client update. +type Update struct { + AttestedHeader LightClientHeader `json:"attested_header"` + NextSyncCommittee SyncCommittee `json:"next_sync_committee"` + NextSyncCommitteeBranch []phase0.Root `json:"next_sync_committee_branch"` + FinalizedHeader LightClientHeader `json:"finalized_header"` + FinalityBranch []phase0.Root `json:"finality_branch"` + SyncAggregate SyncAggregate `json:"sync_aggregate"` + SignatureSlot phase0.Slot `json:"signature_slot"` +} + +// updateJSON is the JSON representation of an update +type updateJSON struct { + AttestedHeader lightClientHeaderJSON `json:"attested_header"` + NextSyncCommittee syncCommitteeJSON `json:"next_sync_committee"` + NextSyncCommitteeBranch []string `json:"next_sync_committee_branch"` + FinalizedHeader lightClientHeaderJSON `json:"finalized_header"` + FinalityBranch []string `json:"finality_branch"` + SyncAggregate syncAggregateJSON `json:"sync_aggregate"` + SignatureSlot string `json:"signature_slot"` +} + +func (u Update) MarshalJSON() ([]byte, error) { + nextSyncCommitteeBranch := make([]string, len(u.NextSyncCommitteeBranch)) + for i, root := range u.NextSyncCommitteeBranch { + nextSyncCommitteeBranch[i] = root.String() + } + + finalityBranch := make([]string, len(u.FinalityBranch)) + for i, root := range u.FinalityBranch { + finalityBranch[i] = root.String() + } + + return json.Marshal(&updateJSON{ + AttestedHeader: u.AttestedHeader.ToJSON(), + NextSyncCommittee: u.NextSyncCommittee.ToJSON(), + NextSyncCommitteeBranch: nextSyncCommitteeBranch, + FinalizedHeader: u.FinalizedHeader.ToJSON(), + FinalityBranch: finalityBranch, + SyncAggregate: u.SyncAggregate.ToJSON(), + SignatureSlot: fmt.Sprintf("%d", u.SignatureSlot), + }) +} + +func (u *Update) UnmarshalJSON(input []byte) error { + var jsonData updateJSON + if err := json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + if err := u.AttestedHeader.FromJSON(jsonData.AttestedHeader); err != nil { + return errors.Wrap(err, "invalid attested header") + } + + if err := u.NextSyncCommittee.FromJSON(jsonData.NextSyncCommittee); err != nil { + return errors.Wrap(err, "invalid next sync committee") + } + + u.NextSyncCommitteeBranch = make([]phase0.Root, len(jsonData.NextSyncCommitteeBranch)) + for i, root := range jsonData.NextSyncCommitteeBranch { + r, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid next sync committee branch root: %s", root)) + } + u.NextSyncCommitteeBranch[i] = phase0.Root(r) + } + + if err := u.FinalizedHeader.FromJSON(jsonData.FinalizedHeader); err != nil { + return errors.Wrap(err, "invalid finalized header") + } + + u.FinalityBranch = make([]phase0.Root, len(jsonData.FinalityBranch)) + for i, root := range jsonData.FinalityBranch { + r, err := hex.DecodeString(strings.TrimPrefix(root, "0x")) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid finality branch root: %s", root)) + } + u.FinalityBranch[i] = phase0.Root(r) + } + + if err := u.SyncAggregate.FromJSON(jsonData.SyncAggregate); err != nil { + return errors.Wrap(err, "invalid sync aggregate") + } + + slot, err := strconv.ParseUint(jsonData.SignatureSlot, 10, 64) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("invalid signature slot: %s", jsonData.SignatureSlot)) + } + u.SignatureSlot = phase0.Slot(slot) + + return nil +} diff --git a/pkg/beacon/api/types/lightclient/update_test.go b/pkg/beacon/api/types/lightclient/update_test.go new file mode 100644 index 0000000..9635c59 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/update_test.go @@ -0,0 +1,184 @@ +package lightclient_test + +import ( + "testing" + + "encoding/json" + "reflect" + + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + "github.com/prysmaticlabs/go-bitfield" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpdateMarshalUnmarshal(t *testing.T) { + testCases := []struct { + name string + update lightclient.Update + }{ + { + name: "Basic Update", + update: lightclient.Update{ + AttestedHeader: lightclient.LightClientHeader{ + Beacon: lightclient.BeaconBlockHeader{ + Slot: 1234, + ProposerIndex: 5678, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, + }, + NextSyncCommittee: lightclient.SyncCommittee{ + Pubkeys: []phase0.BLSPubKey{{0x04}}, + AggregatePubkey: phase0.BLSPubKey{0x05}, + }, + NextSyncCommitteeBranch: []phase0.Root{{0x06}}, + FinalizedHeader: lightclient.LightClientHeader{ + Beacon: lightclient.BeaconBlockHeader{ + Slot: 5678, + ProposerIndex: 1234, + ParentRoot: phase0.Root{0x07}, + StateRoot: phase0.Root{0x08}, + BodyRoot: phase0.Root{0x09}, + }, + }, + FinalityBranch: []phase0.Root{{0x0a}}, + SyncAggregate: lightclient.SyncAggregate{ + SyncCommitteeBits: bitfield.Bitvector512{0, 1}, + SyncCommitteeSignature: [96]byte{0x0c}, + }, + SignatureSlot: 9876, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Marshal + marshaled, err := json.Marshal(tc.update) + if err != nil { + t.Fatalf("Failed to marshal Update: %v", err) + } + + // Unmarshal + var unmarshaled lightclient.Update + err = json.Unmarshal(marshaled, &unmarshaled) + if err != nil { + t.Fatalf("Failed to unmarshal Update: %v", err) + } + + // Compare + if !reflect.DeepEqual(tc.update, unmarshaled) { + t.Errorf("Unmarshaled Update does not match original. Got %+v, want %+v", unmarshaled, tc.update) + } + }) + } +} + +func TestUpdateUnmarshalJSON(t *testing.T) { + expectedRoot := "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + expectedSignature := "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + expectedPubkey := "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + + jsonStr := `{ + "attested_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "next_sync_committee": { + "pubkeys": [ + "` + expectedPubkey + `", + "` + expectedPubkey + `" + ], + "aggregate_pubkey": "` + expectedPubkey + `" + }, + "next_sync_committee_branch": [ + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `" + ], + "finalized_header": { + "beacon": { + "slot": "1", + "proposer_index": "1", + "parent_root": "` + expectedRoot + `", + "state_root": "` + expectedRoot + `", + "body_root": "` + expectedRoot + `" + } + }, + "finality_branch": [ + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `", + "` + expectedRoot + `" + ], + "sync_aggregate": { + "sync_committee_bits": "0x01", + "sync_committee_signature": "` + expectedSignature + `" + }, + "signature_slot": "1" + }` + + var update lightclient.Update + err := json.Unmarshal([]byte(jsonStr), &update) + require.NoError(t, err) + + // Check all fields manually + assert.Equal(t, phase0.Slot(1), update.AttestedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.AttestedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.AttestedHeader.Beacon.BodyRoot.String()) + + assert.Len(t, update.NextSyncCommittee.Pubkeys, 2) + for _, pubkey := range update.NextSyncCommittee.Pubkeys { + assert.Equal(t, expectedPubkey, pubkey.String()) + } + assert.Equal(t, expectedPubkey, update.NextSyncCommittee.AggregatePubkey.String()) + + assert.Len(t, update.NextSyncCommitteeBranch, 5) + for _, root := range update.NextSyncCommitteeBranch { + assert.Equal(t, expectedRoot, root.String()) + } + + assert.Equal(t, phase0.Slot(1), update.FinalizedHeader.Beacon.Slot) + assert.Equal(t, phase0.ValidatorIndex(1), update.FinalizedHeader.Beacon.ProposerIndex) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.ParentRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.StateRoot.String()) + assert.Equal(t, expectedRoot, update.FinalizedHeader.Beacon.BodyRoot.String()) + + assert.Len(t, update.FinalityBranch, 6) + for _, root := range update.FinalityBranch { + assert.Equal(t, expectedRoot, root.String()) + } + + assert.Equal(t, bitfield.Bitvector512{1}, update.SyncAggregate.SyncCommitteeBits) + assert.Equal(t, expectedSignature, update.SyncAggregate.SyncCommitteeSignature.String()) + + assert.Equal(t, phase0.Slot(1), update.SignatureSlot) + + // Marshal back to JSON + marshaledJSON, err := json.Marshal(update) + require.NoError(t, err) + + // Unmarshal both JSONs to interfaces for comparison + var originalData, remarshaledData interface{} + err = json.Unmarshal([]byte(jsonStr), &originalData) + require.NoError(t, err) + err = json.Unmarshal(marshaledJSON, &remarshaledData) + require.NoError(t, err) + + // Compare the unmarshaled data + assert.Equal(t, originalData, remarshaledData, "Remarshaled JSON does not match the original") +} diff --git a/pkg/beacon/fetch.go b/pkg/beacon/fetch.go index 5e9ea54..0963162 100644 --- a/pkg/beacon/fetch.go +++ b/pkg/beacon/fetch.go @@ -15,13 +15,20 @@ import ( ) func (n *node) FetchSyncStatus(ctx context.Context) (*v1.SyncState, error) { + logCtx := n.log.WithField("method", "FetchSyncStatus") provider, isProvider := n.client.(eth2client.NodeSyncingProvider) if !isProvider { + logCtx.Error("client does not implement eth2client.NodeSyncingProvider") + return nil, errors.New("client does not implement eth2client.NodeSyncingProvider") } + logCtx.Debug("Fetching sync status") + status, err := provider.NodeSyncing(ctx, &api.NodeSyncingOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch sync status") + return nil, err } @@ -29,15 +36,25 @@ func (n *node) FetchSyncStatus(ctx context.Context) (*v1.SyncState, error) { n.publishSyncStatus(ctx, status.Data) + logCtx.WithField("status", status.Data).Debug("Successfully fetched sync status") + return status.Data, nil } func (n *node) FetchPeers(ctx context.Context) (*types.Peers, error) { + logCtx := n.log.WithField("method", "FetchPeers") + + logCtx.Debug("Fetching peers") + peers, err := n.api.NodePeers(ctx) if err != nil { + logCtx.WithError(err).Error("failed to fetch peers") + return nil, err } + logCtx.WithField("peers", len(peers)).Debug("Successfully fetched peers") + n.peers = peers n.publishPeersUpdated(ctx, peers) @@ -46,13 +63,22 @@ func (n *node) FetchPeers(ctx context.Context) (*types.Peers, error) { } func (n *node) FetchNodeVersion(ctx context.Context) (string, error) { + logCtx := n.log.WithField("method", "FetchNodeVersion") + + logCtx.Debug("Fetching node version") + provider, isProvider := n.client.(eth2client.NodeVersionProvider) if !isProvider { - return "", errors.New("client does not implement eth2client.NodeVersionProvider") + err := errors.New("client does not implement eth2client.NodeVersionProvider") + logCtx.WithError(err).Error("client does not implement eth2client.NodeVersionProvider") + + return "", err } rsp, err := provider.NodeVersion(ctx, &api.NodeVersionOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch node version") + return "", err } @@ -60,51 +86,127 @@ func (n *node) FetchNodeVersion(ctx context.Context) (string, error) { n.publishNodeVersionUpdated(ctx, rsp.Data) + logCtx.WithField("version", rsp.Data).Debug("Successfully fetched node version") + return rsp.Data, nil } func (n *node) FetchBlock(ctx context.Context, stateID string) (*spec.VersionedSignedBeaconBlock, error) { - return n.getBlock(ctx, stateID) + logCtx := n.log.WithField("method", "FetchBlock").WithField("state_id", stateID) + + logCtx.Debug("Fetching block") + + block, err := n.getBlock(ctx, stateID) + if err != nil { + logCtx.WithError(err).Error("failed to fetch block") + + return nil, err + } + + logCtx.Debug("Successfully fetched block") + + return block, nil } func (n *node) FetchRawBlock(ctx context.Context, stateID string, contentType string) ([]byte, error) { - return n.api.RawBlock(ctx, stateID, contentType) + logCtx := n.log.WithField("method", "FetchRawBlock").WithField("state_id", stateID) + + logCtx.Debug("Fetching raw block") + + block, err := n.api.RawBlock(ctx, stateID, contentType) + if err != nil { + logCtx.WithError(err).Error("failed to fetch raw block") + + return nil, err + } + + logCtx.Debug("Successfully fetched raw block") + + return block, nil } func (n *node) FetchBlockRoot(ctx context.Context, stateID string) (*phase0.Root, error) { - return n.getBlockRoot(ctx, stateID) + logCtx := n.log.WithField("method", "FetchBlockRoot").WithField("state_id", stateID) + + logCtx.Debug("Fetching block root") + + root, err := n.getBlockRoot(ctx, stateID) + if err != nil { + logCtx.WithError(err).Error("failed to fetch block root") + + return nil, err + } + + logCtx.Debug("Successfully fetched block root") + + return root, nil } func (n *node) FetchBeaconState(ctx context.Context, stateID string) (*spec.VersionedBeaconState, error) { + logCtx := n.log.WithField("method", "FetchBeaconState").WithField("state_id", stateID) + provider, isProvider := n.client.(eth2client.BeaconStateProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.NodeVersionProvider") + err := errors.New("client does not implement eth2client.NodeVersionProvider") + + logCtx.Error(err.Error()) + + return nil, err } + logCtx.Debug("Fetching beacon state") + rsp, err := provider.BeaconState(ctx, &api.BeaconStateOpts{ State: stateID, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon state") + return nil, err } + logCtx.Debug("Successfully fetched beacon state") + return rsp.Data, nil } func (n *node) FetchRawBeaconState(ctx context.Context, stateID string, contentType string) ([]byte, error) { - return n.api.RawDebugBeaconState(ctx, stateID, contentType) + logCtx := n.log.WithField("method", "FetchRawBeaconState").WithField("state_id", stateID) + + logCtx.Debug("Fetching raw beacon state") + + block, err := n.api.RawDebugBeaconState(ctx, stateID, contentType) + if err != nil { + logCtx.WithError(err).Error("failed to fetch raw beacon state") + + return nil, err + } + + logCtx.Debug("Successfully fetched raw beacon state") + + return block, nil } func (n *node) FetchFinality(ctx context.Context, stateID string) (*v1.Finality, error) { + logCtx := n.log.WithField("method", "FetchFinality").WithField("state_id", stateID) + provider, isProvider := n.client.(eth2client.FinalityProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.FinalityProvider") + err := errors.New("client does not implement eth2client.FinalityProvider") + + logCtx.Error(err.Error()) + + return nil, err } + logCtx.Debug("Fetching finality") + rsp, err := provider.Finality(ctx, &api.FinalityOpts{ State: stateID, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch finality") + return nil, err } @@ -129,31 +231,55 @@ func (n *node) FetchFinality(ctx context.Context, stateID string) (*v1.Finality, } } + logCtx.Debug("Successfully fetched finality") + return finality, nil } func (n *node) FetchRawSpec(ctx context.Context) (map[string]any, error) { + logCtx := n.log.WithField("method", "FetchRawSpec") + + logCtx.Debug("Fetching raw spec") + provider, isProvider := n.client.(eth2client.SpecProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.SpecProvider") + err := errors.New("client does not implement eth2client.SpecProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.Spec(ctx, &api.SpecOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch raw spec") + return nil, err } + logCtx.Debug("Successfully fetched raw spec") + return rsp.Data, nil } func (n *node) FetchSpec(ctx context.Context) (*state.Spec, error) { + logCtx := n.log.WithField("method", "FetchSpec") + + logCtx.Debug("Fetching spec") + provider, isProvider := n.client.(eth2client.SpecProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.SpecProvider") + err := errors.New("client does not implement eth2client.SpecProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.Spec(ctx, &api.SpecOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch spec") + return nil, err } @@ -163,85 +289,167 @@ func (n *node) FetchSpec(ctx context.Context) (*state.Spec, error) { n.publishSpecUpdated(ctx, &sp) + logCtx.Debug("Successfully fetched spec") + return &sp, nil } func (n *node) FetchBeaconBlockBlobs(ctx context.Context, blockID string) ([]*deneb.BlobSidecar, error) { + logCtx := n.log.WithField("method", "FetchBeaconBlockBlobs").WithField("block_id", blockID) + + logCtx.Debug("Fetching beacon blobs") + provider, isProvider := n.client.(eth2client.BlobSidecarsProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.BlobSidecarsProvider") + err := errors.New("client does not implement eth2client.BlobSidecarsProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.BlobSidecars(ctx, &api.BlobSidecarsOpts{ Block: blockID, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon blobs") + return nil, err } + logCtx.WithField("blob_count", len(rsp.Data)).Debug("Successfully fetched beacon blobs") + return rsp.Data, nil } func (n *node) FetchProposerDuties(ctx context.Context, epoch phase0.Epoch) ([]*v1.ProposerDuty, error) { - n.log.WithField("epoch", epoch).Debug("Fetching proposer duties") + logCtx := n.log.WithField("method", "FetchProposerDuties").WithField("epoch", epoch) + + logCtx.Debug("Fetching proposer duties") provider, isProvider := n.client.(eth2client.ProposerDutiesProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.ProposerDutiesProvider") + err := errors.New("client does not implement eth2client.ProposerDutiesProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.ProposerDuties(ctx, &api.ProposerDutiesOpts{ Epoch: epoch, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch proposer duties") + return nil, err } + logCtx.Debug("Successfully fetched proposer duties") + return rsp.Data, nil } func (n *node) FetchForkChoice(ctx context.Context) (*v1.ForkChoice, error) { + logCtx := n.log.WithField("method", "FetchForkChoice") + + logCtx.Debug("Fetching fork choice") + provider, isProvider := n.client.(eth2client.ForkChoiceProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.ForkChoiceProvider") + err := errors.New("client does not implement eth2client.ForkChoiceProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.ForkChoice(ctx, &api.ForkChoiceOpts{}) if err != nil { + logCtx.WithError(err).Error("failed to fetch fork choice") + return nil, err } + logCtx.Debug("Successfully fetched fork choice") + return rsp.Data, nil } func (n *node) FetchDepositSnapshot(ctx context.Context) (*types.DepositSnapshot, error) { - return n.api.DepositSnapshot(ctx) + logCtx := n.log.WithField("method", "FetchDepositSnapshot") + + logCtx.Debug("Fetching deposit snapshot") + + snapshot, err := n.api.DepositSnapshot(ctx) + if err != nil { + logCtx.WithError(err).Error("failed to fetch deposit snapshot") + + return nil, err + } + + logCtx.Debug("Successfully fetched deposit snapshot") + + return snapshot, nil } func (n *node) FetchNodeIdentity(ctx context.Context) (*types.Identity, error) { - return n.api.NodeIdentity(ctx) + logCtx := n.log.WithField("method", "FetchNodeIdentity") + + logCtx.Debug("Fetching node identity") + + identity, err := n.api.NodeIdentity(ctx) + if err != nil { + logCtx.WithError(err).Error("failed to fetch node identity") + + return nil, err + } + + logCtx.WithField("identity", identity).Debug("Successfully fetched node identity") + + return identity, nil } func (n *node) FetchBeaconStateRoot(ctx context.Context, state string) (phase0.Root, error) { + logCtx := n.log.WithField("method", "FetchBeaconStateRoot").WithField("state", state) + + logCtx.Debug("Fetching beacon state root") + provider, isProvider := n.client.(eth2client.BeaconStateRootProvider) if !isProvider { - return phase0.Root{}, errors.New("client does not implement eth2client.StateRootProvider") + err := errors.New("client does not implement eth2client.StateRootProvider") + + logCtx.Error(err.Error()) + + return phase0.Root{}, err } rsp, err := provider.BeaconStateRoot(ctx, &api.BeaconStateRootOpts{ State: state, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon state root") + return phase0.Root{}, err } + logCtx.Debug("Successfully fetched beacon state root") + return *rsp.Data, nil } func (n *node) FetchBeaconCommittees(ctx context.Context, state string, epoch *phase0.Epoch) ([]*v1.BeaconCommittee, error) { + logCtx := n.log.WithField("method", "FetchBeaconCommittees").WithField("state", state) + + logCtx.Debug("Fetching beacon committees") + provider, isProvider := n.client.(eth2client.BeaconCommitteesProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.BeaconCommitteesProvider") + err := errors.New("client does not implement eth2client.BeaconCommitteesProvider") + + logCtx.Error(err.Error()) + + return nil, err } opts := &api.BeaconCommitteesOpts{ @@ -254,16 +462,28 @@ func (n *node) FetchBeaconCommittees(ctx context.Context, state string, epoch *p rsp, err := provider.BeaconCommittees(ctx, opts) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon committees") + return nil, err } + logCtx.WithField("committee_count", len(rsp.Data)).Debug("Successfully fetched beacon committees") + return rsp.Data, nil } func (n *node) FetchAttestationData(ctx context.Context, slot phase0.Slot, committeeIndex phase0.CommitteeIndex) (*phase0.AttestationData, error) { + logCtx := n.log.WithField("method", "FetchAttestationData").WithField("slot", slot).WithField("committee_index", committeeIndex) + + logCtx.Debug("Fetching attestation data") + provider, isProvider := n.client.(eth2client.AttestationDataProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.AttestationDataProvider") + err := errors.New("client does not implement eth2client.AttestationDataProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.AttestationData(ctx, &api.AttestationDataOpts{ @@ -271,22 +491,38 @@ func (n *node) FetchAttestationData(ctx context.Context, slot phase0.Slot, commi CommitteeIndex: committeeIndex, }) if err != nil { + logCtx.WithError(err).Error("failed to fetch attestation data") + return nil, err } + logCtx.Debug("Successfully fetched attestation data") + return rsp.Data, nil } func (n *node) FetchBeaconBlockHeader(ctx context.Context, opts *api.BeaconBlockHeaderOpts) (*v1.BeaconBlockHeader, error) { + logCtx := n.log.WithField("method", "FetchBeaconBlockHeader") + + logCtx.Debug("Fetching beacon block header") + provider, isProvider := n.client.(eth2client.BeaconBlockHeadersProvider) if !isProvider { - return nil, errors.New("client does not implement eth2client.BeaconBlockHeadersProvider") + err := errors.New("client does not implement eth2client.BeaconBlockHeadersProvider") + + logCtx.Error(err.Error()) + + return nil, err } rsp, err := provider.BeaconBlockHeader(ctx, opts) if err != nil { + logCtx.WithError(err).Error("failed to fetch beacon block header") + return nil, err } + logCtx.Debug("Successfully fetched beacon block header") + return rsp.Data, nil } From d1834c10e23672791bbf2c095d15a8d1ac9ab686 Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Wed, 23 Oct 2024 15:33:23 +1000 Subject: [PATCH 2/3] feat: Update LightClient methods to use structured responses --- pkg/beacon/api/api.go | 87 ++++++++++++------- pkg/beacon/api/responses.go | 24 +++++ pkg/beacon/api/types/agents.go | 8 ++ .../api/types/lightclient/finality_update.go | 2 +- pkg/beacon/api/types/lightclient/update.go | 3 + pkg/beacon/beacon.go | 8 ++ pkg/beacon/fetch.go | 72 +++++++++++++++ 7 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 pkg/beacon/api/responses.go diff --git a/pkg/beacon/api/api.go b/pkg/beacon/api/api.go index 8a7d7d0..21c856b 100644 --- a/pkg/beacon/api/api.go +++ b/pkg/beacon/api/api.go @@ -24,10 +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) + LightClientBootstrap(ctx context.Context, blockRoot string) (*LightClientBootstrapResponse, error) + LightClientUpdates(ctx context.Context, startPeriod, count int) (*LightClientUpdatesResponse, error) + LightClientFinalityUpdate(ctx context.Context) (*LightClientFinalityUpdateResponse, error) + LightClientOptimisticUpdate(ctx context.Context) (*LightClientOptimisticUpdateResponse, error) } type consensusClient struct { @@ -47,12 +47,13 @@ func NewConsensusClient(ctx context.Context, log logrus.FieldLogger, url string, } } -type apiResponse struct { - Data json.RawMessage `json:"data"` +type BeaconAPIResponse struct { + Data json.RawMessage `json:"data"` + Version string `json:"version"` } //nolint:unused // this is used in the future -func (c *consensusClient) post(ctx context.Context, path string, body map[string]interface{}) (json.RawMessage, error) { +func (c *consensusClient) post(ctx context.Context, path string, body map[string]interface{}) (*BeaconAPIResponse, error) { jsonData, err := json.Marshal(body) if err != nil { return nil, err @@ -84,16 +85,16 @@ func (c *consensusClient) post(ctx context.Context, path string, body map[string return nil, err } - resp := new(apiResponse) + resp := new(BeaconAPIResponse) if err := json.Unmarshal(data, resp); err != nil { return nil, err } - return resp.Data, nil + return resp, nil } //nolint:unparam // ctx will probably be used in the future -func (c *consensusClient) get(ctx context.Context, path string) (json.RawMessage, error) { +func (c *consensusClient) get(ctx context.Context, path string) (*BeaconAPIResponse, error) { req, err := http.NewRequestWithContext(ctx, "GET", c.url+path, nil) if err != nil { return nil, err @@ -120,12 +121,12 @@ func (c *consensusClient) get(ctx context.Context, path string) (json.RawMessage return nil, err } - resp := new(apiResponse) + resp := new(BeaconAPIResponse) if err := json.Unmarshal(data, resp); err != nil { return nil, err } - return resp.Data, nil + return resp, nil } func (c *consensusClient) getRaw(ctx context.Context, path string, contentType string) ([]byte, error) { @@ -171,7 +172,7 @@ func (c *consensusClient) NodePeers(ctx context.Context) (types.Peers, error) { } rsp := types.Peers{} - if err := json.Unmarshal(data, &rsp); err != nil { + if err := json.Unmarshal(data.Data, &rsp); err != nil { return nil, err } @@ -186,7 +187,7 @@ func (c *consensusClient) NodePeer(ctx context.Context, peerID string) (types.Pe } rsp := types.Peer{} - if err := json.Unmarshal(data, &rsp); err != nil { + if err := json.Unmarshal(data.Data, &rsp); err != nil { return types.Peer{}, err } @@ -201,7 +202,7 @@ func (c *consensusClient) NodePeerCount(ctx context.Context) (types.PeerCount, e } rsp := types.PeerCount{} - if err := json.Unmarshal(data, &rsp); err != nil { + if err := json.Unmarshal(data.Data, &rsp); err != nil { return types.PeerCount{}, err } @@ -236,7 +237,7 @@ func (c *consensusClient) DepositSnapshot(ctx context.Context) (*types.DepositSn } rsp := types.DepositSnapshot{} - if err := json.Unmarshal(data, &rsp); err != nil { + if err := json.Unmarshal(data.Data, &rsp); err != nil { return nil, err } @@ -250,28 +251,35 @@ func (c *consensusClient) NodeIdentity(ctx context.Context) (*types.Identity, er } rsp := types.Identity{} - if err := json.Unmarshal(data, &rsp); err != nil { + if err := json.Unmarshal(data.Data, &rsp); err != nil { return nil, err } return &rsp, nil } -func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot string) (*lightclient.Bootstrap, error) { +func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot string) (*LightClientBootstrapResponse, 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 { + rsp := LightClientBootstrapResponse{ + Response: Response[*lightclient.Bootstrap]{ + Data: &lightclient.Bootstrap{}, + Metadata: map[string]any{ + "version": data.Version, + }, + }, + } + if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { return nil, err } return &rsp, nil } -func (c *consensusClient) LightClientUpdate(ctx context.Context, startPeriod, count int) (*lightclient.Update, error) { +func (c *consensusClient) LightClientUpdates(ctx context.Context, startPeriod, count int) (*LightClientUpdatesResponse, error) { if count == 0 { return nil, errors.New("count must be greater than 0") } @@ -285,36 +293,57 @@ func (c *consensusClient) LightClientUpdate(ctx context.Context, startPeriod, co return nil, err } - rsp := lightclient.Update{} - if err := json.Unmarshal(data, &rsp); err != nil { + rsp := LightClientUpdatesResponse{ + Response: Response[*lightclient.Updates]{ + Data: &lightclient.Updates{}, + Metadata: map[string]any{ + "version": data.Version, + }, + }, + } + if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { return nil, err } return &rsp, nil } -func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*lightclient.FinalityUpdate, error) { +func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*LightClientFinalityUpdateResponse, 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 { + rsp := LightClientFinalityUpdateResponse{ + Response: Response[*lightclient.FinalityUpdate]{ + Data: &lightclient.FinalityUpdate{}, + Metadata: map[string]any{ + "version": data.Version, + }, + }, + } + if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { return nil, err } return &rsp, nil } -func (c *consensusClient) LightClientOptimisticUpdate(ctx context.Context) (*lightclient.OptimisticUpdate, error) { +func (c *consensusClient) LightClientOptimisticUpdate(ctx context.Context) (*LightClientOptimisticUpdateResponse, 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 { + rsp := LightClientOptimisticUpdateResponse{ + Response: Response[*lightclient.OptimisticUpdate]{ + Data: &lightclient.OptimisticUpdate{}, + Metadata: map[string]any{ + "version": data.Version, + }, + }, + } + if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { return nil, err } diff --git a/pkg/beacon/api/responses.go b/pkg/beacon/api/responses.go new file mode 100644 index 0000000..4b5ed08 --- /dev/null +++ b/pkg/beacon/api/responses.go @@ -0,0 +1,24 @@ +package api + +import "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" + +type Response[T any] struct { + Data T `json:"data"` + Metadata map[string]any `json:"metadata"` +} + +type LightClientUpdatesResponse struct { + Response[*lightclient.Updates] +} + +type LightClientBootstrapResponse struct { + Response[*lightclient.Bootstrap] +} + +type LightClientFinalityUpdateResponse struct { + Response[*lightclient.FinalityUpdate] +} + +type LightClientOptimisticUpdateResponse struct { + Response[*lightclient.OptimisticUpdate] +} diff --git a/pkg/beacon/api/types/agents.go b/pkg/beacon/api/types/agents.go index ff081c6..003bdb9 100644 --- a/pkg/beacon/api/types/agents.go +++ b/pkg/beacon/api/types/agents.go @@ -20,6 +20,8 @@ const ( AgentPrysm Agent = "prysm" // AgentLodestar is a Lodestar agent. AgentLodestar Agent = "lodestar" + // AgentGrandine is a Grandine agent. + AgentGrandine Agent = "grandine" ) // AllAgents is a list of all agents. @@ -30,6 +32,7 @@ var AllAgents = []Agent{ AgentTeku, AgentPrysm, AgentLodestar, + AgentGrandine, } // AgentCount represents the number of peers with each agent. @@ -40,6 +43,7 @@ type AgentCount struct { Teku int `json:"teku"` Prysm int `json:"prysm"` Lodestar int `json:"lodestar"` + Grandine int `json:"grandine"` } // AgentFromString returns the agent from the given string. @@ -66,5 +70,9 @@ func AgentFromString(agent string) Agent { return AgentLodestar } + if strings.Contains(asLower, "grandine") { + return AgentGrandine + } + return AgentUnknown } diff --git a/pkg/beacon/api/types/lightclient/finality_update.go b/pkg/beacon/api/types/lightclient/finality_update.go index 5e4d08d..4bef398 100644 --- a/pkg/beacon/api/types/lightclient/finality_update.go +++ b/pkg/beacon/api/types/lightclient/finality_update.go @@ -81,7 +81,7 @@ func (f FinalityUpdate) MarshalJSON() ([]byte, error) { func (f *FinalityUpdate) ToJSON() finalityUpdateJSON { finalityBranch := make([]string, len(f.FinalityBranch)) for i, root := range f.FinalityBranch { - finalityBranch[i] = fmt.Sprintf("%x", root) + finalityBranch[i] = fmt.Sprintf("%#x", root) } return finalityUpdateJSON{ diff --git a/pkg/beacon/api/types/lightclient/update.go b/pkg/beacon/api/types/lightclient/update.go index 35b7753..25cd571 100644 --- a/pkg/beacon/api/types/lightclient/update.go +++ b/pkg/beacon/api/types/lightclient/update.go @@ -11,6 +11,9 @@ import ( "github.com/pkg/errors" ) +// Updates represents a light client updates. +type Updates []*Update + // Update represents a light client update. type Update struct { AttestedHeader LightClientHeader `json:"attested_header"` diff --git a/pkg/beacon/beacon.go b/pkg/beacon/beacon.go index 1ccab71..d6e80c4 100644 --- a/pkg/beacon/beacon.go +++ b/pkg/beacon/beacon.go @@ -102,6 +102,14 @@ type Node interface { FetchBeaconBlockHeader(ctx context.Context, opts *eapi.BeaconBlockHeaderOpts) (*v1.BeaconBlockHeader, error) // FetchNodeIdentity fetches the node identity. FetchNodeIdentity(ctx context.Context) (*types.Identity, error) + // FetchLightClientBootstrap fetches the light client bootstrap. + FetchLightClientBootstrap(ctx context.Context, root phase0.Root) (*api.LightClientBootstrapResponse, error) + // FetchLightClientFinalityUpdate fetches the light client finality update. + FetchLightClientFinalityUpdate(ctx context.Context) (*api.LightClientFinalityUpdateResponse, error) + // FetchLightClientOptimisticUpdate fetches the light client optimistic update. + FetchLightClientOptimisticUpdate(ctx context.Context) (*api.LightClientOptimisticUpdateResponse, error) + // FetchLightClientUpdates fetches the light client updates. + FetchLightClientUpdates(ctx context.Context, startPeriod, count int) (*api.LightClientUpdatesResponse, error) // Subscriptions // - Proxied Beacon events diff --git a/pkg/beacon/fetch.go b/pkg/beacon/fetch.go index 0963162..4ca8747 100644 --- a/pkg/beacon/fetch.go +++ b/pkg/beacon/fetch.go @@ -3,6 +3,7 @@ package beacon import ( "context" "errors" + "fmt" eth2client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" @@ -10,6 +11,7 @@ import ( "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/deneb" "github.com/attestantio/go-eth2-client/spec/phase0" + bapi "github.com/ethpandaops/beacon/pkg/beacon/api" "github.com/ethpandaops/beacon/pkg/beacon/api/types" "github.com/ethpandaops/beacon/pkg/beacon/state" ) @@ -526,3 +528,73 @@ func (n *node) FetchBeaconBlockHeader(ctx context.Context, opts *api.BeaconBlock return rsp.Data, nil } + +func (n *node) FetchLightClientBootstrap(ctx context.Context, root phase0.Root) (*bapi.LightClientBootstrapResponse, error) { + rootAsHex := fmt.Sprintf("0x%x", root) + + logCtx := n.log.WithField("method", "FetchLightClientBootstrap").WithField("root", rootAsHex) + + logCtx.Debug("Fetching light client bootstrap") + + rsp, err := n.api.LightClientBootstrap(ctx, rootAsHex) + if err != nil { + logCtx.WithError(err).Error("failed to fetch light client bootstrap") + + return nil, err + } + + logCtx.Debug("Successfully fetched light client bootstrap") + + return rsp, nil +} + +func (n *node) FetchLightClientFinalityUpdate(ctx context.Context) (*bapi.LightClientFinalityUpdateResponse, error) { + logCtx := n.log.WithField("method", "FetchLightClientFinalityUpdate") + + logCtx.Debug("Fetching light client finality update") + + rsp, err := n.api.LightClientFinalityUpdate(ctx) + if err != nil { + logCtx.WithError(err).Error("failed to fetch light client finality update") + + return nil, err + } + + logCtx.Debug("Successfully fetched light client finality update") + + return rsp, nil +} + +func (n *node) FetchLightClientOptimisticUpdate(ctx context.Context) (*bapi.LightClientOptimisticUpdateResponse, error) { + logCtx := n.log.WithField("method", "FetchLightClientOptimisticUpdate") + + logCtx.Debug("Fetching light client optimistic update") + + rsp, err := n.api.LightClientOptimisticUpdate(ctx) + if err != nil { + logCtx.WithError(err).Error("failed to fetch light client optimistic update") + + return nil, err + } + + logCtx.Debug("Successfully fetched light client optimistic update") + + return rsp, nil +} + +func (n *node) FetchLightClientUpdates(ctx context.Context, startPeriod, count int) (*bapi.LightClientUpdatesResponse, error) { + logCtx := n.log.WithField("method", "FetchLightClientUpdates").WithField("start_period", startPeriod).WithField("count", count) + + logCtx.Debug("Fetching light client update") + + rsp, err := n.api.LightClientUpdates(ctx, startPeriod, count) + if err != nil { + logCtx.WithError(err).Error("failed to fetch light client update") + + return nil, err + } + + logCtx.Debug("Successfully fetched light client update") + + return rsp, nil +} From 8e0b4cd9bc1f02e66dde7fefbabb768f52793b27 Mon Sep 17 00:00:00 2001 From: Sam Calder-Mason Date: Thu, 24 Oct 2024 16:00:31 +1000 Subject: [PATCH 3/3] feat: Add content type constants and update API requests --- pkg/beacon/api/api.go | 92 +++++++++----- pkg/beacon/api/content_type.go | 6 + pkg/beacon/api/types/lightclient/bootstrap.go | 118 +----------------- .../api/types/lightclient/bootstrap_header.go | 42 +++++++ .../api/types/lightclient/bootstrap_test.go | 46 ++++--- 5 files changed, 135 insertions(+), 169 deletions(-) create mode 100644 pkg/beacon/api/content_type.go create mode 100644 pkg/beacon/api/types/lightclient/bootstrap_header.go diff --git a/pkg/beacon/api/api.go b/pkg/beacon/api/api.go index 21c856b..61f97f9 100644 --- a/pkg/beacon/api/api.go +++ b/pkg/beacon/api/api.go @@ -9,6 +9,7 @@ import ( "io" "net/http" "net/url" + "strings" "github.com/ethpandaops/beacon/pkg/beacon/api/types" "github.com/ethpandaops/beacon/pkg/beacon/api/types/lightclient" @@ -52,6 +53,8 @@ type BeaconAPIResponse struct { Version string `json:"version"` } +type BeaconAPIResponses[T any] []BeaconAPIResponse + //nolint:unused // this is used in the future func (c *consensusClient) post(ctx context.Context, path string, body map[string]interface{}) (*BeaconAPIResponse, error) { jsonData, err := json.Marshal(body) @@ -94,12 +97,18 @@ func (c *consensusClient) post(ctx context.Context, path string, body map[string } //nolint:unparam // ctx will probably be used in the future -func (c *consensusClient) get(ctx context.Context, path string) (*BeaconAPIResponse, error) { +func (c *consensusClient) get(ctx context.Context, path string, contentType string, rspType any) error { + if contentType == "" { + contentType = "application/json" + } + req, err := http.NewRequestWithContext(ctx, "GET", c.url+path, nil) if err != nil { - return nil, err + return err } + req.Header.Set("Accept", contentType) + // Set headers from c.headers for k, v := range c.headers { req.Header.Set(k, v) @@ -107,32 +116,40 @@ func (c *consensusClient) get(ctx context.Context, path string) (*BeaconAPIRespo rsp, err := c.client.Do(req) if err != nil { - return nil, err + return err } defer rsp.Body.Close() if rsp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("status code: %d", rsp.StatusCode) + return fmt.Errorf("status code: %d", rsp.StatusCode) + } + + // Parse the content type header to handle parameters like charset + contentTypeHeader := rsp.Header.Get("Content-Type") + if contentTypeHeader != "" { + if !strings.Contains(contentTypeHeader, contentType) { + return fmt.Errorf("unexpected content type: wanted (%s): got (%s)", contentType, contentTypeHeader) + } } data, err := io.ReadAll(rsp.Body) if err != nil { - return nil, err + return err } - resp := new(BeaconAPIResponse) - if err := json.Unmarshal(data, resp); err != nil { - return nil, err + if err := json.Unmarshal(data, rspType); err != nil { + return err } - return resp, nil + return nil } func (c *consensusClient) getRaw(ctx context.Context, path string, contentType string) ([]byte, error) { if contentType == "" { contentType = "application/json" } + u, err := url.Parse(c.url + path) if err != nil { return nil, err @@ -166,8 +183,8 @@ func (c *consensusClient) getRaw(ctx context.Context, path string, contentType s // NodePeers returns the list of peers connected to the node. func (c *consensusClient) NodePeers(ctx context.Context) (types.Peers, error) { - data, err := c.get(ctx, "/eth/v1/node/peers") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/node/peers", ContentTypeJSON, data); err != nil { return nil, err } @@ -181,8 +198,8 @@ func (c *consensusClient) NodePeers(ctx context.Context) (types.Peers, error) { // NodePeer returns the peer with the given peer ID. func (c *consensusClient) NodePeer(ctx context.Context, peerID string) (types.Peer, error) { - data, err := c.get(ctx, fmt.Sprintf("/eth/v1/node/peers/%s", peerID)) - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, fmt.Sprintf("/eth/v1/node/peers/%s", peerID), ContentTypeJSON, data); err != nil { return types.Peer{}, err } @@ -196,8 +213,8 @@ func (c *consensusClient) NodePeer(ctx context.Context, peerID string) (types.Pe // NodePeerCount returns the number of peers connected to the node. func (c *consensusClient) NodePeerCount(ctx context.Context) (types.PeerCount, error) { - data, err := c.get(ctx, "/eth/v1/node/peer_count") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/node/peer_count", ContentTypeJSON, data); err != nil { return types.PeerCount{}, err } @@ -231,8 +248,8 @@ func (c *consensusClient) RawBlock(ctx context.Context, stateID string, contentT // DepositSnapshot returns the deposit snapshot in the requested format. func (c *consensusClient) DepositSnapshot(ctx context.Context) (*types.DepositSnapshot, error) { - data, err := c.get(ctx, "/eth/v1/beacon/deposit_snapshot") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/beacon/deposit_snapshot", ContentTypeJSON, data); err != nil { return nil, err } @@ -245,8 +262,8 @@ func (c *consensusClient) DepositSnapshot(ctx context.Context) (*types.DepositSn } func (c *consensusClient) NodeIdentity(ctx context.Context) (*types.Identity, error) { - data, err := c.get(ctx, "/eth/v1/node/identity") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/node/identity", ContentTypeJSON, data); err != nil { return nil, err } @@ -259,8 +276,8 @@ func (c *consensusClient) NodeIdentity(ctx context.Context) (*types.Identity, er } func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot string) (*LightClientBootstrapResponse, error) { - data, err := c.get(ctx, fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/%s", blockRoot)) - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, fmt.Sprintf("/eth/v1/beacon/light_client/bootstrap/%s", blockRoot), ContentTypeJSON, data); err != nil { return nil, err } @@ -272,7 +289,7 @@ func (c *consensusClient) LightClientBootstrap(ctx context.Context, blockRoot st }, }, } - if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { + if err := json.Unmarshal(data.Data, &rsp.Response.Data); err != nil { return nil, err } @@ -288,29 +305,36 @@ func (c *consensusClient) LightClientUpdates(ctx context.Context, startPeriod, c 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 { + data := new(BeaconAPIResponses[*lightclient.Updates]) + if err := c.get(ctx, "/eth/v1/beacon/light_client/updates?"+params.Encode(), ContentTypeJSON, data); err != nil { return nil, err } rsp := LightClientUpdatesResponse{ Response: Response[*lightclient.Updates]{ - Data: &lightclient.Updates{}, - Metadata: map[string]any{ - "version": data.Version, - }, + Data: &lightclient.Updates{}, + Metadata: map[string]any{}, }, } - if err := json.Unmarshal(data.Data, &rsp.Data); err != nil { - return nil, err + + updates := make(lightclient.Updates, 0) + for _, resp := range *data { + update := lightclient.Update{} + if err := json.Unmarshal(resp.Data, &update); err != nil { + return nil, err + } + + updates = append(updates, &update) } + rsp.Response.Data = &updates + return &rsp, nil } func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*LightClientFinalityUpdateResponse, error) { - data, err := c.get(ctx, "/eth/v1/beacon/light_client/finality_update") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/beacon/light_client/finality_update", ContentTypeJSON, data); err != nil { return nil, err } @@ -330,8 +354,8 @@ func (c *consensusClient) LightClientFinalityUpdate(ctx context.Context) (*Light } func (c *consensusClient) LightClientOptimisticUpdate(ctx context.Context) (*LightClientOptimisticUpdateResponse, error) { - data, err := c.get(ctx, "/eth/v1/beacon/light_client/optimistic_update") - if err != nil { + data := new(BeaconAPIResponse) + if err := c.get(ctx, "/eth/v1/beacon/light_client/optimistic_update", ContentTypeJSON, data); err != nil { return nil, err } diff --git a/pkg/beacon/api/content_type.go b/pkg/beacon/api/content_type.go new file mode 100644 index 0000000..c025650 --- /dev/null +++ b/pkg/beacon/api/content_type.go @@ -0,0 +1,6 @@ +package api + +const ( + ContentTypeJSON = "application/json" + ContentTypeSSZ = "application/octet-stream" +) diff --git a/pkg/beacon/api/types/lightclient/bootstrap.go b/pkg/beacon/api/types/lightclient/bootstrap.go index acbd4b1..a65b7d4 100644 --- a/pkg/beacon/api/types/lightclient/bootstrap.go +++ b/pkg/beacon/api/types/lightclient/bootstrap.go @@ -4,7 +4,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "strconv" "strings" "github.com/attestantio/go-eth2-client/spec/phase0" @@ -25,24 +24,6 @@ type bootstrapJSON struct { CurrentSyncCommitteeBranch bootstrapCurrentSyncCommitteeBranchJSON `json:"current_sync_committee_branch"` } -// BootstrapHeader is the header of a light client bootstrap. -type BootstrapHeader struct { - Slot phase0.Slot - ProposerIndex phase0.ValidatorIndex - ParentRoot phase0.Root - StateRoot phase0.Root - BodyRoot phase0.Root -} - -// bootstrapHeaderJSON is the JSON representation of a bootstrap header. -type bootstrapHeaderJSON 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"` -} - // BootstrapCurrentSyncCommittee is the current sync committee of a light client bootstrap. type BootstrapCurrentSyncCommittee struct { Pubkeys []phase0.BLSPubKey @@ -70,13 +51,7 @@ func (b Bootstrap) MarshalJSON() ([]byte, error) { } return json.Marshal(&bootstrapJSON{ - Header: bootstrapHeaderJSON{ - Slot: fmt.Sprintf("%d", b.Header.Slot), - ProposerIndex: fmt.Sprintf("%d", b.Header.ProposerIndex), - ParentRoot: b.Header.ParentRoot.String(), - StateRoot: b.Header.StateRoot.String(), - BodyRoot: b.Header.BodyRoot.String(), - }, + Header: b.Header.ToJSON(), CurrentSyncCommittee: bootstrapCurrentSyncCommitteeJSON{ Pubkeys: pubkeys, AggregatePubkey: b.CurrentSyncCommittee.AggregatePubkey.String(), @@ -93,55 +68,9 @@ func (b *Bootstrap) UnmarshalJSON(input []byte) error { return errors.Wrap(err, "invalid JSON") } - if jsonData.Header.Slot == "" { - return errors.New("slot is required") - } - - slot, err := strconv.ParseUint(jsonData.Header.Slot, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid slot: %s", jsonData.Header.Slot)) - } - b.Header.Slot = phase0.Slot(slot) - - if jsonData.Header.ProposerIndex == "" { - return errors.New("proposer index is required") - } - - proposerIndex, err := strconv.ParseUint(jsonData.Header.ProposerIndex, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid proposer index: %s", jsonData.Header.ProposerIndex)) - } - b.Header.ProposerIndex = phase0.ValidatorIndex(proposerIndex) - - if jsonData.Header.ParentRoot == "" { - return errors.New("parent root is required") - } - - parentRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.ParentRoot, "0x")) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid parent root: %s", jsonData.Header.ParentRoot)) - } - b.Header.ParentRoot = phase0.Root(parentRoot) - - if jsonData.Header.StateRoot == "" { - return errors.New("state root is required") - } - - stateRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.StateRoot, "0x")) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid state root: %s", jsonData.Header.StateRoot)) - } - b.Header.StateRoot = phase0.Root(stateRoot) - - if jsonData.Header.BodyRoot == "" { - return errors.New("body root is required") - } - - bodyRoot, err := hex.DecodeString(strings.TrimPrefix(jsonData.Header.BodyRoot, "0x")) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid body root: %s", jsonData.Header.BodyRoot)) + if err = b.Header.Beacon.FromJSON(jsonData.Header.Beacon); err != nil { + return errors.Wrap(err, "invalid header") } - b.Header.BodyRoot = phase0.Root(bodyRoot) if len(jsonData.CurrentSyncCommitteeBranch) == 0 { return errors.New("current sync committee branch is required") @@ -187,47 +116,6 @@ func (b *Bootstrap) UnmarshalJSON(input []byte) error { return nil } -func (b *BootstrapHeader) UnmarshalJSON(input []byte) error { - var err error - - var jsonData bootstrapHeaderJSON - if err = json.Unmarshal(input, &jsonData); err != nil { - return errors.Wrap(err, "invalid JSON") - } - - slot, err := strconv.ParseUint(jsonData.Slot, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid slot: %s", jsonData.Slot)) - } - b.Slot = phase0.Slot(slot) - - proposerIndex, err := strconv.ParseUint(jsonData.ProposerIndex, 10, 64) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid proposer index: %s", jsonData.ProposerIndex)) - } - b.ProposerIndex = phase0.ValidatorIndex(proposerIndex) - - parentRoot, err := hex.DecodeString(jsonData.ParentRoot) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid parent root: %s", jsonData.ParentRoot)) - } - b.ParentRoot = phase0.Root(parentRoot) - - stateRoot, err := hex.DecodeString(jsonData.StateRoot) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid state root: %s", jsonData.StateRoot)) - } - b.StateRoot = phase0.Root(stateRoot) - - bodyRoot, err := hex.DecodeString(jsonData.BodyRoot) - if err != nil { - return errors.Wrap(err, fmt.Sprintf("invalid body root: %s", jsonData.BodyRoot)) - } - b.BodyRoot = phase0.Root(bodyRoot) - - return nil -} - func (b *BootstrapCurrentSyncCommittee) UnmarshalJSON(input []byte) error { var err error diff --git a/pkg/beacon/api/types/lightclient/bootstrap_header.go b/pkg/beacon/api/types/lightclient/bootstrap_header.go new file mode 100644 index 0000000..24905e5 --- /dev/null +++ b/pkg/beacon/api/types/lightclient/bootstrap_header.go @@ -0,0 +1,42 @@ +package lightclient + +import ( + "encoding/json" + + "github.com/pkg/errors" +) + +// BootstrapHeader is the header of a light client bootstrap. +type BootstrapHeader struct { + Beacon BeaconBlockHeader `json:"beacon"` +} + +// bootstrapHeaderJSON is the JSON representation of a bootstrap header. +type bootstrapHeaderJSON struct { + Beacon beaconBlockHeaderJSON `json:"beacon"` +} + +func (h *BootstrapHeader) ToJSON() bootstrapHeaderJSON { + return bootstrapHeaderJSON{ + Beacon: h.Beacon.ToJSON(), + } +} + +func (h *BootstrapHeader) FromJSON(input bootstrapHeaderJSON) error { + return h.Beacon.FromJSON(input.Beacon) +} + +func (h BootstrapHeader) MarshalJSON() ([]byte, error) { + return json.Marshal(h.ToJSON()) +} + +func (b *BootstrapHeader) UnmarshalJSON(input []byte) error { + var err error + + var jsonData bootstrapHeaderJSON + if err = json.Unmarshal(input, &jsonData); err != nil { + return errors.Wrap(err, "invalid JSON") + } + + return b.FromJSON(jsonData) +} diff --git a/pkg/beacon/api/types/lightclient/bootstrap_test.go b/pkg/beacon/api/types/lightclient/bootstrap_test.go index ba899ff..2195a1a 100644 --- a/pkg/beacon/api/types/lightclient/bootstrap_test.go +++ b/pkg/beacon/api/types/lightclient/bootstrap_test.go @@ -12,11 +12,13 @@ import ( func TestBootstrap_MarshalJSON(t *testing.T) { bootstrap := &lightclient.Bootstrap{ Header: lightclient.BootstrapHeader{ - Slot: 123, - ProposerIndex: 456, - ParentRoot: phase0.Root{0x01}, - StateRoot: phase0.Root{0x02}, - BodyRoot: phase0.Root{0x03}, + Beacon: lightclient.BeaconBlockHeader{ + Slot: 123, + ProposerIndex: 456, + ParentRoot: phase0.Root{0x01}, + StateRoot: phase0.Root{0x02}, + BodyRoot: phase0.Root{0x03}, + }, }, CurrentSyncCommittee: lightclient.BootstrapCurrentSyncCommittee{ Pubkeys: []phase0.BLSPubKey{{0x04}, {0x05}}, @@ -30,11 +32,13 @@ func TestBootstrap_MarshalJSON(t *testing.T) { expectedJSON := `{ "header": { - "slot": "123", - "proposer_index": "456", - "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", - "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", - "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + "beacon": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + } }, "current_sync_committee": { "pubkeys": [ @@ -54,11 +58,13 @@ func TestBootstrap_MarshalJSON(t *testing.T) { func TestBootstrap_UnmarshalJSON(t *testing.T) { jsonData := []byte(`{ "header": { - "slot": "123", - "proposer_index": "456", - "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", - "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", - "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + "beacon": { + "slot": "123", + "proposer_index": "456", + "parent_root": "0x0100000000000000000000000000000000000000000000000000000000000000", + "state_root": "0x0200000000000000000000000000000000000000000000000000000000000000", + "body_root": "0x0300000000000000000000000000000000000000000000000000000000000000" + } }, "current_sync_committee": { "pubkeys": [ @@ -77,11 +83,11 @@ func TestBootstrap_UnmarshalJSON(t *testing.T) { err := json.Unmarshal(jsonData, &bootstrap) require.NoError(t, err) - require.Equal(t, phase0.Slot(123), bootstrap.Header.Slot) - require.Equal(t, phase0.ValidatorIndex(456), bootstrap.Header.ProposerIndex) - require.Equal(t, phase0.Root{0x01}, bootstrap.Header.ParentRoot) - require.Equal(t, phase0.Root{0x02}, bootstrap.Header.StateRoot) - require.Equal(t, phase0.Root{0x03}, bootstrap.Header.BodyRoot) + require.Equal(t, phase0.Slot(123), bootstrap.Header.Beacon.Slot) + require.Equal(t, phase0.ValidatorIndex(456), bootstrap.Header.Beacon.ProposerIndex) + require.Equal(t, phase0.Root{0x01}, bootstrap.Header.Beacon.ParentRoot) + require.Equal(t, phase0.Root{0x02}, bootstrap.Header.Beacon.StateRoot) + require.Equal(t, phase0.Root{0x03}, bootstrap.Header.Beacon.BodyRoot) require.Equal(t, []phase0.BLSPubKey{{0x04}, {0x05}}, bootstrap.CurrentSyncCommittee.Pubkeys) require.Equal(t, phase0.BLSPubKey{0x06}, bootstrap.CurrentSyncCommittee.AggregatePubkey) require.Equal(t, []phase0.Root{{0x07}, {0x08}}, bootstrap.CurrentSyncCommitteeBranch)