mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-06 22:23:56 -05:00
* Migrate Prysm repo to Offchain Labs organization ahead of Pectra upgrade v6 * Replace prysmaticlabs with OffchainLabs on general markdowns * Update mock * Gazelle and add mock.go to excluded generated mock file
700 lines
22 KiB
Go
700 lines
22 KiB
Go
package builder
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/OffchainLabs/prysm/v6/api"
|
|
"github.com/OffchainLabs/prysm/v6/api/client"
|
|
"github.com/OffchainLabs/prysm/v6/api/server/structs"
|
|
"github.com/OffchainLabs/prysm/v6/config/params"
|
|
"github.com/OffchainLabs/prysm/v6/consensus-types/blocks"
|
|
"github.com/OffchainLabs/prysm/v6/consensus-types/interfaces"
|
|
"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
|
|
"github.com/OffchainLabs/prysm/v6/monitoring/tracing"
|
|
"github.com/OffchainLabs/prysm/v6/monitoring/tracing/trace"
|
|
v1 "github.com/OffchainLabs/prysm/v6/proto/engine/v1"
|
|
ethpb "github.com/OffchainLabs/prysm/v6/proto/prysm/v1alpha1"
|
|
"github.com/OffchainLabs/prysm/v6/runtime/version"
|
|
"github.com/pkg/errors"
|
|
log "github.com/sirupsen/logrus"
|
|
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
|
)
|
|
|
|
const (
|
|
getExecHeaderPath = "/eth/v1/builder/header/{{.Slot}}/{{.ParentHash}}/{{.Pubkey}}"
|
|
getStatus = "/eth/v1/builder/status"
|
|
postBlindedBeaconBlockPath = "/eth/v1/builder/blinded_blocks"
|
|
postRegisterValidatorPath = "/eth/v1/builder/validators"
|
|
)
|
|
|
|
var (
|
|
vrExample = ðpb.SignedValidatorRegistrationV1{}
|
|
vrSize = vrExample.SizeSSZ()
|
|
errMalformedHostname = errors.New("hostname must include port, separated by one colon, like example.com:3500")
|
|
errMalformedRequest = errors.New("required request data are missing")
|
|
errNotBlinded = errors.New("submitted block is not blinded")
|
|
errVersionUnsupported = errors.New("version is not supported")
|
|
)
|
|
|
|
// ClientOpt is a functional option for the Client type (http.Client wrapper)
|
|
type ClientOpt func(*Client)
|
|
|
|
type observer interface {
|
|
observe(r *http.Request) error
|
|
}
|
|
|
|
func WithObserver(m observer) ClientOpt {
|
|
return func(c *Client) {
|
|
c.obvs = append(c.obvs, m)
|
|
}
|
|
}
|
|
|
|
func WithSSZ() ClientOpt {
|
|
return func(c *Client) {
|
|
c.sszEnabled = true
|
|
}
|
|
}
|
|
|
|
type requestLogger struct{}
|
|
|
|
func (*requestLogger) observe(r *http.Request) (e error) {
|
|
b := bytes.NewBuffer(nil)
|
|
if r.Body == nil {
|
|
log.WithFields(log.Fields{
|
|
"bodyBase64": "(nil value)",
|
|
"url": r.URL.String(),
|
|
}).Info("builder http request")
|
|
return nil
|
|
}
|
|
t := io.TeeReader(r.Body, b)
|
|
defer func() {
|
|
if r.Body != nil {
|
|
e = r.Body.Close()
|
|
}
|
|
}()
|
|
body, err := io.ReadAll(t)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Body = io.NopCloser(b)
|
|
log.WithFields(log.Fields{
|
|
"bodyBase64": string(body),
|
|
"url": r.URL.String(),
|
|
}).Info("builder http request")
|
|
|
|
return nil
|
|
}
|
|
|
|
var _ observer = &requestLogger{}
|
|
|
|
// BuilderClient provides a collection of helper methods for calling Builder API endpoints.
|
|
type BuilderClient interface {
|
|
NodeURL() string
|
|
GetHeader(ctx context.Context, slot primitives.Slot, parentHash [32]byte, pubkey [48]byte) (SignedBid, error)
|
|
RegisterValidator(ctx context.Context, svr []*ethpb.SignedValidatorRegistrationV1) error
|
|
SubmitBlindedBlock(ctx context.Context, sb interfaces.ReadOnlySignedBeaconBlock) (interfaces.ExecutionData, *v1.BlobsBundle, error)
|
|
Status(ctx context.Context) error
|
|
}
|
|
|
|
// Client provides a collection of helper methods for calling Builder API endpoints.
|
|
type Client struct {
|
|
hc *http.Client
|
|
baseURL *url.URL
|
|
obvs []observer
|
|
sszEnabled bool
|
|
}
|
|
|
|
// NewClient constructs a new client with the provided options (ex WithTimeout).
|
|
// `host` is the base host + port used to construct request urls. This value can be
|
|
// a URL string, or NewClient will assume an http endpoint if just `host:port` is used.
|
|
func NewClient(host string, opts ...ClientOpt) (*Client, error) {
|
|
u, err := urlForHost(host)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c := &Client{
|
|
hc: &http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)},
|
|
baseURL: u,
|
|
}
|
|
for _, o := range opts {
|
|
o(c)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
func urlForHost(h string) (*url.URL, error) {
|
|
// try to parse as url (being permissive)
|
|
if u, err := url.Parse(h); err == nil && u.Host != "" {
|
|
return u, nil
|
|
}
|
|
// try to parse as host:port
|
|
host, port, err := net.SplitHostPort(h)
|
|
if err != nil {
|
|
return nil, errMalformedHostname
|
|
}
|
|
return &url.URL{Host: net.JoinHostPort(host, port), Scheme: "http"}, nil
|
|
}
|
|
|
|
// NodeURL returns a human-readable string representation of the beacon node base url.
|
|
func (c *Client) NodeURL() string {
|
|
return c.baseURL.String()
|
|
}
|
|
|
|
type reqOption func(*http.Request)
|
|
|
|
// do is a generic, opinionated request function to reduce boilerplate amongst the methods in this package api/client/builder.
|
|
func (c *Client) do(ctx context.Context, method string, path string, body io.Reader, opts ...reqOption) (res []byte, header http.Header, err error) {
|
|
ctx, span := trace.StartSpan(ctx, "builder.client.do")
|
|
defer func() {
|
|
tracing.AnnotateError(span, err)
|
|
span.End()
|
|
}()
|
|
|
|
u := c.baseURL.ResolveReference(&url.URL{Path: path})
|
|
|
|
span.SetAttributes(trace.StringAttribute("url", u.String()),
|
|
trace.StringAttribute("method", method))
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, u.String(), body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
req.Header.Add("User-Agent", version.BuildData())
|
|
for _, o := range opts {
|
|
o(req)
|
|
}
|
|
for _, o := range c.obvs {
|
|
if err = o.observe(req); err != nil {
|
|
return
|
|
}
|
|
}
|
|
r, err := c.hc.Do(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer func() {
|
|
closeErr := r.Body.Close()
|
|
if closeErr != nil {
|
|
log.WithError(closeErr).Error("Failed to close response body")
|
|
}
|
|
}()
|
|
if r.StatusCode != http.StatusOK {
|
|
err = non200Err(r)
|
|
return
|
|
}
|
|
res, err = io.ReadAll(io.LimitReader(r.Body, client.MaxBodySize))
|
|
if err != nil {
|
|
err = errors.Wrap(err, "error reading http response body from builder server")
|
|
return
|
|
}
|
|
header = r.Header
|
|
return
|
|
}
|
|
|
|
var execHeaderTemplate = template.Must(template.New("").Parse(getExecHeaderPath))
|
|
|
|
func execHeaderPath(slot primitives.Slot, parentHash [32]byte, pubkey [48]byte) (string, error) {
|
|
v := struct {
|
|
Slot primitives.Slot
|
|
ParentHash string
|
|
Pubkey string
|
|
}{
|
|
Slot: slot,
|
|
ParentHash: fmt.Sprintf("%#x", parentHash),
|
|
Pubkey: fmt.Sprintf("%#x", pubkey),
|
|
}
|
|
b := bytes.NewBuffer(nil)
|
|
err := execHeaderTemplate.Execute(b, v)
|
|
if err != nil {
|
|
return "", errors.Wrapf(err, "error rendering exec header template with slot=%d, parentHash=%#x, pubkey=%#x", slot, parentHash, pubkey)
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
|
|
// GetHeader is used by a proposing validator to request an execution payload header from the Builder node.
|
|
func (c *Client) GetHeader(ctx context.Context, slot primitives.Slot, parentHash [32]byte, pubkey [48]byte) (SignedBid, error) {
|
|
path, err := execHeaderPath(slot, parentHash, pubkey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var getOpts reqOption
|
|
if c.sszEnabled {
|
|
getOpts = func(r *http.Request) {
|
|
r.Header.Set("Accept", api.OctetStreamMediaType)
|
|
}
|
|
} else {
|
|
getOpts = func(r *http.Request) {
|
|
r.Header.Set("Accept", api.JsonMediaType)
|
|
}
|
|
}
|
|
data, header, err := c.do(ctx, http.MethodGet, path, nil, getOpts)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "error getting header from builder server")
|
|
}
|
|
|
|
bid, err := c.parseHeaderResponse(data, header)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(
|
|
err,
|
|
"error rendering exec header template with slot=%d, parentHash=%#x, pubkey=%#x",
|
|
slot,
|
|
parentHash,
|
|
pubkey,
|
|
)
|
|
}
|
|
return bid, nil
|
|
}
|
|
|
|
func (c *Client) parseHeaderResponse(data []byte, header http.Header) (SignedBid, error) {
|
|
var versionHeader string
|
|
if c.sszEnabled || header.Get(api.VersionHeader) != "" {
|
|
versionHeader = header.Get(api.VersionHeader)
|
|
} else {
|
|
// If we don't have a version header, attempt to parse JSON for version
|
|
v := &VersionResponse{}
|
|
if err := json.Unmarshal(data, v); err != nil {
|
|
return nil, errors.Wrap(
|
|
err,
|
|
"error unmarshaling builder GetHeader response",
|
|
)
|
|
}
|
|
versionHeader = strings.ToLower(v.Version)
|
|
}
|
|
|
|
ver, err := version.FromString(versionHeader)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, fmt.Sprintf("unsupported header version %s", versionHeader))
|
|
}
|
|
|
|
if ver >= version.Electra {
|
|
return c.parseHeaderElectra(data)
|
|
}
|
|
if ver >= version.Deneb {
|
|
return c.parseHeaderDeneb(data)
|
|
}
|
|
if ver >= version.Capella {
|
|
return c.parseHeaderCapella(data)
|
|
}
|
|
if ver >= version.Bellatrix {
|
|
return c.parseHeaderBellatrix(data)
|
|
}
|
|
|
|
return nil, fmt.Errorf("unsupported header version %s", versionHeader)
|
|
}
|
|
|
|
func (c *Client) parseHeaderElectra(data []byte) (SignedBid, error) {
|
|
if c.sszEnabled {
|
|
sb := ðpb.SignedBuilderBidElectra{}
|
|
if err := sb.UnmarshalSSZ(data); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal SignedBuilderBidElectra SSZ")
|
|
}
|
|
return WrappedSignedBuilderBidElectra(sb)
|
|
}
|
|
hr := &ExecHeaderResponseElectra{}
|
|
if err := json.Unmarshal(data, hr); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal ExecHeaderResponseElectra JSON")
|
|
}
|
|
p, err := hr.ToProto()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert ExecHeaderResponseElectra to proto")
|
|
}
|
|
return WrappedSignedBuilderBidElectra(p)
|
|
}
|
|
|
|
func (c *Client) parseHeaderDeneb(data []byte) (SignedBid, error) {
|
|
if c.sszEnabled {
|
|
sb := ðpb.SignedBuilderBidDeneb{}
|
|
if err := sb.UnmarshalSSZ(data); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal SignedBuilderBidDeneb SSZ")
|
|
}
|
|
return WrappedSignedBuilderBidDeneb(sb)
|
|
}
|
|
hr := &ExecHeaderResponseDeneb{}
|
|
if err := json.Unmarshal(data, hr); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal ExecHeaderResponseDeneb JSON")
|
|
}
|
|
p, err := hr.ToProto()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert ExecHeaderResponseDeneb to proto")
|
|
}
|
|
return WrappedSignedBuilderBidDeneb(p)
|
|
}
|
|
|
|
func (c *Client) parseHeaderCapella(data []byte) (SignedBid, error) {
|
|
if c.sszEnabled {
|
|
sb := ðpb.SignedBuilderBidCapella{}
|
|
if err := sb.UnmarshalSSZ(data); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal SignedBuilderBidCapella SSZ")
|
|
}
|
|
return WrappedSignedBuilderBidCapella(sb)
|
|
}
|
|
hr := &ExecHeaderResponseCapella{}
|
|
if err := json.Unmarshal(data, hr); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal ExecHeaderResponseCapella JSON")
|
|
}
|
|
p, err := hr.ToProto()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert ExecHeaderResponseCapella to proto")
|
|
}
|
|
return WrappedSignedBuilderBidCapella(p)
|
|
}
|
|
|
|
func (c *Client) parseHeaderBellatrix(data []byte) (SignedBid, error) {
|
|
if c.sszEnabled {
|
|
sb := ðpb.SignedBuilderBid{}
|
|
if err := sb.UnmarshalSSZ(data); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal SignedBuilderBid SSZ")
|
|
}
|
|
return WrappedSignedBuilderBid(sb)
|
|
}
|
|
hr := &ExecHeaderResponse{}
|
|
if err := json.Unmarshal(data, hr); err != nil {
|
|
return nil, errors.Wrap(err, "could not unmarshal ExecHeaderResponse JSON")
|
|
}
|
|
p, err := hr.ToProto()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "could not convert ExecHeaderResponse to proto")
|
|
}
|
|
return WrappedSignedBuilderBid(p)
|
|
}
|
|
|
|
// RegisterValidator encodes the SignedValidatorRegistrationV1 message to json (including hex-encoding the byte
|
|
// fields with 0x prefixes) and posts to the builder validator registration endpoint.
|
|
func (c *Client) RegisterValidator(ctx context.Context, svr []*ethpb.SignedValidatorRegistrationV1) error {
|
|
ctx, span := trace.StartSpan(ctx, "builder.client.RegisterValidator")
|
|
defer span.End()
|
|
span.SetAttributes(trace.Int64Attribute("num_reqs", int64(len(svr))))
|
|
|
|
if len(svr) == 0 {
|
|
err := errors.Wrap(errMalformedRequest, "empty validator registration list")
|
|
tracing.AnnotateError(span, err)
|
|
return err
|
|
}
|
|
|
|
var (
|
|
body []byte
|
|
err error
|
|
postOpts reqOption
|
|
)
|
|
if c.sszEnabled {
|
|
postOpts = func(r *http.Request) {
|
|
r.Header.Set("Content-Type", api.OctetStreamMediaType)
|
|
r.Header.Set("Accept", api.OctetStreamMediaType)
|
|
}
|
|
body, err = sszValidatorRegisterRequest(svr)
|
|
if err != nil {
|
|
err := errors.Wrap(err, "error ssz encoding the SignedValidatorRegistration value body in RegisterValidator")
|
|
tracing.AnnotateError(span, err)
|
|
return err
|
|
}
|
|
} else {
|
|
postOpts = func(r *http.Request) {
|
|
r.Header.Set("Content-Type", api.JsonMediaType)
|
|
r.Header.Set("Accept", api.JsonMediaType)
|
|
}
|
|
body, err = jsonValidatorRegisterRequest(svr)
|
|
if err != nil {
|
|
err := errors.Wrap(err, "error json encoding the SignedValidatorRegistration value body in RegisterValidator")
|
|
tracing.AnnotateError(span, err)
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, _, err = c.do(ctx, http.MethodPost, postRegisterValidatorPath, bytes.NewBuffer(body), postOpts); err != nil {
|
|
return errors.Wrap(err, "do")
|
|
}
|
|
log.WithField("registrationCount", len(svr)).Debug("Successfully registered validator(s) on builder")
|
|
return nil
|
|
}
|
|
|
|
func jsonValidatorRegisterRequest(svr []*ethpb.SignedValidatorRegistrationV1) ([]byte, error) {
|
|
vs := make([]*structs.SignedValidatorRegistration, len(svr))
|
|
for i := 0; i < len(svr); i++ {
|
|
vs[i] = structs.SignedValidatorRegistrationFromConsensus(svr[i])
|
|
}
|
|
body, err := json.Marshal(vs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return body, nil
|
|
}
|
|
|
|
func sszValidatorRegisterRequest(svr []*ethpb.SignedValidatorRegistrationV1) ([]byte, error) {
|
|
if uint64(len(svr)) > params.BeaconConfig().ValidatorRegistryLimit {
|
|
return nil, errors.Wrap(errMalformedRequest, "validator registry limit exceeded")
|
|
}
|
|
ssz := make([]byte, vrSize*len(svr))
|
|
for i, vr := range svr {
|
|
sszrep, err := vr.MarshalSSZ()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to marshal validator registry ssz")
|
|
}
|
|
copy(ssz[i*vrSize:(i+1)*vrSize], sszrep)
|
|
}
|
|
return ssz, nil
|
|
}
|
|
|
|
var errResponseVersionMismatch = errors.New("builder API response uses a different version than requested in " + api.VersionHeader + " header")
|
|
|
|
func getVersionsBlockToPayload(blockVersion int) (int, error) {
|
|
if blockVersion >= version.Deneb {
|
|
return version.Deneb, nil
|
|
}
|
|
if blockVersion == version.Capella {
|
|
return version.Capella, nil
|
|
}
|
|
if blockVersion == version.Bellatrix {
|
|
return version.Bellatrix, nil
|
|
}
|
|
return 0, errors.Wrapf(errVersionUnsupported, "block version %d", blockVersion)
|
|
}
|
|
|
|
// SubmitBlindedBlock calls the builder API endpoint that binds the validator to the builder and submits the block.
|
|
// The response is the full execution payload used to create the blinded block.
|
|
func (c *Client) SubmitBlindedBlock(ctx context.Context, sb interfaces.ReadOnlySignedBeaconBlock) (interfaces.ExecutionData, *v1.BlobsBundle, error) {
|
|
body, postOpts, err := c.buildBlindedBlockRequest(sb)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// post the blinded block - the execution payload response should contain the unblinded payload, along with the
|
|
// blobs bundle if it is post deneb.
|
|
data, header, err := c.do(ctx, http.MethodPost, postBlindedBeaconBlockPath, bytes.NewBuffer(body), postOpts)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "error posting the blinded block to the builder api")
|
|
}
|
|
|
|
ver, err := c.checkBlockVersion(data, header)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
expectedPayloadVer, err := getVersionsBlockToPayload(sb.Version())
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
gotPayloadVer, err := getVersionsBlockToPayload(ver)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if expectedPayloadVer != gotPayloadVer {
|
|
return nil, nil, errors.Wrapf(errResponseVersionMismatch, "expected payload version %d, got %d", expectedPayloadVer, gotPayloadVer)
|
|
}
|
|
|
|
ed, blobs, err := c.parseBlindedBlockResponse(data, ver)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return ed, blobs, nil
|
|
}
|
|
|
|
func (c *Client) checkBlockVersion(respBytes []byte, header http.Header) (int, error) {
|
|
var versionHeader string
|
|
if c.sszEnabled {
|
|
versionHeader = strings.ToLower(header.Get(api.VersionHeader))
|
|
} else {
|
|
// fallback to JSON-based version extraction
|
|
v := &VersionResponse{}
|
|
if err := json.Unmarshal(respBytes, v); err != nil {
|
|
return 0, errors.Wrapf(err, "error unmarshaling JSON version fallback")
|
|
}
|
|
versionHeader = strings.ToLower(v.Version)
|
|
}
|
|
|
|
ver, err := version.FromString(versionHeader)
|
|
if err != nil {
|
|
return 0, errors.Wrapf(err, "unsupported header version %s", versionHeader)
|
|
}
|
|
|
|
return ver, nil
|
|
}
|
|
|
|
// Helper: build request body for SubmitBlindedBlock
|
|
func (c *Client) buildBlindedBlockRequest(sb interfaces.ReadOnlySignedBeaconBlock) ([]byte, reqOption, error) {
|
|
if !sb.IsBlinded() {
|
|
return nil, nil, errNotBlinded
|
|
}
|
|
|
|
if c.sszEnabled {
|
|
body, err := sb.MarshalSSZ()
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "could not marshal SSZ for blinded block")
|
|
}
|
|
opt := func(r *http.Request) {
|
|
r.Header.Set(api.VersionHeader, version.String(sb.Version()))
|
|
r.Header.Set("Content-Type", api.OctetStreamMediaType)
|
|
r.Header.Set("Accept", api.OctetStreamMediaType)
|
|
}
|
|
return body, opt, nil
|
|
}
|
|
|
|
mj, err := structs.SignedBeaconBlockMessageJsoner(sb)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "error generating blinded beacon block post request")
|
|
}
|
|
body, err := json.Marshal(mj)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "error marshaling blinded block to JSON")
|
|
}
|
|
opt := func(r *http.Request) {
|
|
r.Header.Set(api.VersionHeader, version.String(sb.Version()))
|
|
r.Header.Set("Content-Type", api.JsonMediaType)
|
|
r.Header.Set("Accept", api.JsonMediaType)
|
|
}
|
|
return body, opt, nil
|
|
}
|
|
|
|
// Helper: parse the response returned by SubmitBlindedBlock
|
|
func (c *Client) parseBlindedBlockResponse(
|
|
respBytes []byte,
|
|
forkVersion int,
|
|
) (interfaces.ExecutionData, *v1.BlobsBundle, error) {
|
|
if c.sszEnabled {
|
|
return c.parseBlindedBlockResponseSSZ(respBytes, forkVersion)
|
|
}
|
|
return c.parseBlindedBlockResponseJSON(respBytes, forkVersion)
|
|
}
|
|
|
|
func (c *Client) parseBlindedBlockResponseSSZ(
|
|
respBytes []byte,
|
|
forkVersion int,
|
|
) (interfaces.ExecutionData, *v1.BlobsBundle, error) {
|
|
if forkVersion >= version.Deneb {
|
|
payloadAndBlobs := &v1.ExecutionPayloadDenebAndBlobsBundle{}
|
|
if err := payloadAndBlobs.UnmarshalSSZ(respBytes); err != nil {
|
|
return nil, nil, errors.Wrap(err, "unable to unmarshal ExecutionPayloadDenebAndBlobsBundle SSZ")
|
|
}
|
|
ed, err := blocks.NewWrappedExecutionData(payloadAndBlobs.Payload)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "unable to wrap execution data for %s", version.String(forkVersion))
|
|
}
|
|
return ed, payloadAndBlobs.BlobsBundle, nil
|
|
} else if forkVersion >= version.Capella {
|
|
payload := &v1.ExecutionPayloadCapella{}
|
|
if err := payload.UnmarshalSSZ(respBytes); err != nil {
|
|
return nil, nil, errors.Wrap(err, "unable to unmarshal ExecutionPayloadCapella SSZ")
|
|
}
|
|
ed, err := blocks.NewWrappedExecutionData(payload)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "unable to wrap execution data for %s", version.String(forkVersion))
|
|
}
|
|
return ed, nil, nil
|
|
} else if forkVersion >= version.Bellatrix {
|
|
payload := &v1.ExecutionPayload{}
|
|
if err := payload.UnmarshalSSZ(respBytes); err != nil {
|
|
return nil, nil, errors.Wrap(err, "unable to unmarshal ExecutionPayload SSZ")
|
|
}
|
|
ed, err := blocks.NewWrappedExecutionData(payload)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "unable to wrap execution data for %s", version.String(forkVersion))
|
|
}
|
|
return ed, nil, nil
|
|
} else {
|
|
return nil, nil, fmt.Errorf("unsupported header version %s", version.String(forkVersion))
|
|
}
|
|
}
|
|
|
|
func (c *Client) parseBlindedBlockResponseJSON(
|
|
respBytes []byte,
|
|
forkVersion int,
|
|
) (interfaces.ExecutionData, *v1.BlobsBundle, error) {
|
|
ep := &ExecutionPayloadResponse{}
|
|
if err := json.Unmarshal(respBytes, ep); err != nil {
|
|
return nil, nil, errors.Wrap(err, "error unmarshaling ExecutionPayloadResponse")
|
|
}
|
|
pp, err := ep.ParsePayload()
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "failed to parse payload with version=%s", ep.Version)
|
|
}
|
|
pb, err := pp.PayloadProto()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
ed, err := blocks.NewWrappedExecutionData(pb)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Check if it contains blobs
|
|
bb, ok := pp.(BlobBundler)
|
|
if ok {
|
|
bbpb, err := bb.BundleProto()
|
|
if err != nil {
|
|
return nil, nil, errors.Wrapf(err, "failed to extract blobs bundle from version=%s", ep.Version)
|
|
}
|
|
return ed, bbpb, nil
|
|
}
|
|
return ed, nil, nil
|
|
}
|
|
|
|
// Status asks the remote builder server for a health check. A response of 200 with an empty body is the success/healthy
|
|
// response, and an error response may have an error message. This method will return a nil value for error in the
|
|
// happy path, and an error with information about the server response body for a non-200 response.
|
|
func (c *Client) Status(ctx context.Context) error {
|
|
getOpts := func(r *http.Request) {
|
|
r.Header.Set("Accept", api.JsonMediaType)
|
|
}
|
|
_, _, err := c.do(ctx, http.MethodGet, getStatus, nil, getOpts)
|
|
return err
|
|
}
|
|
|
|
func non200Err(response *http.Response) error {
|
|
bodyBytes, err := io.ReadAll(io.LimitReader(response.Body, client.MaxErrBodySize))
|
|
var errMessage ErrorMessage
|
|
var body string
|
|
if err != nil {
|
|
body = "(Unable to read response body.)"
|
|
} else {
|
|
body = "response body:\n" + string(bodyBytes)
|
|
}
|
|
msg := fmt.Sprintf("code=%d, url=%s, body=%s", response.StatusCode, response.Request.URL, body)
|
|
switch response.StatusCode {
|
|
case http.StatusUnsupportedMediaType:
|
|
log.WithError(ErrUnsupportedMediaType).Debug(msg)
|
|
if jsonErr := json.Unmarshal(bodyBytes, &errMessage); jsonErr != nil {
|
|
return errors.Wrap(jsonErr, "unable to read response body")
|
|
}
|
|
return errors.Wrap(ErrUnsupportedMediaType, errMessage.Message)
|
|
case http.StatusNotAcceptable:
|
|
log.WithError(ErrNotAcceptable).Debug(msg)
|
|
if jsonErr := json.Unmarshal(bodyBytes, &errMessage); jsonErr != nil {
|
|
return errors.Wrap(jsonErr, "unable to read response body")
|
|
}
|
|
return errors.Wrap(ErrNotAcceptable, errMessage.Message)
|
|
case http.StatusNoContent:
|
|
log.WithError(ErrNoContent).Debug(msg)
|
|
return ErrNoContent
|
|
case http.StatusBadRequest:
|
|
log.WithError(ErrBadRequest).Debug(msg)
|
|
if jsonErr := json.Unmarshal(bodyBytes, &errMessage); jsonErr != nil {
|
|
return errors.Wrap(jsonErr, "unable to read response body")
|
|
}
|
|
return errors.Wrap(ErrBadRequest, errMessage.Message)
|
|
case http.StatusNotFound:
|
|
log.WithError(ErrNotFound).Debug(msg)
|
|
if jsonErr := json.Unmarshal(bodyBytes, &errMessage); jsonErr != nil {
|
|
return errors.Wrap(jsonErr, "unable to read response body")
|
|
}
|
|
return errors.Wrap(ErrNotFound, errMessage.Message)
|
|
case http.StatusInternalServerError:
|
|
log.WithError(ErrNotOK).Debug(msg)
|
|
if jsonErr := json.Unmarshal(bodyBytes, &errMessage); jsonErr != nil {
|
|
return errors.Wrap(jsonErr, "unable to read response body")
|
|
}
|
|
return errors.Wrap(ErrNotOK, errMessage.Message)
|
|
default:
|
|
log.WithError(ErrNotOK).Debug(msg)
|
|
return errors.Wrap(ErrNotOK, fmt.Sprintf("unsupported error code: %d", response.StatusCode))
|
|
}
|
|
}
|