mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-02-13 22:44:57 -05:00
<!-- Thanks for sending a PR! Before submitting: 1. If this is your first PR, check out our contribution guide here https://docs.prylabs.network/docs/contribute/contribution-guidelines You will then need to sign our Contributor License Agreement (CLA), which will show up as a comment from a bot in this pull request after you open it. We cannot review code without a signed CLA. 2. Please file an associated tracking issue if this pull request is non-trivial and requires context for our team to understand. All features and most bug fixes should have an associated issue with a design discussed and decided upon. Small bug fixes and documentation improvements don't need issues. 3. New features and bug fixes must have tests. Documentation may need to be updated. If you're unsure what to update, send the PR, and we'll discuss in review. 4. Note that PRs updating dependencies and new Go versions are not accepted. Please file an issue instead. 5. A changelog entry is required for user facing issues. --> **What type of PR is this?** Feature **What does this PR do? Why is it needed?** - upgrading go ethereum to 1.16.7 - enabling fulu e2e - added new e2e field params and build option - removes github.com/MariusVanDerWijden/FuzzyVM v0.0.0-20240516070431-7828990cad7d and github.com/MariusVanDerWijden/tx-fuzz v1.4.0 what changed - e2e config on slots per epoch increased to match minimum 6->8 a 33% increase in run time ( this is needed because field params only have minimum presets and proposer look ahead feature uses it, so if it doesn't match it fails) - reduce presubmit epochs from 18 -> 10 and only run for electra -> fulu - moves bellatrix -> fulu post merge test to post submit **Which issues(s) does this PR fix?** Fixes # **Other notes for review** **Acknowledgements** - [x] I have read [CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md). - [x] I have included a uniquely named [changelog fragment file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd). - [x] I have added a description to this PR with sufficient context for reviewers to understand this PR. --------- Co-authored-by: Preston Van Loon <preston@pvl.dev> Co-authored-by: Manu NALEPA <enalepa@offchainlabs.com> Co-authored-by: Preston Van Loon <pvanloon@offchainlabs.com> Co-authored-by: Radosław Kapka <rkapka@wp.pl> Co-authored-by: Potuz <potuz@prysmaticlabs.com>
1025 lines
32 KiB
Go
1025 lines
32 KiB
Go
package builder
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
builderAPI "github.com/OffchainLabs/prysm/v7/api/client/builder"
|
|
"github.com/OffchainLabs/prysm/v7/api/server/structs"
|
|
"github.com/OffchainLabs/prysm/v7/beacon-chain/core/signing"
|
|
fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
|
|
"github.com/OffchainLabs/prysm/v7/config/params"
|
|
"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
|
|
"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
|
|
types "github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
|
|
"github.com/OffchainLabs/prysm/v7/crypto/bls"
|
|
"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
|
|
"github.com/OffchainLabs/prysm/v7/network"
|
|
"github.com/OffchainLabs/prysm/v7/network/authorization"
|
|
v1 "github.com/OffchainLabs/prysm/v7/proto/engine/v1"
|
|
eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
|
|
"github.com/OffchainLabs/prysm/v7/runtime/version"
|
|
"github.com/ethereum/go-ethereum/beacon/engine"
|
|
"github.com/ethereum/go-ethereum/common"
|
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
|
gethTypes "github.com/ethereum/go-ethereum/core/types"
|
|
gethRPC "github.com/ethereum/go-ethereum/rpc"
|
|
"github.com/ethereum/go-ethereum/trie"
|
|
"github.com/pkg/errors"
|
|
"github.com/sirupsen/logrus"
|
|
)
|
|
|
|
const (
|
|
statusPath = "GET /eth/v1/builder/status"
|
|
registerPath = "POST /eth/v1/builder/validators"
|
|
headerPath = "GET /eth/v1/builder/header/{slot}/{parent_hash}/{pubkey}"
|
|
blindedPath = "POST /eth/v1/builder/blinded_blocks"
|
|
|
|
// ForkchoiceUpdatedMethod v1 request string for JSON-RPC.
|
|
ForkchoiceUpdatedMethod = "engine_forkchoiceUpdatedV1"
|
|
// ForkchoiceUpdatedMethodV2 v2 request string for JSON-RPC.
|
|
ForkchoiceUpdatedMethodV2 = "engine_forkchoiceUpdatedV2"
|
|
// ForkchoiceUpdatedMethodV3 v3 request string for JSON-RPC.
|
|
ForkchoiceUpdatedMethodV3 = "engine_forkchoiceUpdatedV3"
|
|
// GetPayloadMethod v1 request string for JSON-RPC.
|
|
GetPayloadMethod = "engine_getPayloadV1"
|
|
// GetPayloadMethodV2 v2 request string for JSON-RPC.
|
|
GetPayloadMethodV2 = "engine_getPayloadV2"
|
|
// GetPayloadMethodV3 v3 request string for JSON-RPC.
|
|
GetPayloadMethodV3 = "engine_getPayloadV3"
|
|
// GetPayloadMethodV4 v4 request string for JSON-RPC.
|
|
GetPayloadMethodV4 = "engine_getPayloadV4"
|
|
)
|
|
|
|
var (
|
|
defaultBuilderHost = "127.0.0.1"
|
|
defaultBuilderPort = 8551
|
|
)
|
|
|
|
type jsonRPCObject struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params []any `json:"params"`
|
|
ID uint64 `json:"id"`
|
|
Result any `json:"result"`
|
|
}
|
|
|
|
type ForkchoiceUpdatedResponse struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
Method string `json:"method"`
|
|
Params []any `json:"params"`
|
|
ID uint64 `json:"id"`
|
|
Result struct {
|
|
Status *v1.PayloadStatus `json:"payloadStatus"`
|
|
PayloadId *v1.PayloadIDBytes `json:"payloadId"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type ExecPayloadResponse struct {
|
|
Version string `json:"version"`
|
|
Data *v1.ExecutionPayload `json:"data"`
|
|
}
|
|
type Builder struct {
|
|
cfg *config
|
|
address string
|
|
execClient *gethRPC.Client
|
|
currId *v1.PayloadIDBytes
|
|
prevBeaconRoot []byte
|
|
currVersion int
|
|
currPayload interfaces.ExecutionData
|
|
blobBundle *v1.BlobsBundle
|
|
mux *http.ServeMux
|
|
validatorMap map[string]*eth.ValidatorRegistrationV1
|
|
valLock sync.RWMutex
|
|
srv *http.Server
|
|
}
|
|
|
|
// New creates a proxy server forwarding requests from a consensus client to an execution client.
|
|
func New(opts ...Option) (*Builder, error) {
|
|
p := &Builder{
|
|
cfg: &config{
|
|
builderPort: defaultBuilderPort,
|
|
builderHost: defaultBuilderHost,
|
|
logger: logrus.New(),
|
|
},
|
|
}
|
|
for _, o := range opts {
|
|
if err := o(p); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if p.cfg.destinationUrl == nil {
|
|
return nil, errors.New("must provide a destination address for request proxying")
|
|
}
|
|
endpoint := network.HttpEndpoint(p.cfg.destinationUrl.String())
|
|
endpoint.Auth.Method = authorization.Bearer
|
|
endpoint.Auth.Value = p.cfg.secret
|
|
execClient, err := network.NewExecutionRPCClient(context.Background(), endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
router := http.NewServeMux()
|
|
router.Handle("/", p)
|
|
router.HandleFunc(statusPath, func(writer http.ResponseWriter, request *http.Request) {
|
|
writer.WriteHeader(http.StatusOK)
|
|
})
|
|
router.HandleFunc(registerPath, p.registerValidators)
|
|
router.HandleFunc(headerPath, p.handleHeaderRequest)
|
|
router.HandleFunc(blindedPath, p.handleBlindedBlock)
|
|
addr := net.JoinHostPort(p.cfg.builderHost, strconv.Itoa(p.cfg.builderPort))
|
|
srv := &http.Server{
|
|
Handler: router,
|
|
Addr: addr,
|
|
ReadHeaderTimeout: time.Second,
|
|
}
|
|
p.address = addr
|
|
p.srv = srv
|
|
p.execClient = execClient
|
|
p.valLock.Lock()
|
|
p.validatorMap = map[string]*eth.ValidatorRegistrationV1{}
|
|
p.valLock.Unlock()
|
|
p.mux = router
|
|
return p, nil
|
|
}
|
|
|
|
// Address for the proxy server.
|
|
func (p *Builder) Address() string {
|
|
return p.address
|
|
}
|
|
|
|
// Start a proxy server.
|
|
func (p *Builder) Start(ctx context.Context) error {
|
|
p.srv.BaseContext = func(listener net.Listener) context.Context {
|
|
return ctx
|
|
}
|
|
p.cfg.logger.WithFields(logrus.Fields{
|
|
"executionAddress": p.cfg.destinationUrl.String(),
|
|
}).Infof("Builder now listening on address %s", p.address)
|
|
go func() {
|
|
if err := p.srv.ListenAndServe(); err != nil {
|
|
p.cfg.logger.Error(err)
|
|
}
|
|
}()
|
|
for {
|
|
<-ctx.Done()
|
|
return p.srv.Shutdown(context.Background())
|
|
}
|
|
}
|
|
|
|
// ServeHTTP requests from a consensus client to an execution client, modifying in-flight requests
|
|
// and/or responses as desired. It also processes any backed-up requests.
|
|
func (p *Builder) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
p.cfg.logger.Infof("Received %s request from beacon with url: %s", r.Method, r.URL.Path)
|
|
if p.isBuilderCall(r) {
|
|
p.mux.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
requestBytes, err := parseRequestBytes(r)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not parse request")
|
|
return
|
|
}
|
|
execRes, err := p.sendHttpRequest(r, requestBytes)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not forward request")
|
|
return
|
|
}
|
|
p.cfg.logger.Infof("Received response for %s request with method %s from %s", r.Method, r.Method, p.cfg.destinationUrl.String())
|
|
|
|
defer func() {
|
|
if err = execRes.Body.Close(); err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not do close proxy responseGen body")
|
|
}
|
|
}()
|
|
|
|
buf := bytes.NewBuffer([]byte{})
|
|
if _, err = io.Copy(buf, execRes.Body); err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not copy proxy request body")
|
|
return
|
|
}
|
|
byteResp := bytesutil.SafeCopyBytes(buf.Bytes())
|
|
p.handleEngineCalls(requestBytes, byteResp)
|
|
// Pipe the proxy responseGen to the original caller.
|
|
if _, err = io.Copy(w, buf); err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not copy proxy request body")
|
|
return
|
|
}
|
|
}
|
|
|
|
func (p *Builder) handleEngineCalls(req, resp []byte) {
|
|
if !isEngineAPICall(req) {
|
|
return
|
|
}
|
|
rpcObj, err := unmarshalRPCObject(req)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not unmarshal rpc object")
|
|
return
|
|
}
|
|
p.cfg.logger.Infof("Received engine call %s", rpcObj.Method)
|
|
switch rpcObj.Method {
|
|
case ForkchoiceUpdatedMethod, ForkchoiceUpdatedMethodV2, ForkchoiceUpdatedMethodV3:
|
|
result := &ForkchoiceUpdatedResponse{}
|
|
err = json.Unmarshal(resp, result)
|
|
if err != nil {
|
|
p.cfg.logger.Errorf("Could not unmarshal fcu: %v", err)
|
|
return
|
|
}
|
|
if result.Result.PayloadId != nil && *result.Result.PayloadId != [8]byte{} {
|
|
p.currId = result.Result.PayloadId
|
|
}
|
|
if rpcObj.Method == ForkchoiceUpdatedMethodV3 {
|
|
attr := &v1.PayloadAttributesV3{}
|
|
obj, err := json.Marshal(rpcObj.Params[1])
|
|
if err != nil {
|
|
p.cfg.logger.Errorf("Could not marshal attr: %v", err)
|
|
return
|
|
}
|
|
if err := json.Unmarshal(obj, attr); err != nil {
|
|
p.cfg.logger.Errorf("Could not unmarshal attr: %v", err)
|
|
return
|
|
}
|
|
p.prevBeaconRoot = attr.ParentBeaconBlockRoot
|
|
}
|
|
payloadID := [8]byte{}
|
|
status := ""
|
|
var lastValHash []byte
|
|
if result.Result.PayloadId != nil {
|
|
payloadID = *result.Result.PayloadId
|
|
}
|
|
if result.Result.Status != nil {
|
|
status = result.Result.Status.Status.String()
|
|
lastValHash = result.Result.Status.LatestValidHash
|
|
}
|
|
p.cfg.logger.Infof("Received payload id of %#x and status of %s along with a valid hash of %#x", payloadID, status, lastValHash)
|
|
}
|
|
}
|
|
|
|
func (*Builder) isBuilderCall(req *http.Request) bool {
|
|
return strings.Contains(req.URL.Path, "/eth/v1/builder/")
|
|
}
|
|
|
|
func (p *Builder) registerValidators(w http.ResponseWriter, req *http.Request) {
|
|
var registrations []structs.SignedValidatorRegistration
|
|
if err := json.NewDecoder(req.Body).Decode(®istrations); err != nil {
|
|
http.Error(w, "invalid request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
for _, r := range registrations {
|
|
msg, err := r.Message.ToConsensus()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
p.valLock.Lock()
|
|
p.validatorMap[r.Message.Pubkey] = msg
|
|
p.valLock.Unlock()
|
|
}
|
|
// TODO: Verify Signatures from validators
|
|
w.WriteHeader(http.StatusOK)
|
|
}
|
|
|
|
func (p *Builder) handleHeaderRequest(w http.ResponseWriter, req *http.Request) {
|
|
pHash := req.PathValue("parent_hash")
|
|
if pHash == "" {
|
|
http.Error(w, "no valid parent hash", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, err := bytesutil.DecodeHexWithLength(pHash, common.HashLength)
|
|
if err != nil {
|
|
http.Error(w, "invalid parent hash", http.StatusBadRequest)
|
|
return
|
|
}
|
|
reqSlot := req.PathValue("slot")
|
|
if reqSlot == "" {
|
|
http.Error(w, "no valid slot provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
slot, err := strconv.Atoi(reqSlot)
|
|
if err != nil {
|
|
http.Error(w, "invalid slot provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
reqPubkey := req.PathValue("pubkey")
|
|
if reqPubkey == "" {
|
|
http.Error(w, "no valid pubkey provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
_, err = bytesutil.DecodeHexWithLength(reqPubkey, fieldparams.BLSPubkeyLength)
|
|
if err != nil {
|
|
http.Error(w, "invalid pubkey", http.StatusBadRequest)
|
|
return
|
|
}
|
|
ax := types.Slot(slot)
|
|
currEpoch := types.Epoch(ax / params.BeaconConfig().SlotsPerEpoch)
|
|
if currEpoch >= params.BeaconConfig().ElectraForkEpoch {
|
|
p.handleHeaderRequestElectra(w)
|
|
return
|
|
}
|
|
|
|
if currEpoch >= params.BeaconConfig().DenebForkEpoch {
|
|
p.handleHeaderRequestDeneb(w)
|
|
return
|
|
}
|
|
|
|
if currEpoch >= params.BeaconConfig().CapellaForkEpoch {
|
|
p.handleHeaderRequestCapella(w)
|
|
return
|
|
}
|
|
|
|
b, err := p.retrievePendingBlock()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve pending block")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
secKey, err := bls.RandKey()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve secret key")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
wObj, err := blocks.WrappedExecutionPayload(b)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not wrap execution payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
hdr, err := blocks.PayloadToHeader(wObj)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not make payload into header")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
gEth := big.NewInt(int64(params.BeaconConfig().GweiPerEth))
|
|
weiEth := gEth.Mul(gEth, gEth)
|
|
val := builderAPI.Uint256{Int: weiEth}
|
|
|
|
wrappedHdr, err := structs.ExecutionPayloadHeaderFromConsensus(hdr)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not convert wrapped header")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
bid := &builderAPI.BuilderBid{
|
|
Header: wrappedHdr,
|
|
Value: val,
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
}
|
|
sszBid := ð.BuilderBid{
|
|
Header: hdr,
|
|
Value: val.SSZBytes(),
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
}
|
|
d, err := signing.ComputeDomain(params.BeaconConfig().DomainApplicationBuilder,
|
|
nil, /* fork version */
|
|
nil /* genesis val root */)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the domain")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rt, err := signing.ComputeSigningRoot(sszBid, d)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the signing root")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
sig := secKey.Sign(rt[:])
|
|
hdrResp := &builderAPI.ExecHeaderResponse{
|
|
Version: "bellatrix",
|
|
Data: struct {
|
|
Signature hexutil.Bytes `json:"signature"`
|
|
Message *builderAPI.BuilderBid `json:"message"`
|
|
}{
|
|
Signature: sig.Marshal(),
|
|
Message: bid,
|
|
},
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
err = json.NewEncoder(w).Encode(hdrResp)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not encode response")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
p.currVersion = version.Bellatrix
|
|
p.currPayload = wObj
|
|
}
|
|
|
|
func (p *Builder) handleHeaderRequestCapella(w http.ResponseWriter) {
|
|
b, err := p.retrievePendingBlockCapella()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve pending block")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
secKey, err := bls.RandKey()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve secret key")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
v := big.NewInt(0).SetBytes(bytesutil.ReverseByteOrder(b.Value))
|
|
// we set the payload value as twice its actual one so that it always chooses builder payloads vs local payloads
|
|
v = v.Mul(v, big.NewInt(2))
|
|
wObj, err := blocks.WrappedExecutionPayloadCapella(b.Payload)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not wrap execution payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
hdr, err := blocks.PayloadToHeaderCapella(wObj)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not make payload into header")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
val := builderAPI.Uint256{Int: v}
|
|
wrappedHdr, err := structs.ExecutionPayloadHeaderCapellaFromConsensus(hdr)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not make execution payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
bid := &builderAPI.BuilderBidCapella{
|
|
Header: wrappedHdr,
|
|
Value: val,
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
}
|
|
sszBid := ð.BuilderBidCapella{
|
|
Header: hdr,
|
|
Value: val.SSZBytes(),
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
}
|
|
d, err := signing.ComputeDomain(params.BeaconConfig().DomainApplicationBuilder,
|
|
nil, /* fork version */
|
|
nil /* genesis val root */)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the domain")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rt, err := signing.ComputeSigningRoot(sszBid, d)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the signing root")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
sig := secKey.Sign(rt[:])
|
|
hdrResp := &builderAPI.ExecHeaderResponseCapella{
|
|
Version: "capella",
|
|
Data: struct {
|
|
Signature hexutil.Bytes `json:"signature"`
|
|
Message *builderAPI.BuilderBidCapella `json:"message"`
|
|
}{
|
|
Signature: sig.Marshal(),
|
|
Message: bid,
|
|
},
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
err = json.NewEncoder(w).Encode(hdrResp)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not encode response")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
p.currVersion = version.Capella
|
|
p.currPayload = wObj
|
|
}
|
|
|
|
func (p *Builder) handleHeaderRequestDeneb(w http.ResponseWriter) {
|
|
b, err := p.retrievePendingBlockDeneb()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve pending block")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
secKey, err := bls.RandKey()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve secret key")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
v := big.NewInt(0).SetBytes(bytesutil.ReverseByteOrder(b.Value))
|
|
// we set the payload value as twice its actual one so that it always chooses builder payloads vs local payloads
|
|
v = v.Mul(v, big.NewInt(2))
|
|
wObj, err := blocks.WrappedExecutionPayloadDeneb(b.Payload)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not wrap execution payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
hdr, err := blocks.PayloadToHeaderDeneb(wObj)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not make payload into header")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
val := builderAPI.Uint256{Int: v}
|
|
var commitments []hexutil.Bytes
|
|
for _, c := range b.BlobsBundle.KzgCommitments {
|
|
copiedC := c
|
|
commitments = append(commitments, copiedC)
|
|
}
|
|
wrappedHdr, err := structs.ExecutionPayloadHeaderDenebFromConsensus(hdr)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not make execution payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
bid := &builderAPI.BuilderBidDeneb{
|
|
Header: wrappedHdr,
|
|
BlobKzgCommitments: commitments,
|
|
Value: val,
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
}
|
|
sszBid := ð.BuilderBidDeneb{
|
|
Header: hdr,
|
|
BlobKzgCommitments: b.BlobsBundle.KzgCommitments,
|
|
Value: val.SSZBytes(),
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
}
|
|
d, err := signing.ComputeDomain(params.BeaconConfig().DomainApplicationBuilder,
|
|
nil, /* fork version */
|
|
nil /* genesis val root */)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the domain")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rt, err := signing.ComputeSigningRoot(sszBid, d)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the signing root")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
sig := secKey.Sign(rt[:])
|
|
hdrResp := &builderAPI.ExecHeaderResponseDeneb{
|
|
Version: "deneb",
|
|
Data: struct {
|
|
Signature hexutil.Bytes `json:"signature"`
|
|
Message *builderAPI.BuilderBidDeneb `json:"message"`
|
|
}{
|
|
Signature: sig.Marshal(),
|
|
Message: bid,
|
|
},
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
err = json.NewEncoder(w).Encode(hdrResp)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not encode response")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
p.currVersion = version.Deneb
|
|
p.currPayload = wObj
|
|
p.blobBundle = b.BlobsBundle
|
|
}
|
|
|
|
func (p *Builder) handleHeaderRequestElectra(w http.ResponseWriter) {
|
|
b, err := p.retrievePendingBlockElectra()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve pending block")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
secKey, err := bls.RandKey()
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not retrieve secret key")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
v := big.NewInt(0).SetBytes(bytesutil.ReverseByteOrder(b.Value))
|
|
// we set the payload value as twice its actual one so that it always chooses builder payloads vs local payloads
|
|
v = v.Mul(v, big.NewInt(2))
|
|
wObj, err := blocks.WrappedExecutionPayloadDeneb(b.Payload)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not wrap execution payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
hdr, err := blocks.PayloadToHeaderElectra(wObj)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not make payload into header")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
val := builderAPI.Uint256{Int: v}
|
|
var commitments []hexutil.Bytes
|
|
for _, c := range b.BlobsBundle.KzgCommitments {
|
|
copiedC := c
|
|
commitments = append(commitments, copiedC)
|
|
}
|
|
wrappedHdr, err := structs.ExecutionPayloadHeaderDenebFromConsensus(hdr)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not make execution payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
requests, err := b.GetDecodedExecutionRequests(params.BeaconConfig().ExecutionRequestLimits())
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not get decoded execution requests")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rv1 := structs.ExecutionRequestsFromConsensus(requests)
|
|
|
|
bid := &builderAPI.BuilderBidElectra{
|
|
Header: wrappedHdr,
|
|
BlobKzgCommitments: commitments,
|
|
Value: val,
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
ExecutionRequests: rv1,
|
|
}
|
|
|
|
sszBid := ð.BuilderBidElectra{
|
|
Header: hdr,
|
|
BlobKzgCommitments: b.BlobsBundle.KzgCommitments,
|
|
Value: val.SSZBytes(),
|
|
Pubkey: secKey.PublicKey().Marshal(),
|
|
ExecutionRequests: requests,
|
|
}
|
|
d, err := signing.ComputeDomain(params.BeaconConfig().DomainApplicationBuilder,
|
|
nil, /* fork version */
|
|
nil /* genesis val root */)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the domain")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
rt, err := signing.ComputeSigningRoot(sszBid, d)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not compute the signing root")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
sig := secKey.Sign(rt[:])
|
|
hdrResp := &builderAPI.ExecHeaderResponseElectra{
|
|
Version: "electra",
|
|
Data: struct {
|
|
Signature hexutil.Bytes `json:"signature"`
|
|
Message *builderAPI.BuilderBidElectra `json:"message"`
|
|
}{
|
|
Signature: sig.Marshal(),
|
|
Message: bid,
|
|
},
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
err = json.NewEncoder(w).Encode(hdrResp)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not encode response")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
p.currVersion = version.Electra
|
|
p.currPayload = wObj
|
|
p.blobBundle = b.BlobsBundle
|
|
}
|
|
|
|
func (p *Builder) handleBlindedBlock(w http.ResponseWriter, req *http.Request) {
|
|
// Decode blinded block based on the current fork version.
|
|
// The beacon node sends JSON using api/server/structs types.
|
|
var err error
|
|
switch p.currVersion {
|
|
case version.Electra:
|
|
var sb structs.SignedBlindedBeaconBlockElectra
|
|
err = json.NewDecoder(req.Body).Decode(&sb)
|
|
case version.Deneb:
|
|
var sb structs.SignedBlindedBeaconBlockDeneb
|
|
err = json.NewDecoder(req.Body).Decode(&sb)
|
|
case version.Capella:
|
|
var sb structs.SignedBlindedBeaconBlockCapella
|
|
err = json.NewDecoder(req.Body).Decode(&sb)
|
|
default:
|
|
var sb structs.SignedBlindedBeaconBlockBellatrix
|
|
err = json.NewDecoder(req.Body).Decode(&sb)
|
|
}
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).WithField("version", version.String(p.currVersion)).Error("Could not decode blinded block")
|
|
}
|
|
if p.currPayload == nil {
|
|
p.cfg.logger.Error("No payload is cached")
|
|
http.Error(w, "payload not found", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
resp, err := ExecutionPayloadResponseFromData(p.currVersion, p.currPayload, p.blobBundle)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not convert the payload")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
err = json.NewEncoder(w).Encode(resp)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not encode full payload response")
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
var errInvalidTypeConversion = errors.New("unable to translate between api and foreign type")
|
|
|
|
// ExecutionPayloadResponseFromData converts an ExecutionData interface value to a payload response.
|
|
// This involves serializing the execution payload value so that the abstract payload envelope can be used.
|
|
func ExecutionPayloadResponseFromData(v int, ed interfaces.ExecutionData, bundle *v1.BlobsBundle) (*builderAPI.ExecutionPayloadResponse, error) {
|
|
pb := ed.Proto()
|
|
var data any
|
|
var err error
|
|
ver := version.String(v)
|
|
switch pbStruct := pb.(type) {
|
|
case *v1.ExecutionPayloadDeneb:
|
|
payloadStruct, err := structs.ExecutionPayloadDenebFromConsensus(pbStruct)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to convert a Deneb ExecutionPayload to an API response")
|
|
}
|
|
data = &builderAPI.ExecutionPayloadDenebAndBlobsBundle{
|
|
ExecutionPayload: payloadStruct,
|
|
BlobsBundle: builderAPI.FromBundleProto(bundle),
|
|
}
|
|
case *v1.ExecutionPayloadCapella:
|
|
data, err = structs.ExecutionPayloadCapellaFromConsensus(pbStruct)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to convert a Capella ExecutionPayload to an API response")
|
|
}
|
|
case *v1.ExecutionPayload:
|
|
data, err = structs.ExecutionPayloadFromConsensus(pbStruct)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to convert a Bellatrix ExecutionPayload to an API response")
|
|
}
|
|
default:
|
|
return nil, errInvalidTypeConversion
|
|
}
|
|
encoded, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to marshal execution payload version=%s", ver)
|
|
}
|
|
return &builderAPI.ExecutionPayloadResponse{
|
|
Version: ver,
|
|
Data: encoded,
|
|
}, nil
|
|
}
|
|
|
|
func (p *Builder) retrievePendingBlock() (*v1.ExecutionPayload, error) {
|
|
result := &engine.ExecutableData{}
|
|
if p.currId == nil {
|
|
return nil, errors.New("no payload id is cached")
|
|
}
|
|
err := p.execClient.CallContext(context.Background(), result, GetPayloadMethod, *p.currId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payloadEnv, err := modifyExecutionPayload(*result, big.NewInt(0), nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
marshalledOutput, err := payloadEnv.ExecutionPayload.MarshalJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
bellatrixPayload := &v1.ExecutionPayload{}
|
|
if err = json.Unmarshal(marshalledOutput, bellatrixPayload); err != nil {
|
|
return nil, err
|
|
}
|
|
p.currId = nil
|
|
return bellatrixPayload, nil
|
|
}
|
|
|
|
func (p *Builder) retrievePendingBlockCapella() (*v1.ExecutionPayloadCapellaWithValue, error) {
|
|
result := &engine.ExecutionPayloadEnvelope{}
|
|
if p.currId == nil {
|
|
return nil, errors.New("no payload id is cached")
|
|
}
|
|
err := p.execClient.CallContext(context.Background(), result, GetPayloadMethodV2, *p.currId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payloadEnv, err := modifyExecutionPayload(*result.ExecutionPayload, result.BlockValue, nil, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
marshalledOutput, err := payloadEnv.MarshalJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
capellaPayload := &v1.ExecutionPayloadCapellaWithValue{}
|
|
if err = json.Unmarshal(marshalledOutput, capellaPayload); err != nil {
|
|
return nil, err
|
|
}
|
|
p.currId = nil
|
|
return capellaPayload, nil
|
|
}
|
|
|
|
func (p *Builder) retrievePendingBlockDeneb() (*v1.ExecutionPayloadDenebWithValueAndBlobsBundle, error) {
|
|
result := &engine.ExecutionPayloadEnvelope{}
|
|
if p.currId == nil {
|
|
return nil, errors.New("no payload id is cached")
|
|
}
|
|
err := p.execClient.CallContext(context.Background(), result, GetPayloadMethodV3, *p.currId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.prevBeaconRoot == nil {
|
|
p.cfg.logger.Errorf("previous root is nil")
|
|
}
|
|
payloadEnv, err := modifyExecutionPayload(*result.ExecutionPayload, result.BlockValue, p.prevBeaconRoot, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payloadEnv.BlobsBundle = result.BlobsBundle
|
|
marshalledOutput, err := payloadEnv.MarshalJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
denebPayload := &v1.ExecutionPayloadDenebWithValueAndBlobsBundle{}
|
|
if err = json.Unmarshal(marshalledOutput, denebPayload); err != nil {
|
|
return nil, err
|
|
}
|
|
p.currId = nil
|
|
return denebPayload, nil
|
|
}
|
|
|
|
func (p *Builder) retrievePendingBlockElectra() (*v1.ExecutionBundleElectra, error) {
|
|
result := &engine.ExecutionPayloadEnvelope{}
|
|
if p.currId == nil {
|
|
return nil, errors.New("no payload id is cached")
|
|
}
|
|
err := p.execClient.CallContext(context.Background(), result, GetPayloadMethodV4, *p.currId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if p.prevBeaconRoot == nil {
|
|
p.cfg.logger.Errorf("previous root is nil")
|
|
}
|
|
|
|
payloadEnv, err := modifyExecutionPayload(*result.ExecutionPayload, result.BlockValue, p.prevBeaconRoot, result.Requests)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payloadEnv.BlobsBundle = result.BlobsBundle
|
|
marshalledOutput, err := payloadEnv.MarshalJSON()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
electraPayload := &v1.ExecutionBundleElectra{}
|
|
if err = json.Unmarshal(marshalledOutput, electraPayload); err != nil {
|
|
return nil, err
|
|
}
|
|
p.currId = nil
|
|
return electraPayload, nil
|
|
}
|
|
|
|
func (p *Builder) sendHttpRequest(req *http.Request, requestBytes []byte) (*http.Response, error) {
|
|
proxyReq, err := http.NewRequest(req.Method, p.cfg.destinationUrl.String(), req.Body)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not create new request")
|
|
return nil, err
|
|
}
|
|
|
|
// Set the modified request as the proxy request body.
|
|
proxyReq.Body = io.NopCloser(bytes.NewBuffer(requestBytes))
|
|
|
|
// Required proxy headers for forwarding JSON-RPC requests to the execution client.
|
|
proxyReq.Header.Set("Host", req.Host)
|
|
proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
|
|
proxyReq.Header.Set("Content-Type", "application/json")
|
|
|
|
client := &http.Client{}
|
|
if p.cfg.secret != "" {
|
|
client = network.NewHttpClientWithSecret(p.cfg.secret, "")
|
|
}
|
|
proxyRes, err := client.Do(proxyReq)
|
|
if err != nil {
|
|
p.cfg.logger.WithError(err).Error("Could not forward request to destination server")
|
|
return nil, err
|
|
}
|
|
return proxyRes, nil
|
|
}
|
|
|
|
// Peek into the bytes of an HTTP request's body.
|
|
func parseRequestBytes(req *http.Request) ([]byte, error) {
|
|
requestBytes, err := io.ReadAll(req.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err = req.Body.Close(); err != nil {
|
|
return nil, err
|
|
}
|
|
req.Body = io.NopCloser(bytes.NewBuffer(requestBytes))
|
|
return requestBytes, nil
|
|
}
|
|
|
|
// Checks whether the JSON-RPC request is for the Ethereum engine API.
|
|
func isEngineAPICall(reqBytes []byte) bool {
|
|
jsonRequest, err := unmarshalRPCObject(reqBytes)
|
|
if err != nil {
|
|
switch {
|
|
case strings.Contains(err.Error(), "cannot unmarshal array"):
|
|
return false
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return strings.Contains(jsonRequest.Method, "engine_")
|
|
}
|
|
|
|
func unmarshalRPCObject(b []byte) (*jsonRPCObject, error) {
|
|
r := &jsonRPCObject{}
|
|
if err := json.Unmarshal(b, r); err != nil {
|
|
return nil, err
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func modifyExecutionPayload(execPayload engine.ExecutableData, fees *big.Int, prevBeaconRoot []byte, requests [][]byte) (*engine.ExecutionPayloadEnvelope, error) {
|
|
modifiedBlock, err := executableDataToBlock(execPayload, prevBeaconRoot, requests)
|
|
if err != nil {
|
|
return &engine.ExecutionPayloadEnvelope{}, err
|
|
}
|
|
return engine.BlockToExecutableData(modifiedBlock, fees, nil /*blobs*/, requests /*requests*/), nil
|
|
}
|
|
|
|
// This modifies the provided payload to imprint the builder's extra data
|
|
func executableDataToBlock(params engine.ExecutableData, prevBeaconRoot []byte, requests [][]byte) (*gethTypes.Block, error) {
|
|
txs, err := decodeTransactions(params.Transactions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Only set withdrawalsRoot if it is non-nil. This allows CLs to use
|
|
// ExecutableData before withdrawals are enabled by marshaling
|
|
// Withdrawals as the json null value.
|
|
var withdrawalsRoot *common.Hash
|
|
if params.Withdrawals != nil {
|
|
h := gethTypes.DeriveSha(gethTypes.Withdrawals(params.Withdrawals), trie.NewStackTrie(nil))
|
|
withdrawalsRoot = &h
|
|
}
|
|
|
|
var requestsHash *common.Hash
|
|
if requests != nil {
|
|
h := gethTypes.CalcRequestsHash(requests)
|
|
requestsHash = &h
|
|
}
|
|
|
|
header := &gethTypes.Header{
|
|
ParentHash: params.ParentHash,
|
|
UncleHash: gethTypes.EmptyUncleHash,
|
|
Coinbase: params.FeeRecipient,
|
|
Root: params.StateRoot,
|
|
TxHash: gethTypes.DeriveSha(gethTypes.Transactions(txs), trie.NewStackTrie(nil)),
|
|
ReceiptHash: params.ReceiptsRoot,
|
|
Bloom: gethTypes.BytesToBloom(params.LogsBloom),
|
|
Difficulty: common.Big0,
|
|
Number: new(big.Int).SetUint64(params.Number),
|
|
GasLimit: params.GasLimit,
|
|
GasUsed: params.GasUsed,
|
|
Time: params.Timestamp,
|
|
BaseFee: params.BaseFeePerGas,
|
|
Extra: []byte("prysm-builder"), // add in extra data
|
|
MixDigest: params.Random,
|
|
WithdrawalsHash: withdrawalsRoot,
|
|
BlobGasUsed: params.BlobGasUsed,
|
|
ExcessBlobGas: params.ExcessBlobGas,
|
|
RequestsHash: requestsHash,
|
|
}
|
|
|
|
if prevBeaconRoot != nil {
|
|
pRoot := common.Hash(prevBeaconRoot)
|
|
header.ParentBeaconRoot = &pRoot
|
|
}
|
|
|
|
body := gethTypes.Body{
|
|
Transactions: txs,
|
|
Uncles: nil,
|
|
Withdrawals: params.Withdrawals,
|
|
}
|
|
block := gethTypes.NewBlockWithHeader(header).WithBody(body)
|
|
return block, nil
|
|
}
|
|
|
|
func decodeTransactions(enc [][]byte) ([]*gethTypes.Transaction, error) {
|
|
var txs = make([]*gethTypes.Transaction, len(enc))
|
|
for i, encTx := range enc {
|
|
var tx gethTypes.Transaction
|
|
if err := tx.UnmarshalBinary(encTx); err != nil {
|
|
return nil, fmt.Errorf("invalid transaction %d: %w", i, err)
|
|
}
|
|
txs[i] = &tx
|
|
}
|
|
return txs, nil
|
|
}
|