mirror of
https://gitlab.com/pulsechaincom/prysm-pulse.git
synced 2025-01-08 02:31:19 +00:00
b2e3c29ab3
* Improve logging. * Make deepsource happy. * Fix comment.
277 lines
11 KiB
Go
277 lines
11 KiB
Go
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
|
|
}
|