Skip to content

Commit

Permalink
feat: Support proxying light_client routes
Browse files Browse the repository at this point in the history
  • Loading branch information
samcm committed Oct 23, 2024
1 parent 9c4823c commit 1445f02
Show file tree
Hide file tree
Showing 9 changed files with 475 additions and 499 deletions.
42 changes: 25 additions & 17 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module github.com/ethpandaops/checkpointz

go 1.17
go 1.22

require (
github.com/attestantio/go-eth2-client v0.19.9
github.com/attestantio/go-eth2-client v0.21.9
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9
github.com/creasty/defaults v1.6.0
github.com/ethpandaops/beacon v0.35.0
github.com/ethpandaops/beacon v0.42.1-0.20241023053323-d1834c10e236
github.com/ethpandaops/ethwallclock v0.2.0
github.com/go-co-op/gocron v1.18.0
github.com/julienschmidt/httprouter v1.3.0
Expand All @@ -15,14 +15,17 @@ require (
github.com/prometheus/client_golang v1.16.0
github.com/sirupsen/logrus v1.9.1
github.com/spf13/cobra v1.6.1
github.com/stretchr/testify v1.8.4
github.com/stretchr/testify v1.9.0
gopkg.in/yaml.v2 v2.4.0
)

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/go-ethereum v1.14.10 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/ferranbt/fastssz v0.1.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
Expand All @@ -33,13 +36,14 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-playground/validator/v10 v10.9.0 // indirect
github.com/goccy/go-yaml v1.9.5 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/huandu/go-clone v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.13.6 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/compress v1.16.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand All @@ -48,28 +52,32 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pk910/dynamic-ssz v0.0.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 // indirect
github.com/prysmaticlabs/go-bitfield v0.0.0-20240328144219-a1caa50c3a1e // indirect
github.com/r3labs/sse/v2 v2.10.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rs/zerolog v1.29.1 // indirect
github.com/rs/zerolog v1.32.0 // indirect
github.com/signalsciences/ac v1.2.0 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/ugorji/go/codec v1.2.6 // indirect
go.opentelemetry.io/otel v1.16.0 // indirect
go.opentelemetry.io/otel/metric v1.16.0 // indirect
go.opentelemetry.io/otel/trace v1.16.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/net v0.10.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
gopkg.in/cenkalti/backoff.v1 v1.1.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
563 changes: 81 additions & 482 deletions go.sum

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions pkg/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ func (h *Handler) Register(ctx context.Context, router *httprouter.Router) error
router.GET("/eth/v1/beacon/deposit_snapshot", h.wrappedHandler(h.handleEthV1BeaconDepositSnapshot))
router.GET("/eth/v1/beacon/blob_sidecars/:block_id", h.wrappedHandler(h.handleEthV1BeaconBlobSidecars))

// Light client
router.GET("/eth/v1/beacon/light_client/bootstrap/:root", h.wrappedHandler(h.handleEthV1BeaconLightClientBootstrap))
router.GET("/eth/v1/beacon/light_client/finality_update", h.wrappedHandler(h.handleEthV1BeaconLightClientFinalityUpdate))
router.GET("/eth/v1/beacon/light_client/optimistic_update", h.wrappedHandler(h.handleEthV1BeaconLightClientOptimisticUpdate))
router.GET("/eth/v1/beacon/light_client/updates", h.wrappedHandler(h.handleEthV1BeaconLightClientUpdates))

router.GET("/eth/v1/config/spec", h.wrappedHandler(h.handleEthV1ConfigSpec))
router.GET("/eth/v1/config/deposit_contract", h.wrappedHandler(h.handleEthV1ConfigDepositContract))
router.GET("/eth/v1/config/fork_schedule", h.wrappedHandler(h.handleEthV1ConfigForkSchedule))
Expand Down Expand Up @@ -649,3 +655,107 @@ func (h *Handler) handleEthV1BeaconBlobSidecars(ctx context.Context, r *http.Req

return rsp, nil
}

// Light client
func (h *Handler) handleEthV1BeaconLightClientBootstrap(ctx context.Context, r *http.Request, p httprouter.Params, contentType ContentType) (*HTTPResponse, error) {
if err := ValidateContentType(contentType, []ContentType{ContentTypeJSON}); err != nil {
return NewUnsupportedMediaTypeResponse(nil), err
}

root, err := eth.NewRootFromString(p.ByName("root"))
if err != nil {
return NewBadRequestResponse(nil), err
}

bootstrap, version, err := h.eth.LightClientBootstrap(ctx, root)
if err != nil {
return NewInternalServerErrorResponse(nil), err
}

rsp := NewSuccessResponse(ContentTypeResolvers{
ContentTypeJSON: bootstrap.MarshalJSON,
})

rsp.ExtraData["version"] = version

return rsp, nil
}

func (h *Handler) handleEthV1BeaconLightClientFinalityUpdate(ctx context.Context, r *http.Request, p httprouter.Params, contentType ContentType) (*HTTPResponse, error) {
if err := ValidateContentType(contentType, []ContentType{ContentTypeJSON}); err != nil {
return NewUnsupportedMediaTypeResponse(nil), err
}

finality, version, err := h.eth.LightClientFinalityUpdate(ctx)
if err != nil {
return NewInternalServerErrorResponse(nil), err
}

rsp := NewSuccessResponse(ContentTypeResolvers{
ContentTypeJSON: finality.MarshalJSON,
})

rsp.ExtraData["version"] = version

return rsp, nil
}

func (h *Handler) handleEthV1BeaconLightClientOptimisticUpdate(ctx context.Context, r *http.Request, p httprouter.Params, contentType ContentType) (*HTTPResponse, error) {
if err := ValidateContentType(contentType, []ContentType{ContentTypeJSON}); err != nil {
return NewUnsupportedMediaTypeResponse(nil), err
}

optimistic, version, err := h.eth.LightClientOptimisticUpdate(ctx)
if err != nil {
return NewInternalServerErrorResponse(nil), err
}

rsp := NewSuccessResponse(ContentTypeResolvers{
ContentTypeJSON: optimistic.MarshalJSON,
})

rsp.ExtraData["version"] = version

return rsp, nil
}

func (h *Handler) handleEthV1BeaconLightClientUpdates(ctx context.Context, r *http.Request, p httprouter.Params, contentType ContentType) (*HTTPResponse, error) {
if err := ValidateContentType(contentType, []ContentType{ContentTypeJSON}); err != nil {
return NewUnsupportedMediaTypeResponse(nil), err
}

startPeriod := r.URL.Query().Get("start_period")
if startPeriod == "" {
return NewBadRequestResponse(nil), errors.New("start_period is required")
}

startPeriodInt, err := strconv.Atoi(startPeriod)
if err != nil {
return NewBadRequestResponse(nil), fmt.Errorf("invalid start_period: %w", err)
}

count := r.URL.Query().Get("count")
if count == "" {
return NewBadRequestResponse(nil), errors.New("count is required")
}

countInt, err := strconv.Atoi(count)
if err != nil {
return NewBadRequestResponse(nil), fmt.Errorf("invalid count: %w", err)
}

updates, version, err := h.eth.LightClientUpdates(ctx, startPeriodInt, countInt)
if err != nil {
return NewInternalServerErrorResponse(nil), err
}

rsp := NewSuccessResponse(ContentTypeResolvers{
ContentTypeJSON: func() ([]byte, error) {
return json.Marshal(updates)
},
})

rsp.ExtraData["version"] = version

return rsp, nil
}
10 changes: 10 additions & 0 deletions pkg/beacon/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ type Config struct {

// Cache holds configuration for the caches.
Frontend FrontendConfig `yaml:"frontend"`

// LightClient holds configuration for serving light client data.
LightClient LightClientConfig `yaml:"light_client"`
}

// Cache configuration holds configuration for the caches.
Expand Down Expand Up @@ -47,6 +50,13 @@ type FrontendConfig struct {
BrandImageURL string `yaml:"brand_image_url"`
}

type LightClientConfig struct {
// Enabled flag enables the light client data to be served
Enabled bool `yaml:"enabled" default:"false"`
// Mode sets the mode of operation for serving light client data.
Mode LightClientMode `yaml:"mode" default:"proxy"`
}

func (c *Config) Validate() error {
if c.HistoricalEpochCount < 1 {
return errors.New("historical_epoch_count must be at least 1")
Expand Down
86 changes: 86 additions & 0 deletions pkg/beacon/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/chuckpreslar/emission"
"github.com/ethpandaops/beacon/pkg/beacon"
"github.com/ethpandaops/beacon/pkg/beacon/api"
"github.com/ethpandaops/beacon/pkg/beacon/api/types"
"github.com/ethpandaops/beacon/pkg/beacon/state"
"github.com/ethpandaops/checkpointz/pkg/beacon/checkpoints"
Expand Down Expand Up @@ -96,6 +97,10 @@ func NewDefaultProvider(namespace string, log logrus.FieldLogger, nodes []node.C
func (d *Default) Start(ctx context.Context) error {
d.log.Infof("Starting Finality provider in %s mode", d.OperatingMode())

if d.config.LightClient.Enabled {
d.log.WithField("mode", d.config.LightClient.Mode).Info("Light client is enabled!")
}

d.metrics.ObserveOperatingMode(d.OperatingMode())

if err := d.nodes.StartAll(ctx); err != nil {
Expand Down Expand Up @@ -781,3 +786,84 @@ func (d *Default) GetSlotTime(ctx context.Context, slot phase0.Slot) (eth.SlotTi
func (d *Default) GetDepositSnapshot(ctx context.Context, epoch phase0.Epoch) (*types.DepositSnapshot, error) {
return d.depositSnapshots.GetByEpoch(epoch)
}

func (d *Default) LightClientMode() LightClientMode {
return d.config.LightClient.Mode
}

func (d *Default) LightClientEnabled() bool {
return d.config.LightClient.Enabled
}

func (d *Default) getLightClientNode(ctx context.Context) (*Node, error) {
if !d.LightClientEnabled() {
return nil, errors.New("light client is not enabled")
}

if d.LightClientMode() != LightClientModeProxy {
return nil, errors.New("light client mode is not supported")
}

nodes := d.nodes.Agents(ctx, types.AgentNimbus, types.AgentLodestar)
if len(nodes) == 0 {
return nil, errors.New("no nodes running nimbus or lodestar found")
}

nodes = nodes.Healthy(ctx)
if len(nodes) == 0 {
return nil, errors.New("no healthy nodes found")
}

nodes = nodes.NotSyncing(ctx)
if len(nodes) == 0 {
return nil, errors.New("no non-syncing nodes found")
}

nodes = nodes.DataProviders(ctx)
if len(nodes) == 0 {
return nil, errors.New("no data provider nodes found")
}

n, err := nodes.RandomNode(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get random node: %w", err)
}

return n, nil
}

func (d *Default) GetLightClientBootstrap(ctx context.Context, root phase0.Root) (*api.LightClientBootstrapResponse, error) {
n, err := d.getLightClientNode(ctx)
if err != nil {
return nil, err
}

return n.Beacon.FetchLightClientBootstrap(ctx, root)
}

func (d *Default) GetLightClientFinalityUpdate(ctx context.Context) (*api.LightClientFinalityUpdateResponse, error) {
n, err := d.getLightClientNode(ctx)
if err != nil {
return nil, err
}

return n.Beacon.FetchLightClientFinalityUpdate(ctx)
}

func (d *Default) GetLightClientOptimisticUpdate(ctx context.Context) (*api.LightClientOptimisticUpdateResponse, error) {
n, err := d.getLightClientNode(ctx)
if err != nil {
return nil, err
}

return n.Beacon.FetchLightClientOptimisticUpdate(ctx)
}

func (d *Default) GetLightClientUpdates(ctx context.Context, startPeriod, count int) (*api.LightClientUpdatesResponse, error) {
n, err := d.getLightClientNode(ctx)
if err != nil {
return nil, err
}

return n.Beacon.FetchLightClientUpdates(ctx, startPeriod, count)
}
9 changes: 9 additions & 0 deletions pkg/beacon/finality_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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"
"github.com/ethpandaops/beacon/pkg/beacon/api"
"github.com/ethpandaops/beacon/pkg/beacon/api/types"
"github.com/ethpandaops/beacon/pkg/beacon/state"
"github.com/ethpandaops/checkpointz/pkg/eth"
Expand Down Expand Up @@ -60,4 +61,12 @@ type FinalityProvider interface {
GetSlotTime(ctx context.Context, slot phase0.Slot) (eth.SlotTime, error)
// GetDepositSnapshot returns the deposit snapshot at the given epoch.
GetDepositSnapshot(ctx context.Context, epoch phase0.Epoch) (*types.DepositSnapshot, error)
// GetLightClientBootstrap returns the light client bootstrap for the given block root.
GetLightClientBootstrap(ctx context.Context, root phase0.Root) (*api.LightClientBootstrapResponse, error)
// GetLightClientUpdate returns the light client update for the given block root.
GetLightClientUpdates(ctx context.Context, startPeriod int, count int) (*api.LightClientUpdatesResponse, error)
// GetLightClientFinalityUpdate returns the light client finality update.
GetLightClientFinalityUpdate(ctx context.Context) (*api.LightClientFinalityUpdateResponse, error)
// GetLightClientOptimisticUpdate returns the light client optimistic update.
GetLightClientOptimisticUpdate(ctx context.Context) (*api.LightClientOptimisticUpdateResponse, error)
}
Loading

0 comments on commit 1445f02

Please sign in to comment.