From 842f241cb92be0d93122b001045592193d3d501e Mon Sep 17 00:00:00 2001 From: kasey <489222+kasey@users.noreply.github.com> Date: Wed, 5 Feb 2025 14:40:13 -0600 Subject: [PATCH] Reduce size of api/client import graph (#14871) * relocate DownloadFinalizedData from api to sync * unexpected go mod changes --------- Co-authored-by: Kasey Kirkham --- api/client/beacon/BUILD.bazel | 22 -- api/client/beacon/checkpoint.go | 276 ------------------ api/client/beacon/client.go | 37 ++- api/client/beacon/client_test.go | 10 +- beacon-chain/sync/checkpoint/BUILD.bazel | 38 ++- beacon-chain/sync/checkpoint/api.go | 121 +++++++- beacon-chain/sync/checkpoint/api_test.go | 127 ++++++++ .../sync/checkpoint/weak-subjectivity.go | 128 ++++++++ .../sync/checkpoint/weak-subjectivity_test.go | 164 +---------- .../kasey_refactor-checkpoint-download.md | 2 + cmd/prysmctl/checkpointsync/BUILD.bazel | 1 + cmd/prysmctl/checkpointsync/download.go | 3 +- cmd/prysmctl/weaksubjectivity/BUILD.bazel | 1 + cmd/prysmctl/weaksubjectivity/checkpoint.go | 3 +- go.mod | 2 +- network/forks/ordered.go | 23 ++ 16 files changed, 494 insertions(+), 464 deletions(-) delete mode 100644 api/client/beacon/checkpoint.go create mode 100644 beacon-chain/sync/checkpoint/api_test.go create mode 100644 beacon-chain/sync/checkpoint/weak-subjectivity.go rename api/client/beacon/checkpoint_test.go => beacon-chain/sync/checkpoint/weak-subjectivity_test.go (68%) create mode 100644 changelog/kasey_refactor-checkpoint-download.md diff --git a/api/client/beacon/BUILD.bazel b/api/client/beacon/BUILD.bazel index 11e8ecb2e7..5990c8be33 100644 --- a/api/client/beacon/BUILD.bazel +++ b/api/client/beacon/BUILD.bazel @@ -3,7 +3,6 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ - "checkpoint.go", "client.go", "doc.go", "health.go", @@ -16,28 +15,19 @@ go_library( "//api/client/beacon/iface:go_default_library", "//api/server:go_default_library", "//api/server/structs:go_default_library", - "//beacon-chain/core/helpers:go_default_library", - "//beacon-chain/state:go_default_library", - "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", "//encoding/bytesutil:go_default_library", - "//encoding/ssz/detect:go_default_library", - "//io/file:go_default_library", "//network/forks:go_default_library", "//proto/prysm/v1alpha1:go_default_library", - "//runtime/version:go_default_library", - "//time/slots:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", - "@org_golang_x_mod//semver:go_default_library", ], ) go_test( name = "go_default_test", srcs = [ - "checkpoint_test.go", "client_test.go", "health_test.go", ], @@ -45,19 +35,7 @@ go_test( deps = [ "//api/client:go_default_library", "//api/client/beacon/testing:go_default_library", - "//beacon-chain/state:go_default_library", - "//config/params:go_default_library", - "//consensus-types/blocks:go_default_library", - "//consensus-types/blocks/testing:go_default_library", - "//consensus-types/primitives:go_default_library", - "//encoding/ssz/detect:go_default_library", - "//network/forks:go_default_library", - "//proto/prysm/v1alpha1:go_default_library", - "//runtime/version:go_default_library", "//testing/require:go_default_library", - "//testing/util:go_default_library", - "//time/slots:go_default_library", - "@com_github_pkg_errors//:go_default_library", "@org_uber_go_mock//gomock:go_default_library", ], ) diff --git a/api/client/beacon/checkpoint.go b/api/client/beacon/checkpoint.go deleted file mode 100644 index bfe3f503e1..0000000000 --- a/api/client/beacon/checkpoint.go +++ /dev/null @@ -1,276 +0,0 @@ -package beacon - -import ( - "context" - "fmt" - "path" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/pkg/errors" - base "github.com/prysmaticlabs/prysm/v5/api/client" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" - "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" - "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" - "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" - "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" - "github.com/prysmaticlabs/prysm/v5/encoding/ssz/detect" - "github.com/prysmaticlabs/prysm/v5/io/file" - "github.com/prysmaticlabs/prysm/v5/runtime/version" - "github.com/prysmaticlabs/prysm/v5/time/slots" - "github.com/sirupsen/logrus" - "golang.org/x/mod/semver" -) - -var errCheckpointBlockMismatch = errors.New("mismatch between checkpoint sync state and block") - -// OriginData represents the BeaconState and ReadOnlySignedBeaconBlock necessary to start an empty Beacon Node -// using Checkpoint Sync. -type OriginData struct { - sb []byte - bb []byte - st state.BeaconState - b interfaces.ReadOnlySignedBeaconBlock - vu *detect.VersionedUnmarshaler - br [32]byte - sr [32]byte -} - -// SaveBlock saves the downloaded block to a unique file in the given path. -// For readability and collision avoidance, the file name includes: type, config name, slot and root -func (o *OriginData) SaveBlock(dir string) (string, error) { - blockPath := path.Join(dir, fname("block", o.vu, o.b.Block().Slot(), o.br)) - return blockPath, file.WriteFile(blockPath, o.BlockBytes()) -} - -// SaveState saves the downloaded state to a unique file in the given path. -// For readability and collision avoidance, the file name includes: type, config name, slot and root -func (o *OriginData) SaveState(dir string) (string, error) { - statePath := path.Join(dir, fname("state", o.vu, o.st.Slot(), o.sr)) - return statePath, file.WriteFile(statePath, o.StateBytes()) -} - -// StateBytes returns the ssz-encoded bytes of the downloaded BeaconState value. -func (o *OriginData) StateBytes() []byte { - return o.sb -} - -// BlockBytes returns the ssz-encoded bytes of the downloaded ReadOnlySignedBeaconBlock value. -func (o *OriginData) BlockBytes() []byte { - return o.bb -} - -func fname(prefix string, vu *detect.VersionedUnmarshaler, slot primitives.Slot, root [32]byte) string { - return fmt.Sprintf("%s_%s_%s_%d-%#x.ssz", prefix, vu.Config.ConfigName, version.String(vu.Fork), slot, root) -} - -// DownloadFinalizedData downloads the most recently finalized state, and the block most recently applied to that state. -// This pair can be used to initialize a new beacon node via checkpoint sync. -func DownloadFinalizedData(ctx context.Context, client *Client) (*OriginData, error) { - sb, err := client.GetState(ctx, IdFinalized) - if err != nil { - return nil, err - } - vu, err := detect.FromState(sb) - if err != nil { - return nil, errors.Wrap(err, "error detecting chain config for finalized state") - } - - log.WithFields(logrus.Fields{ - "name": vu.Config.ConfigName, - "fork": version.String(vu.Fork), - }).Info("Detected supported config in remote finalized state") - - s, err := vu.UnmarshalBeaconState(sb) - if err != nil { - return nil, errors.Wrap(err, "error unmarshaling finalized state to correct version") - } - - slot := s.LatestBlockHeader().Slot - bb, err := client.GetBlock(ctx, IdFromSlot(slot)) - if err != nil { - return nil, errors.Wrapf(err, "error requesting block by slot = %d", slot) - } - b, err := vu.UnmarshalBeaconBlock(bb) - if err != nil { - return nil, errors.Wrap(err, "unable to unmarshal block to a supported type using the detected fork schedule") - } - br, err := b.Block().HashTreeRoot() - if err != nil { - return nil, errors.Wrap(err, "error computing hash_tree_root of retrieved block") - } - bodyRoot, err := b.Block().Body().HashTreeRoot() - if err != nil { - return nil, errors.Wrap(err, "error computing hash_tree_root of retrieved block body") - } - - sbr := bytesutil.ToBytes32(s.LatestBlockHeader().BodyRoot) - if sbr != bodyRoot { - return nil, errors.Wrapf(errCheckpointBlockMismatch, "state body root = %#x, block body root = %#x", sbr, bodyRoot) - } - sr, err := s.HashTreeRoot(ctx) - if err != nil { - return nil, errors.Wrapf(err, "failed to compute htr for finalized state at slot=%d", s.Slot()) - } - - log. - WithField("blockSlot", b.Block().Slot()). - WithField("stateSlot", s.Slot()). - WithField("stateRoot", hexutil.Encode(sr[:])). - WithField("blockRoot", hexutil.Encode(br[:])). - Info("Downloaded checkpoint sync state and block.") - return &OriginData{ - st: s, - b: b, - sb: sb, - bb: bb, - vu: vu, - br: br, - sr: sr, - }, nil -} - -// WeakSubjectivityData represents the state root, block root and epoch of the BeaconState + ReadOnlySignedBeaconBlock -// that falls at the beginning of the current weak subjectivity period. These values can be used to construct -// a weak subjectivity checkpoint beacon node flag to be used for validation. -type WeakSubjectivityData struct { - BlockRoot [32]byte - StateRoot [32]byte - Epoch primitives.Epoch -} - -// CheckpointString returns the standard string representation of a Checkpoint. -// The format is a hex-encoded block root, followed by the epoch of the block, separated by a colon. For example: -// "0x1c35540cac127315fabb6bf29181f2ae0de1a3fc909d2e76ba771e61312cc49a:74888" -func (wsd *WeakSubjectivityData) CheckpointString() string { - return fmt.Sprintf("%#x:%d", wsd.BlockRoot, wsd.Epoch) -} - -// ComputeWeakSubjectivityCheckpoint attempts to use the prysm weak_subjectivity api -// to obtain the current weak_subjectivity checkpoint. -// For non-prysm nodes, the same computation will be performed with extra steps, -// using the head state downloaded from the beacon node api. -func ComputeWeakSubjectivityCheckpoint(ctx context.Context, client *Client) (*WeakSubjectivityData, error) { - ws, err := client.GetWeakSubjectivity(ctx) - if err != nil { - // a 404/405 is expected if querying an endpoint that doesn't support the weak subjectivity checkpoint api - if !errors.Is(err, base.ErrNotOK) { - return nil, errors.Wrap(err, "unexpected API response for prysm-only weak subjectivity checkpoint API") - } - // fall back to vanilla Beacon Node API method - return computeBackwardsCompatible(ctx, client) - } - log.Printf("server weak subjectivity checkpoint response - epoch=%d, block_root=%#x, state_root=%#x", ws.Epoch, ws.BlockRoot, ws.StateRoot) - return ws, nil -} - -const ( - prysmMinimumVersion = "v2.0.7" - prysmImplementationName = "Prysm" -) - -// errUnsupportedPrysmCheckpointVersion indicates remote beacon node can't be used for checkpoint retrieval. -var errUnsupportedPrysmCheckpointVersion = errors.New("node does not meet minimum version requirements for checkpoint retrieval") - -// for older endpoints or clients that do not support the weak_subjectivity api method -// we gather the necessary data for a checkpoint sync by: -// - inspecting the remote server's head state and computing the weak subjectivity epoch locally -// - requesting the state at the first slot of the epoch -// - using hash_tree_root(state.latest_block_header) to compute the block the state integrates -// - requesting that block by its root -func computeBackwardsCompatible(ctx context.Context, client *Client) (*WeakSubjectivityData, error) { - log.Print("falling back to generic checkpoint derivation, weak_subjectivity API not supported by server") - nv, err := client.GetNodeVersion(ctx) - if err != nil { - return nil, errors.Wrap(err, "unable to proceed with fallback method without confirming node version") - } - if nv.implementation == prysmImplementationName && semver.Compare(nv.semver, prysmMinimumVersion) < 0 { - return nil, errors.Wrapf(errUnsupportedPrysmCheckpointVersion, "%s < minimum (%s)", nv.semver, prysmMinimumVersion) - } - epoch, err := getWeakSubjectivityEpochFromHead(ctx, client) - if err != nil { - return nil, errors.Wrap(err, "error computing weak subjectivity epoch via head state inspection") - } - - // use first slot of the epoch for the state slot - slot, err := slots.EpochStart(epoch) - if err != nil { - return nil, errors.Wrapf(err, "error computing first slot of epoch=%d", epoch) - } - - log.Printf("requesting checkpoint state at slot %d", slot) - // get the state at the first slot of the epoch - sb, err := client.GetState(ctx, IdFromSlot(slot)) - if err != nil { - return nil, errors.Wrapf(err, "failed to request state by slot from api, slot=%d", slot) - } - - // ConfigFork is used to unmarshal the BeaconState so we can read the block root in latest_block_header - vu, err := detect.FromState(sb) - if err != nil { - return nil, errors.Wrap(err, "error detecting chain config for beacon state") - } - log.Printf("detected supported config in checkpoint state, name=%s, fork=%s", vu.Config.ConfigName, version.String(vu.Fork)) - - s, err := vu.UnmarshalBeaconState(sb) - if err != nil { - return nil, errors.Wrap(err, "error using detected config fork to unmarshal state bytes") - } - - // compute state and block roots - sr, err := s.HashTreeRoot(ctx) - if err != nil { - return nil, errors.Wrap(err, "error computing hash_tree_root of state") - } - - h := s.LatestBlockHeader() - h.StateRoot = sr[:] - br, err := h.HashTreeRoot() - if err != nil { - return nil, errors.Wrap(err, "error while computing block root using state data") - } - - bb, err := client.GetBlock(ctx, IdFromRoot(br)) - if err != nil { - return nil, errors.Wrapf(err, "error requesting block by root = %d", br) - } - b, err := vu.UnmarshalBeaconBlock(bb) - if err != nil { - return nil, errors.Wrap(err, "unable to unmarshal block to a supported type using the detected fork schedule") - } - br, err = b.Block().HashTreeRoot() - if err != nil { - return nil, errors.Wrap(err, "error computing hash_tree_root for block obtained via root") - } - - return &WeakSubjectivityData{ - Epoch: epoch, - BlockRoot: br, - StateRoot: sr, - }, nil -} - -// this method downloads the head state, which can be used to find the correct chain config -// and use prysm's helper methods to compute the latest weak subjectivity epoch. -func getWeakSubjectivityEpochFromHead(ctx context.Context, client *Client) (primitives.Epoch, error) { - headBytes, err := client.GetState(ctx, IdHead) - if err != nil { - return 0, err - } - vu, err := detect.FromState(headBytes) - if err != nil { - return 0, errors.Wrap(err, "error detecting chain config for beacon state") - } - log.Printf("detected supported config in remote head state, name=%s, fork=%s", vu.Config.ConfigName, version.String(vu.Fork)) - headState, err := vu.UnmarshalBeaconState(headBytes) - if err != nil { - return 0, errors.Wrap(err, "error unmarshaling state to correct version") - } - - epoch, err := helpers.LatestWeakSubjectivityEpoch(ctx, headState, vu.Config) - if err != nil { - return 0, errors.Wrap(err, "error computing the weak subjectivity epoch from head state") - } - - log.Printf("(computed client-side) weak subjectivity epoch = %d", epoch) - return epoch, nil -} diff --git a/api/client/beacon/client.go b/api/client/beacon/client.go index 04cdd46fa1..8af0b38bf4 100644 --- a/api/client/beacon/client.go +++ b/api/client/beacon/client.go @@ -29,12 +29,13 @@ const ( getSignedBlockPath = "/eth/v2/beacon/blocks" getBlockRootPath = "/eth/v1/beacon/blocks/{{.Id}}/root" getForkForStatePath = "/eth/v1/beacon/states/{{.Id}}/fork" - getWeakSubjectivityPath = "/prysm/v1/beacon/weak_subjectivity" getForkSchedulePath = "/eth/v1/config/fork_schedule" getConfigSpecPath = "/eth/v1/config/spec" getStatePath = "/eth/v2/debug/beacon/states" - getNodeVersionPath = "/eth/v1/node/version" changeBLStoExecutionPath = "/eth/v1/beacon/pool/bls_to_execution_changes" + + GetNodeVersionPath = "/eth/v1/node/version" + GetWeakSubjectivityPath = "/prysm/v1/beacon/weak_subjectivity" ) // StateOrBlockId represents the block_id / state_id parameters that several of the Eth Beacon API methods accept. @@ -80,7 +81,8 @@ func idTemplate(ts string) func(StateOrBlockId) string { return f } -func renderGetBlockPath(id StateOrBlockId) string { +// RenderGetBlockPath formats a block id into a path for the GetBlock API endpoint. +func RenderGetBlockPath(id StateOrBlockId) string { return path.Join(getSignedBlockPath, string(id)) } @@ -104,7 +106,7 @@ func NewClient(host string, opts ...client.ClientOpt) (*Client, error) { // for the named identifiers. // The return value contains the ssz-encoded bytes. func (c *Client) GetBlock(ctx context.Context, blockId StateOrBlockId) ([]byte, error) { - blockPath := renderGetBlockPath(blockId) + blockPath := RenderGetBlockPath(blockId) b, err := c.Get(ctx, blockPath, client.WithSSZEncoding()) if err != nil { return nil, errors.Wrapf(err, "error requesting state by id = %s", blockId) @@ -195,6 +197,10 @@ type NodeVersion struct { systemInfo string } +func (nv *NodeVersion) SetImplementation(impl string) { + nv.implementation = impl +} + var versionRE = regexp.MustCompile(`^(\w+)/(v\d+\.\d+\.\d+[-a-zA-Z0-9]*)\s*/?(.*)$`) func parseNodeVersion(v string) (*NodeVersion, error) { @@ -212,7 +218,7 @@ func parseNodeVersion(v string) (*NodeVersion, error) { // GetNodeVersion requests that the beacon node identify information about its implementation in a format // similar to a HTTP User-Agent field. ex: Lighthouse/v0.1.5 (Linux x86_64) func (c *Client) GetNodeVersion(ctx context.Context) (*NodeVersion, error) { - b, err := c.Get(ctx, getNodeVersionPath) + b, err := c.Get(ctx, GetNodeVersionPath) if err != nil { return nil, errors.Wrap(err, "error requesting node version") } @@ -228,7 +234,8 @@ func (c *Client) GetNodeVersion(ctx context.Context) (*NodeVersion, error) { return parseNodeVersion(d.Data.Version) } -func renderGetStatePath(id StateOrBlockId) string { +// RenderGetStatePath formats a state id into a path for the GetState API endpoint. +func RenderGetStatePath(id StateOrBlockId) string { return path.Join(getStatePath, string(id)) } @@ -246,13 +253,29 @@ func (c *Client) GetState(ctx context.Context, stateId StateOrBlockId) ([]byte, return b, nil } +// WeakSubjectivityData represents the state root, block root and epoch of the BeaconState + ReadOnlySignedBeaconBlock +// that falls at the beginning of the current weak subjectivity period. These values can be used to construct +// a weak subjectivity checkpoint beacon node flag to be used for validation. +type WeakSubjectivityData struct { + BlockRoot [32]byte + StateRoot [32]byte + Epoch primitives.Epoch +} + +// CheckpointString returns the standard string representation of a Checkpoint. +// The format is a hex-encoded block root, followed by the epoch of the block, separated by a colon. For example: +// "0x1c35540cac127315fabb6bf29181f2ae0de1a3fc909d2e76ba771e61312cc49a:74888" +func (wsd *WeakSubjectivityData) CheckpointString() string { + return fmt.Sprintf("%#x:%d", wsd.BlockRoot, wsd.Epoch) +} + // GetWeakSubjectivity calls a proposed API endpoint that is unique to prysm // This api method does the following: // - computes weak subjectivity epoch // - finds the highest non-skipped block preceding the epoch // - returns the htr of the found block and returns this + the value of state_root from the block func (c *Client) GetWeakSubjectivity(ctx context.Context) (*WeakSubjectivityData, error) { - body, err := c.Get(ctx, getWeakSubjectivityPath) + body, err := c.Get(ctx, GetWeakSubjectivityPath) if err != nil { return nil, err } diff --git a/api/client/beacon/client_test.go b/api/client/beacon/client_test.go index 4c84f36426..5e7adbad52 100644 --- a/api/client/beacon/client_test.go +++ b/api/client/beacon/client_test.go @@ -97,31 +97,31 @@ func TestValidHostname(t *testing.T) { { name: "hostname with port", hostArg: "mydomain.org:3500", - path: getNodeVersionPath, + path: GetNodeVersionPath, joined: "http://mydomain.org:3500/eth/v1/node/version", }, { name: "https scheme, hostname with port", hostArg: "https://mydomain.org:3500", - path: getNodeVersionPath, + path: GetNodeVersionPath, joined: "https://mydomain.org:3500/eth/v1/node/version", }, { name: "http scheme, hostname without port", hostArg: "http://mydomain.org", - path: getNodeVersionPath, + path: GetNodeVersionPath, joined: "http://mydomain.org/eth/v1/node/version", }, { name: "http scheme, trailing slash, hostname without port", hostArg: "http://mydomain.org/", - path: getNodeVersionPath, + path: GetNodeVersionPath, joined: "http://mydomain.org/eth/v1/node/version", }, { name: "http scheme, hostname with basic auth creds and no port", hostArg: "http://username:pass@mydomain.org/", - path: getNodeVersionPath, + path: GetNodeVersionPath, joined: "http://username:pass@mydomain.org/eth/v1/node/version", }, } diff --git a/beacon-chain/sync/checkpoint/BUILD.bazel b/beacon-chain/sync/checkpoint/BUILD.bazel index 9f437f27e1..f5c5789fcd 100644 --- a/beacon-chain/sync/checkpoint/BUILD.bazel +++ b/beacon-chain/sync/checkpoint/BUILD.bazel @@ -1,4 +1,4 @@ -load("@prysm//tools/go:def.bzl", "go_library") +load("@prysm//tools/go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", @@ -6,16 +6,52 @@ go_library( "api.go", "file.go", "log.go", + "weak-subjectivity.go", ], importpath = "github.com/prysmaticlabs/prysm/v5/beacon-chain/sync/checkpoint", visibility = ["//visibility:public"], deps = [ "//api/client:go_default_library", "//api/client/beacon:go_default_library", + "//beacon-chain/core/helpers:go_default_library", "//beacon-chain/db:go_default_library", + "//beacon-chain/state:go_default_library", "//config/params:go_default_library", + "//consensus-types/interfaces:go_default_library", + "//consensus-types/primitives:go_default_library", + "//encoding/bytesutil:go_default_library", + "//encoding/ssz/detect:go_default_library", "//io/file:go_default_library", + "//runtime/version:go_default_library", + "//time/slots:go_default_library", + "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", ], ) + +go_test( + name = "go_default_test", + srcs = [ + "api_test.go", + "weak-subjectivity_test.go", + ], + embed = [":go_default_library"], + deps = [ + "//api/client:go_default_library", + "//api/client/beacon:go_default_library", + "//beacon-chain/state:go_default_library", + "//config/params:go_default_library", + "//consensus-types/blocks:go_default_library", + "//consensus-types/blocks/testing:go_default_library", + "//consensus-types/primitives:go_default_library", + "//encoding/ssz/detect:go_default_library", + "//network/forks:go_default_library", + "//proto/prysm/v1alpha1:go_default_library", + "//runtime/version:go_default_library", + "//testing/require:go_default_library", + "//testing/util:go_default_library", + "//time/slots:go_default_library", + "@com_github_pkg_errors//:go_default_library", + ], +) diff --git a/beacon-chain/sync/checkpoint/api.go b/beacon-chain/sync/checkpoint/api.go index 44a6fb2456..6f201bd502 100644 --- a/beacon-chain/sync/checkpoint/api.go +++ b/beacon-chain/sync/checkpoint/api.go @@ -2,14 +2,27 @@ package checkpoint import ( "context" + "fmt" + "path" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api/client" "github.com/prysmaticlabs/prysm/v5/api/client/beacon" "github.com/prysmaticlabs/prysm/v5/beacon-chain/db" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v5/encoding/ssz/detect" + "github.com/prysmaticlabs/prysm/v5/io/file" + "github.com/prysmaticlabs/prysm/v5/runtime/version" + "github.com/sirupsen/logrus" ) +var errCheckpointBlockMismatch = errors.New("mismatch between checkpoint sync state and block") + // APIInitializer manages initializing the beacon node using checkpoint sync, retrieving the checkpoint state and root // from the remote beacon node api. type APIInitializer struct { @@ -37,9 +50,115 @@ func (dl *APIInitializer) Initialize(ctx context.Context, d db.Database) error { if err != nil && !errors.Is(err, db.ErrNotFound) { return errors.Wrap(err, "error while checking database for origin root") } - od, err := beacon.DownloadFinalizedData(ctx, dl.c) + od, err := DownloadFinalizedData(ctx, dl.c) if err != nil { return errors.Wrap(err, "Error retrieving checkpoint origin state and block") } return d.SaveOrigin(ctx, od.StateBytes(), od.BlockBytes()) } + +// OriginData represents the BeaconState and ReadOnlySignedBeaconBlock necessary to start an empty Beacon Node +// using Checkpoint Sync. +type OriginData struct { + sb []byte + bb []byte + st state.BeaconState + b interfaces.ReadOnlySignedBeaconBlock + vu *detect.VersionedUnmarshaler + br [32]byte + sr [32]byte +} + +// SaveBlock saves the downloaded block to a unique file in the given path. +// For readability and collision avoidance, the file name includes: type, config name, slot and root +func (o *OriginData) SaveBlock(dir string) (string, error) { + blockPath := path.Join(dir, fname("block", o.vu, o.b.Block().Slot(), o.br)) + return blockPath, file.WriteFile(blockPath, o.BlockBytes()) +} + +// SaveState saves the downloaded state to a unique file in the given path. +// For readability and collision avoidance, the file name includes: type, config name, slot and root +func (o *OriginData) SaveState(dir string) (string, error) { + statePath := path.Join(dir, fname("state", o.vu, o.st.Slot(), o.sr)) + return statePath, file.WriteFile(statePath, o.StateBytes()) +} + +// StateBytes returns the ssz-encoded bytes of the downloaded BeaconState value. +func (o *OriginData) StateBytes() []byte { + return o.sb +} + +// BlockBytes returns the ssz-encoded bytes of the downloaded ReadOnlySignedBeaconBlock value. +func (o *OriginData) BlockBytes() []byte { + return o.bb +} + +func fname(prefix string, vu *detect.VersionedUnmarshaler, slot primitives.Slot, root [32]byte) string { + return fmt.Sprintf("%s_%s_%s_%d-%#x.ssz", prefix, vu.Config.ConfigName, version.String(vu.Fork), slot, root) +} + +// DownloadFinalizedData downloads the most recently finalized state, and the block most recently applied to that state. +// This pair can be used to initialize a new beacon node via checkpoint sync. +func DownloadFinalizedData(ctx context.Context, client *beacon.Client) (*OriginData, error) { + sb, err := client.GetState(ctx, beacon.IdFinalized) + if err != nil { + return nil, err + } + vu, err := detect.FromState(sb) + if err != nil { + return nil, errors.Wrap(err, "error detecting chain config for finalized state") + } + + log.WithFields(logrus.Fields{ + "name": vu.Config.ConfigName, + "fork": version.String(vu.Fork), + }).Info("Detected supported config in remote finalized state") + + s, err := vu.UnmarshalBeaconState(sb) + if err != nil { + return nil, errors.Wrap(err, "error unmarshaling finalized state to correct version") + } + + slot := s.LatestBlockHeader().Slot + bb, err := client.GetBlock(ctx, beacon.IdFromSlot(slot)) + if err != nil { + return nil, errors.Wrapf(err, "error requesting block by slot = %d", slot) + } + b, err := vu.UnmarshalBeaconBlock(bb) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal block to a supported type using the detected fork schedule") + } + br, err := b.Block().HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "error computing hash_tree_root of retrieved block") + } + bodyRoot, err := b.Block().Body().HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "error computing hash_tree_root of retrieved block body") + } + + sbr := bytesutil.ToBytes32(s.LatestBlockHeader().BodyRoot) + if sbr != bodyRoot { + return nil, errors.Wrapf(errCheckpointBlockMismatch, "state body root = %#x, block body root = %#x", sbr, bodyRoot) + } + sr, err := s.HashTreeRoot(ctx) + if err != nil { + return nil, errors.Wrapf(err, "failed to compute htr for finalized state at slot=%d", s.Slot()) + } + + log. + WithField("blockSlot", b.Block().Slot()). + WithField("stateSlot", s.Slot()). + WithField("stateRoot", hexutil.Encode(sr[:])). + WithField("blockRoot", hexutil.Encode(br[:])). + Info("Downloaded checkpoint sync state and block.") + return &OriginData{ + st: s, + b: b, + sb: sb, + bb: bb, + vu: vu, + br: br, + sr: sr, + }, nil +} diff --git a/beacon-chain/sync/checkpoint/api_test.go b/beacon-chain/sync/checkpoint/api_test.go new file mode 100644 index 0000000000..67ed35fe2c --- /dev/null +++ b/beacon-chain/sync/checkpoint/api_test.go @@ -0,0 +1,127 @@ +package checkpoint + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/api/client" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/config/params" + "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" + blocktest "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks/testing" + "github.com/prysmaticlabs/prysm/v5/encoding/ssz/detect" + "github.com/prysmaticlabs/prysm/v5/network/forks" + "github.com/prysmaticlabs/prysm/v5/testing/require" + "github.com/prysmaticlabs/prysm/v5/testing/util" + "github.com/prysmaticlabs/prysm/v5/time/slots" +) + +func TestDownloadFinalizedData(t *testing.T) { + ctx := context.Background() + cfg := params.MainnetConfig().Copy() + + // avoid the altair zone because genesis tests are easier to set up + epoch := cfg.AltairForkEpoch - 1 + // set up checkpoint state, using the epoch that will be computed as the ws checkpoint state based on the head state + slot, err := slots.EpochStart(epoch) + require.NoError(t, err) + st, err := util.NewBeaconState() + require.NoError(t, err) + fork, err := forks.ForkForEpochFromConfig(cfg, epoch) + require.NoError(t, err) + require.NoError(t, st.SetFork(fork)) + require.NoError(t, st.SetSlot(slot)) + + // set up checkpoint block + b, err := blocks.NewSignedBeaconBlock(util.NewBeaconBlock()) + require.NoError(t, err) + b, err = blocktest.SetBlockParentRoot(b, cfg.ZeroHash) + require.NoError(t, err) + b, err = blocktest.SetBlockSlot(b, slot) + require.NoError(t, err) + b, err = blocktest.SetProposerIndex(b, 0) + require.NoError(t, err) + + // set up state header pointing at checkpoint block - this is how the block is downloaded by root + header, err := b.Header() + require.NoError(t, err) + require.NoError(t, st.SetLatestBlockHeader(header.Header)) + + // order of operations can be confusing here: + // - when computing the state root, make sure block header is complete, EXCEPT the state root should be zero-value + // - before computing the block root (to match the request route), the block should include the state root + // *computed from the state with a header that does not have a state root set yet* + sr, err := st.HashTreeRoot(ctx) + require.NoError(t, err) + + b, err = blocktest.SetBlockStateRoot(b, sr) + require.NoError(t, err) + mb, err := b.MarshalSSZ() + require.NoError(t, err) + br, err := b.Block().HashTreeRoot() + require.NoError(t, err) + + ms, err := st.MarshalSSZ() + require.NoError(t, err) + + trans := &testRT{rt: func(req *http.Request) (*http.Response, error) { + res := &http.Response{Request: req} + switch req.URL.Path { + case beacon.RenderGetStatePath(beacon.IdFinalized): + res.StatusCode = http.StatusOK + res.Body = io.NopCloser(bytes.NewBuffer(ms)) + case beacon.RenderGetBlockPath(beacon.IdFromSlot(b.Block().Slot())): + res.StatusCode = http.StatusOK + res.Body = io.NopCloser(bytes.NewBuffer(mb)) + default: + res.StatusCode = http.StatusInternalServerError + res.Body = io.NopCloser(bytes.NewBufferString("")) + } + + return res, nil + }} + c, err := beacon.NewClient("http://localhost:3500", client.WithRoundTripper(trans)) + require.NoError(t, err) + // sanity check before we go through checkpoint + // make sure we can download the state and unmarshal it with the VersionedUnmarshaler + sb, err := c.GetState(ctx, beacon.IdFinalized) + require.NoError(t, err) + require.Equal(t, true, bytes.Equal(sb, ms)) + vu, err := detect.FromState(sb) + require.NoError(t, err) + us, err := vu.UnmarshalBeaconState(sb) + require.NoError(t, err) + ushtr, err := us.HashTreeRoot(ctx) + require.NoError(t, err) + require.Equal(t, sr, ushtr) + + expected := &OriginData{ + sb: ms, + bb: mb, + br: br, + sr: sr, + } + od, err := DownloadFinalizedData(ctx, c) + require.NoError(t, err) + require.Equal(t, true, bytes.Equal(expected.sb, od.sb)) + require.Equal(t, true, bytes.Equal(expected.bb, od.bb)) + require.Equal(t, expected.br, od.br) + require.Equal(t, expected.sr, od.sr) +} + +type testRT struct { + rt func(*http.Request) (*http.Response, error) +} + +func (rt *testRT) RoundTrip(req *http.Request) (*http.Response, error) { + if rt.rt != nil { + return rt.rt(req) + } + return nil, errors.New("RoundTripper not implemented") +} + +var _ http.RoundTripper = &testRT{} diff --git a/beacon-chain/sync/checkpoint/weak-subjectivity.go b/beacon-chain/sync/checkpoint/weak-subjectivity.go new file mode 100644 index 0000000000..1666d36f87 --- /dev/null +++ b/beacon-chain/sync/checkpoint/weak-subjectivity.go @@ -0,0 +1,128 @@ +package checkpoint + +import ( + "context" + + "github.com/pkg/errors" + base "github.com/prysmaticlabs/prysm/v5/api/client" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/core/helpers" + "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" + "github.com/prysmaticlabs/prysm/v5/encoding/ssz/detect" + "github.com/prysmaticlabs/prysm/v5/runtime/version" + "github.com/prysmaticlabs/prysm/v5/time/slots" +) + +// ComputeWeakSubjectivityCheckpoint attempts to use the prysm weak_subjectivity api +// to obtain the current weak_subjectivity checkpoint. +// For non-prysm nodes, the same computation will be performed with extra steps, +// using the head state downloaded from the beacon node api. +func ComputeWeakSubjectivityCheckpoint(ctx context.Context, client *beacon.Client) (*beacon.WeakSubjectivityData, error) { + ws, err := client.GetWeakSubjectivity(ctx) + if err != nil { + // a 404/405 is expected if querying an endpoint that doesn't support the weak subjectivity checkpoint api + if !errors.Is(err, base.ErrNotOK) { + return nil, errors.Wrap(err, "unexpected API response for prysm-only weak subjectivity checkpoint API") + } + // fall back to vanilla Beacon Node API method + return computeBackwardsCompatible(ctx, client) + } + log.Printf("server weak subjectivity checkpoint response - epoch=%d, block_root=%#x, state_root=%#x", ws.Epoch, ws.BlockRoot, ws.StateRoot) + return ws, nil +} + +// for clients that do not support the weak_subjectivity api method we gather the necessary data for a checkpoint sync by: +// - inspecting the remote server's head state and computing the weak subjectivity epoch locally +// - requesting the state at the first slot of the epoch +// - using hash_tree_root(state.latest_block_header) to compute the block the state integrates +// - requesting that block by its root +func computeBackwardsCompatible(ctx context.Context, client *beacon.Client) (*beacon.WeakSubjectivityData, error) { + log.Print("falling back to generic checkpoint derivation, weak_subjectivity API not supported by server") + epoch, err := getWeakSubjectivityEpochFromHead(ctx, client) + if err != nil { + return nil, errors.Wrap(err, "error computing weak subjectivity epoch via head state inspection") + } + + // use first slot of the epoch for the state slot + slot, err := slots.EpochStart(epoch) + if err != nil { + return nil, errors.Wrapf(err, "error computing first slot of epoch=%d", epoch) + } + + log.Printf("requesting checkpoint state at slot %d", slot) + // get the state at the first slot of the epoch + sb, err := client.GetState(ctx, beacon.IdFromSlot(slot)) + if err != nil { + return nil, errors.Wrapf(err, "failed to request state by slot from api, slot=%d", slot) + } + + // ConfigFork is used to unmarshal the BeaconState so we can read the block root in latest_block_header + vu, err := detect.FromState(sb) + if err != nil { + return nil, errors.Wrap(err, "error detecting chain config for beacon state") + } + log.Printf("detected supported config in checkpoint state, name=%s, fork=%s", vu.Config.ConfigName, version.String(vu.Fork)) + + s, err := vu.UnmarshalBeaconState(sb) + if err != nil { + return nil, errors.Wrap(err, "error using detected config fork to unmarshal state bytes") + } + + // compute state and block roots + sr, err := s.HashTreeRoot(ctx) + if err != nil { + return nil, errors.Wrap(err, "error computing hash_tree_root of state") + } + + h := s.LatestBlockHeader() + h.StateRoot = sr[:] + br, err := h.HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "error while computing block root using state data") + } + + bb, err := client.GetBlock(ctx, beacon.IdFromRoot(br)) + if err != nil { + return nil, errors.Wrapf(err, "error requesting block by root = %d", br) + } + b, err := vu.UnmarshalBeaconBlock(bb) + if err != nil { + return nil, errors.Wrap(err, "unable to unmarshal block to a supported type using the detected fork schedule") + } + br, err = b.Block().HashTreeRoot() + if err != nil { + return nil, errors.Wrap(err, "error computing hash_tree_root for block obtained via root") + } + + return &beacon.WeakSubjectivityData{ + Epoch: epoch, + BlockRoot: br, + StateRoot: sr, + }, nil +} + +// this method downloads the head state, which can be used to find the correct chain config +// and use prysm's helper methods to compute the latest weak subjectivity epoch. +func getWeakSubjectivityEpochFromHead(ctx context.Context, client *beacon.Client) (primitives.Epoch, error) { + headBytes, err := client.GetState(ctx, beacon.IdHead) + if err != nil { + return 0, err + } + vu, err := detect.FromState(headBytes) + if err != nil { + return 0, errors.Wrap(err, "error detecting chain config for beacon state") + } + log.Printf("detected supported config in remote head state, name=%s, fork=%s", vu.Config.ConfigName, version.String(vu.Fork)) + headState, err := vu.UnmarshalBeaconState(headBytes) + if err != nil { + return 0, errors.Wrap(err, "error unmarshaling state to correct version") + } + + epoch, err := helpers.LatestWeakSubjectivityEpoch(ctx, headState, vu.Config) + if err != nil { + return 0, errors.Wrap(err, "error computing the weak subjectivity epoch from head state") + } + + log.Printf("(computed client-side) weak subjectivity epoch = %d", epoch) + return epoch, nil +} diff --git a/api/client/beacon/checkpoint_test.go b/beacon-chain/sync/checkpoint/weak-subjectivity_test.go similarity index 68% rename from api/client/beacon/checkpoint_test.go rename to beacon-chain/sync/checkpoint/weak-subjectivity_test.go index 250608ebf9..e090f8cec1 100644 --- a/api/client/beacon/checkpoint_test.go +++ b/beacon-chain/sync/checkpoint/weak-subjectivity_test.go @@ -1,4 +1,4 @@ -package beacon +package checkpoint import ( "bytes" @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/v5/api/client" + "github.com/prysmaticlabs/prysm/v5/api/client/beacon" "github.com/prysmaticlabs/prysm/v5/beacon-chain/state" "github.com/prysmaticlabs/prysm/v5/config/params" "github.com/prysmaticlabs/prysm/v5/consensus-types/blocks" @@ -25,19 +26,6 @@ import ( "github.com/prysmaticlabs/prysm/v5/time/slots" ) -type testRT struct { - rt func(*http.Request) (*http.Response, error) -} - -func (rt *testRT) RoundTrip(req *http.Request) (*http.Response, error) { - if rt.rt != nil { - return rt.rt(req) - } - return nil, errors.New("RoundTripper not implemented") -} - -var _ http.RoundTripper = &testRT{} - func marshalToEnvelope(val interface{}) ([]byte, error) { raw, err := json.Marshal(val) if err != nil { @@ -63,35 +51,6 @@ func TestMarshalToEnvelope(t *testing.T) { require.Equal(t, expected, string(encoded)) } -func TestFallbackVersionCheck(t *testing.T) { - trans := &testRT{rt: func(req *http.Request) (*http.Response, error) { - res := &http.Response{Request: req} - switch req.URL.Path { - case getNodeVersionPath: - res.StatusCode = http.StatusOK - b := bytes.NewBuffer(nil) - d := struct { - Version string `json:"version"` - }{ - Version: "Prysm/v2.0.5 (linux amd64)", - } - encoded, err := marshalToEnvelope(d) - require.NoError(t, err) - b.Write(encoded) - res.Body = io.NopCloser(b) - case getWeakSubjectivityPath: - res.StatusCode = http.StatusNotFound - } - return res, nil - }} - - c, err := NewClient("http://localhost:3500", client.WithRoundTripper(trans)) - require.NoError(t, err) - ctx := context.Background() - _, err = ComputeWeakSubjectivityCheckpoint(ctx, c) - require.ErrorIs(t, err, errUnsupportedPrysmCheckpointVersion) -} - func TestFname(t *testing.T) { vu := &detect.VersionedUnmarshaler{ Config: params.MainnetConfig(), @@ -160,7 +119,7 @@ func TestDownloadWeakSubjectivityCheckpoint(t *testing.T) { wsSerialized, err := wst.MarshalSSZ() require.NoError(t, err) - expectedWSD := WeakSubjectivityData{ + expectedWSD := beacon.WeakSubjectivityData{ BlockRoot: bRoot, StateRoot: wRoot, Epoch: epoch, @@ -169,7 +128,7 @@ func TestDownloadWeakSubjectivityCheckpoint(t *testing.T) { trans := &testRT{rt: func(req *http.Request) (*http.Response, error) { res := &http.Response{Request: req} switch req.URL.Path { - case getWeakSubjectivityPath: + case beacon.GetWeakSubjectivityPath: res.StatusCode = http.StatusOK cp := struct { Epoch string `json:"epoch"` @@ -188,10 +147,10 @@ func TestDownloadWeakSubjectivityCheckpoint(t *testing.T) { rb, err := marshalToEnvelope(wsr) require.NoError(t, err) res.Body = io.NopCloser(bytes.NewBuffer(rb)) - case renderGetStatePath(IdFromSlot(wSlot)): + case beacon.RenderGetStatePath(beacon.IdFromSlot(wSlot)): res.StatusCode = http.StatusOK res.Body = io.NopCloser(bytes.NewBuffer(wsSerialized)) - case renderGetBlockPath(IdFromRoot(bRoot)): + case beacon.RenderGetBlockPath(beacon.IdFromRoot(bRoot)): res.StatusCode = http.StatusOK res.Body = io.NopCloser(bytes.NewBuffer(serBlock)) } @@ -199,7 +158,7 @@ func TestDownloadWeakSubjectivityCheckpoint(t *testing.T) { return res, nil }} - c, err := NewClient("http://localhost:3500", client.WithRoundTripper(trans)) + c, err := beacon.NewClient("http://localhost:3500", client.WithRoundTripper(trans)) require.NoError(t, err) wsd, err := ComputeWeakSubjectivityCheckpoint(ctx, c) @@ -263,7 +222,7 @@ func TestDownloadBackwardsCompatibleCombined(t *testing.T) { trans := &testRT{rt: func(req *http.Request) (*http.Response, error) { res := &http.Response{Request: req} switch req.URL.Path { - case getNodeVersionPath: + case beacon.GetNodeVersionPath: res.StatusCode = http.StatusOK b := bytes.NewBuffer(nil) d := struct { @@ -275,15 +234,15 @@ func TestDownloadBackwardsCompatibleCombined(t *testing.T) { require.NoError(t, err) b.Write(encoded) res.Body = io.NopCloser(b) - case getWeakSubjectivityPath: + case beacon.GetWeakSubjectivityPath: res.StatusCode = http.StatusNotFound - case renderGetStatePath(IdHead): + case beacon.RenderGetStatePath(beacon.IdHead): res.StatusCode = http.StatusOK res.Body = io.NopCloser(bytes.NewBuffer(serialized)) - case renderGetStatePath(IdFromSlot(wSlot)): + case beacon.RenderGetStatePath(beacon.IdFromSlot(wSlot)): res.StatusCode = http.StatusOK res.Body = io.NopCloser(bytes.NewBuffer(wsSerialized)) - case renderGetBlockPath(IdFromRoot(bRoot)): + case beacon.RenderGetBlockPath(beacon.IdFromRoot(bRoot)): res.StatusCode = http.StatusOK res.Body = io.NopCloser(bytes.NewBuffer(serBlock)) } @@ -291,7 +250,7 @@ func TestDownloadBackwardsCompatibleCombined(t *testing.T) { return res, nil }} - c, err := NewClient("http://localhost:3500", client.WithRoundTripper(trans)) + c, err := beacon.NewClient("http://localhost:3500", client.WithRoundTripper(trans)) require.NoError(t, err) wsPub, err := ComputeWeakSubjectivityCheckpoint(ctx, c) @@ -308,13 +267,13 @@ func TestGetWeakSubjectivityEpochFromHead(t *testing.T) { require.NoError(t, err) trans := &testRT{rt: func(req *http.Request) (*http.Response, error) { res := &http.Response{Request: req} - if req.URL.Path == renderGetStatePath(IdHead) { + if req.URL.Path == beacon.RenderGetStatePath(beacon.IdHead) { res.StatusCode = http.StatusOK res.Body = io.NopCloser(bytes.NewBuffer(serialized)) } return res, nil }} - c, err := NewClient("http://localhost:3500", client.WithRoundTripper(trans)) + c, err := beacon.NewClient("http://localhost:3500", client.WithRoundTripper(trans)) require.NoError(t, err) actualEpoch, err := getWeakSubjectivityEpochFromHead(context.Background(), c) require.NoError(t, err) @@ -386,96 +345,3 @@ func populateValidators(cfg *params.BeaconChainConfig, st state.BeaconState, val } return st.SetBalances(balances) } - -func TestDownloadFinalizedData(t *testing.T) { - ctx := context.Background() - cfg := params.MainnetConfig().Copy() - - // avoid the altair zone because genesis tests are easier to set up - epoch := cfg.AltairForkEpoch - 1 - // set up checkpoint state, using the epoch that will be computed as the ws checkpoint state based on the head state - slot, err := slots.EpochStart(epoch) - require.NoError(t, err) - st, err := util.NewBeaconState() - require.NoError(t, err) - fork, err := forkForEpoch(cfg, epoch) - require.NoError(t, err) - require.NoError(t, st.SetFork(fork)) - require.NoError(t, st.SetSlot(slot)) - - // set up checkpoint block - b, err := blocks.NewSignedBeaconBlock(util.NewBeaconBlock()) - require.NoError(t, err) - b, err = blocktest.SetBlockParentRoot(b, cfg.ZeroHash) - require.NoError(t, err) - b, err = blocktest.SetBlockSlot(b, slot) - require.NoError(t, err) - b, err = blocktest.SetProposerIndex(b, 0) - require.NoError(t, err) - - // set up state header pointing at checkpoint block - this is how the block is downloaded by root - header, err := b.Header() - require.NoError(t, err) - require.NoError(t, st.SetLatestBlockHeader(header.Header)) - - // order of operations can be confusing here: - // - when computing the state root, make sure block header is complete, EXCEPT the state root should be zero-value - // - before computing the block root (to match the request route), the block should include the state root - // *computed from the state with a header that does not have a state root set yet* - sr, err := st.HashTreeRoot(ctx) - require.NoError(t, err) - - b, err = blocktest.SetBlockStateRoot(b, sr) - require.NoError(t, err) - mb, err := b.MarshalSSZ() - require.NoError(t, err) - br, err := b.Block().HashTreeRoot() - require.NoError(t, err) - - ms, err := st.MarshalSSZ() - require.NoError(t, err) - - trans := &testRT{rt: func(req *http.Request) (*http.Response, error) { - res := &http.Response{Request: req} - switch req.URL.Path { - case renderGetStatePath(IdFinalized): - res.StatusCode = http.StatusOK - res.Body = io.NopCloser(bytes.NewBuffer(ms)) - case renderGetBlockPath(IdFromSlot(b.Block().Slot())): - res.StatusCode = http.StatusOK - res.Body = io.NopCloser(bytes.NewBuffer(mb)) - default: - res.StatusCode = http.StatusInternalServerError - res.Body = io.NopCloser(bytes.NewBufferString("")) - } - - return res, nil - }} - c, err := NewClient("http://localhost:3500", client.WithRoundTripper(trans)) - require.NoError(t, err) - // sanity check before we go through checkpoint - // make sure we can download the state and unmarshal it with the VersionedUnmarshaler - sb, err := c.GetState(ctx, IdFinalized) - require.NoError(t, err) - require.Equal(t, true, bytes.Equal(sb, ms)) - vu, err := detect.FromState(sb) - require.NoError(t, err) - us, err := vu.UnmarshalBeaconState(sb) - require.NoError(t, err) - ushtr, err := us.HashTreeRoot(ctx) - require.NoError(t, err) - require.Equal(t, sr, ushtr) - - expected := &OriginData{ - sb: ms, - bb: mb, - br: br, - sr: sr, - } - od, err := DownloadFinalizedData(ctx, c) - require.NoError(t, err) - require.Equal(t, true, bytes.Equal(expected.sb, od.sb)) - require.Equal(t, true, bytes.Equal(expected.bb, od.bb)) - require.Equal(t, expected.br, od.br) - require.Equal(t, expected.sr, od.sr) -} diff --git a/changelog/kasey_refactor-checkpoint-download.md b/changelog/kasey_refactor-checkpoint-download.md new file mode 100644 index 0000000000..d13ed46204 --- /dev/null +++ b/changelog/kasey_refactor-checkpoint-download.md @@ -0,0 +1,2 @@ +### Changed +- DownloadFinalizedData has moved from the api/client package to beacon-chain/sync/checkpoint diff --git a/cmd/prysmctl/checkpointsync/BUILD.bazel b/cmd/prysmctl/checkpointsync/BUILD.bazel index eafecf7c46..c220ba3250 100644 --- a/cmd/prysmctl/checkpointsync/BUILD.bazel +++ b/cmd/prysmctl/checkpointsync/BUILD.bazel @@ -11,6 +11,7 @@ go_library( deps = [ "//api/client:go_default_library", "//api/client/beacon:go_default_library", + "//beacon-chain/sync/checkpoint:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", ], diff --git a/cmd/prysmctl/checkpointsync/download.go b/cmd/prysmctl/checkpointsync/download.go index 65954b66af..116743de8e 100644 --- a/cmd/prysmctl/checkpointsync/download.go +++ b/cmd/prysmctl/checkpointsync/download.go @@ -7,6 +7,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/api/client" "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/sync/checkpoint" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -57,7 +58,7 @@ func cliActionDownload(_ *cli.Context) error { return err } - od, err := beacon.DownloadFinalizedData(ctx, client) + od, err := checkpoint.DownloadFinalizedData(ctx, client) if err != nil { return err } diff --git a/cmd/prysmctl/weaksubjectivity/BUILD.bazel b/cmd/prysmctl/weaksubjectivity/BUILD.bazel index d0f9c0be50..f56ae220ff 100644 --- a/cmd/prysmctl/weaksubjectivity/BUILD.bazel +++ b/cmd/prysmctl/weaksubjectivity/BUILD.bazel @@ -11,6 +11,7 @@ go_library( deps = [ "//api/client:go_default_library", "//api/client/beacon:go_default_library", + "//beacon-chain/sync/checkpoint:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", ], diff --git a/cmd/prysmctl/weaksubjectivity/checkpoint.go b/cmd/prysmctl/weaksubjectivity/checkpoint.go index dc35c44323..094a006f38 100644 --- a/cmd/prysmctl/weaksubjectivity/checkpoint.go +++ b/cmd/prysmctl/weaksubjectivity/checkpoint.go @@ -7,6 +7,7 @@ import ( "github.com/prysmaticlabs/prysm/v5/api/client" "github.com/prysmaticlabs/prysm/v5/api/client/beacon" + "github.com/prysmaticlabs/prysm/v5/beacon-chain/sync/checkpoint" log "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -52,7 +53,7 @@ func cliActionCheckpoint(_ *cli.Context) error { return err } - ws, err := beacon.ComputeWeakSubjectivityCheckpoint(ctx, client) + ws, err := checkpoint.ComputeWeakSubjectivityCheckpoint(ctx, client) if err != nil { return err } diff --git a/go.mod b/go.mod index 6b006e90f7..718c3d027f 100644 --- a/go.mod +++ b/go.mod @@ -88,7 +88,6 @@ require ( go.uber.org/mock v0.4.0 golang.org/x/crypto v0.32.0 golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa - golang.org/x/mod v0.22.0 golang.org/x/sync v0.10.0 golang.org/x/tools v0.29.0 google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 @@ -251,6 +250,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/mod v0.22.0 // indirect golang.org/x/net v0.34.0 // indirect golang.org/x/oauth2 v0.21.0 // indirect golang.org/x/term v0.28.0 // indirect diff --git a/network/forks/ordered.go b/network/forks/ordered.go index 6c080ee2bf..13232fd7ee 100644 --- a/network/forks/ordered.go +++ b/network/forks/ordered.go @@ -100,3 +100,26 @@ func NewOrderedSchedule(b *params.BeaconChainConfig) OrderedSchedule { sort.Sort(ofs) return ofs } + +// ForkForEpochFromConfig returns the fork data for the given epoch from the provided config. +func ForkForEpochFromConfig(cfg *params.BeaconChainConfig, epoch primitives.Epoch) (*ethpb.Fork, error) { + os := NewOrderedSchedule(cfg) + currentVersion, err := os.VersionForEpoch(epoch) + if err != nil { + return nil, err + } + prevVersion, err := os.Previous(currentVersion) + if err != nil { + if !errors.Is(err, ErrNoPreviousVersion) { + return nil, err + } + // use same version for both in the case of genesis + prevVersion = currentVersion + } + forkEpoch := cfg.ForkVersionSchedule[currentVersion] + return ðpb.Fork{ + PreviousVersion: prevVersion[:], + CurrentVersion: currentVersion[:], + Epoch: forkEpoch, + }, nil +}