Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

server: re-add capella types #724

Merged
merged 3 commits into from
Feb 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 52 additions & 28 deletions server/functionality.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
builderApi "github.com/attestantio/go-builder-client/api"
denebApi "github.com/attestantio/go-builder-client/api/deneb"
builderSpec "github.com/attestantio/go-builder-client/spec"
eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella"
eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb"
eth2ApiV1Electra "github.com/attestantio/go-eth2-client/api/v1/electra"
"github.com/attestantio/go-eth2-client/spec"
Expand All @@ -25,7 +26,8 @@ import (
)

type Payload interface {
*eth2ApiV1Deneb.SignedBlindedBeaconBlock |
*eth2ApiV1Capella.SignedBlindedBeaconBlock |
*eth2ApiV1Deneb.SignedBlindedBeaconBlock |
*eth2ApiV1Electra.SignedBlindedBeaconBlock
}

Expand All @@ -39,8 +41,8 @@ var (

func processPayload[P Payload](m *BoostService, log *logrus.Entry, ua UserAgent, blindedBlock P) (*builderApi.VersionedSubmitBlindedBlockResponse, bidResp) {
var (
slot = slot[P](blindedBlock)
blockHash = blockHash[P](blindedBlock)
slot = slot(blindedBlock)
blockHash = blockHash(blindedBlock)
)
// Get the currentSlotUID for this slot
currentSlotUID := ""
Expand All @@ -53,7 +55,7 @@ func processPayload[P Payload](m *BoostService, log *logrus.Entry, ua UserAgent,
m.slotUIDLock.Unlock()

// Prepare logger
log = prepareLogger[P](log, blindedBlock, ua, currentSlotUID)
log = prepareLogger(log, blindedBlock, ua, currentSlotUID)

// Log how late into the slot the request starts
slotStartTimestamp := m.genesisTime + slot*config.SlotTimeSec
Expand Down Expand Up @@ -110,7 +112,7 @@ func processPayload[P Payload](m *BoostService, log *logrus.Entry, ua UserAgent,
return
}

if err := verifyPayload[P](blindedBlock, log, responsePayload); err != nil {
if err := verifyPayload(blindedBlock, log, responsePayload); err != nil {
return
}

Expand All @@ -133,6 +135,13 @@ func processPayload[P Payload](m *BoostService, log *logrus.Entry, ua UserAgent,
func verifyPayload[P Payload](payload P, log *logrus.Entry, response *builderApi.VersionedSubmitBlindedBlockResponse) error {
// Step 1: verify version
switch any(payload).(type) {
case *eth2ApiV1Capella.SignedBlindedBeaconBlock:
if response.Version != spec.DataVersionCapella {
log.WithFields(logrus.Fields{
"version": response.Version,
}).Error("response version was not capella")
return errInvalidVersion
}
case *eth2ApiV1Deneb.SignedBlindedBeaconBlock:
if response.Version != spec.DataVersionDeneb {
log.WithFields(logrus.Fields{
Expand All @@ -155,38 +164,41 @@ func verifyPayload[P Payload](payload P, log *logrus.Entry, response *builderApi
return errEmptyPayload
}

// TODO(MariusVanDerWijden): make this generic once
// execution payload or blobs bundle change between forks.
var (
executionPayload *deneb.ExecutionPayload
blobs *denebApi.BlobsBundle
)

switch any(payload).(type) {
// Step 3: verify post-conditions
switch block := any(payload).(type) {
case *eth2ApiV1Capella.SignedBlindedBeaconBlock:
if err := verifyBlockhash(log, payload, response.Capella.BlockHash); err != nil {
return err
}
case *eth2ApiV1Deneb.SignedBlindedBeaconBlock:
executionPayload = response.Deneb.ExecutionPayload
blobs = response.Deneb.BlobsBundle
if err := verifyBlockhash(log, payload, response.Deneb.ExecutionPayload.BlockHash); err != nil {
return err
}
if err := verifyKZGCommitments(log, response.Deneb.BlobsBundle, block.Message.Body.BlobKZGCommitments); err != nil {
return err
}
case *eth2ApiV1Electra.SignedBlindedBeaconBlock:
executionPayload = response.Electra.ExecutionPayload
blobs = response.Electra.BlobsBundle
if err := verifyBlockhash(log, payload, response.Electra.ExecutionPayload.BlockHash); err != nil {
return err
}
if err := verifyKZGCommitments(log, response.Electra.BlobsBundle, block.Message.Body.BlobKZGCommitments); err != nil {
return err
}
}
return nil
}

// Step 3: Ensure the response blockhash matches the request
if blockHash[P](payload) != executionPayload.BlockHash {
func verifyBlockhash[P Payload](log *logrus.Entry, payload P, executionPayloadHash phase0.Hash32) error {
if blockHash(payload) != executionPayloadHash {
log.WithFields(logrus.Fields{
"responseBlockHash": executionPayload.String(),
"responseBlockHash": executionPayloadHash.String(),
}).Error("requestBlockHash does not equal responseBlockHash")
return errInvalidBlockhash
}
return nil
}

// Step 4: Verify KZG commitments
var commitments []deneb.KZGCommitment
switch block := any(payload).(type) {
case *eth2ApiV1Deneb.SignedBlindedBeaconBlock:
commitments = block.Message.Body.BlobKZGCommitments
case *eth2ApiV1Electra.SignedBlindedBeaconBlock:
commitments = block.Message.Body.BlobKZGCommitments
}
func verifyKZGCommitments(log *logrus.Entry, blobs *denebApi.BlobsBundle, commitments []deneb.KZGCommitment) error {
// Ensure that blobs are valid and matches the request
if len(commitments) != len(blobs.Blobs) || len(commitments) != len(blobs.Commitments) || len(commitments) != len(blobs.Proofs) {
log.WithFields(logrus.Fields{
Expand All @@ -213,6 +225,14 @@ func verifyPayload[P Payload](payload P, log *logrus.Entry, response *builderApi

func prepareLogger[P Payload](log *logrus.Entry, payload P, userAgent UserAgent, slotUID string) *logrus.Entry {
switch block := any(payload).(type) {
case *eth2ApiV1Capella.SignedBlindedBeaconBlock:
return log.WithFields(logrus.Fields{
"ua": userAgent,
"slot": block.Message.Slot,
"blockHash": block.Message.Body.ExecutionPayloadHeader.BlockHash.String(),
"parentHash": block.Message.Body.ExecutionPayloadHeader.ParentHash.String(),
"slotUID": slotUID,
})
case *eth2ApiV1Deneb.SignedBlindedBeaconBlock:
return log.WithFields(logrus.Fields{
"ua": userAgent,
Expand All @@ -235,6 +255,8 @@ func prepareLogger[P Payload](log *logrus.Entry, payload P, userAgent UserAgent,

func slot[P Payload](payload P) uint64 {
switch block := any(payload).(type) {
case *eth2ApiV1Capella.SignedBlindedBeaconBlock:
return uint64(block.Message.Slot)
case *eth2ApiV1Deneb.SignedBlindedBeaconBlock:
return uint64(block.Message.Slot)
case *eth2ApiV1Electra.SignedBlindedBeaconBlock:
Expand All @@ -245,6 +267,8 @@ func slot[P Payload](payload P) uint64 {

func blockHash[P Payload](payload P) phase0.Hash32 {
switch block := any(payload).(type) {
case *eth2ApiV1Capella.SignedBlindedBeaconBlock:
return block.Message.Body.ExecutionPayloadHeader.BlockHash
case *eth2ApiV1Deneb.SignedBlindedBeaconBlock:
return block.Message.Body.ExecutionPayloadHeader.BlockHash
case *eth2ApiV1Electra.SignedBlindedBeaconBlock:
Expand Down
24 changes: 23 additions & 1 deletion server/mock/mock_relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

builderApi "github.com/attestantio/go-builder-client/api"
builderApiCapella "github.com/attestantio/go-builder-client/api/capella"
builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb"
builderApiElectra "github.com/attestantio/go-builder-client/api/electra"
builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
Expand Down Expand Up @@ -173,6 +174,27 @@ func (m *Relay) defaultHandleRegisterValidator(w http.ResponseWriter, req *http.
// method
func (m *Relay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publicKey string, version spec.DataVersion) *builderSpec.VersionedSignedBuilderBid {
switch version {
case spec.DataVersionCapella:
// Fill the payload with custom values.
message := &builderApiCapella.BuilderBid{
Header: &capella.ExecutionPayloadHeader{
BlockHash: HexToHash(blockHash),
ParentHash: HexToHash(parentHash),
WithdrawalsRoot: phase0.Root{},
},
Value: uint256.NewInt(value),
Pubkey: HexToPubkey(publicKey),
}
// Sign the message.
signature, err := ssz.SignMessage(message, ssz.DomainBuilder, m.secretKey)
require.NoError(m.t, err)
return &builderSpec.VersionedSignedBuilderBid{
Version: spec.DataVersionCapella,
Capella: &builderApiCapella.SignedBuilderBid{
Message: message,
Signature: signature,
},
}
case spec.DataVersionDeneb:
message := &builderApiDeneb.BuilderBid{
Header: &deneb.ExecutionPayloadHeader{
Expand Down Expand Up @@ -222,7 +244,7 @@ func (m *Relay) MakeGetHeaderResponse(value uint64, blockHash, parentHash, publi
Signature: signature,
},
}
case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix, spec.DataVersionCapella:
case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix:
return nil
}
return nil
Expand Down
56 changes: 43 additions & 13 deletions server/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (

builderApi "github.com/attestantio/go-builder-client/api"
builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella"
eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb"
eth2ApiV1Electra "github.com/attestantio/go-eth2-client/api/v1/electra"
"github.com/attestantio/go-eth2-client/spec/phase0"
Expand Down Expand Up @@ -369,25 +370,54 @@ func (m *BoostService) handleGetPayload(w http.ResponseWriter, req *http.Request
// Read user agent for logging
userAgent := UserAgent(req.Header.Get("User-Agent"))

// New forks need to be added at the front of this array.
// The ordering of the array conveys precedence of the decoders.
//nolint: forcetypeassert
decoders := []struct {
fork string
payload any
processor func(payload any) (*builderApi.VersionedSubmitBlindedBlockResponse, bidResp)
}{
{
fork: "Electra",
payload: new(eth2ApiV1Electra.SignedBlindedBeaconBlock),
processor: func(payload any) (*builderApi.VersionedSubmitBlindedBlockResponse, bidResp) {
return processPayload[*eth2ApiV1Electra.SignedBlindedBeaconBlock](m, log, userAgent, payload.(*eth2ApiV1Electra.SignedBlindedBeaconBlock))
},
},
{
fork: "Deneb",
payload: new(eth2ApiV1Deneb.SignedBlindedBeaconBlock),
processor: func(payload any) (*builderApi.VersionedSubmitBlindedBlockResponse, bidResp) {
return processPayload[*eth2ApiV1Deneb.SignedBlindedBeaconBlock](m, log, userAgent, payload.(*eth2ApiV1Deneb.SignedBlindedBeaconBlock))
},
},
{
fork: "Capella",
payload: new(eth2ApiV1Capella.SignedBlindedBeaconBlock),
processor: func(payload any) (*builderApi.VersionedSubmitBlindedBlockResponse, bidResp) {
return processPayload[*eth2ApiV1Capella.SignedBlindedBeaconBlock](m, log, userAgent, payload.(*eth2ApiV1Capella.SignedBlindedBeaconBlock))
},
},
}

// Decode the body now
payload := new(eth2ApiV1Electra.SignedBlindedBeaconBlock)
log.Debug("attempting to decode body into Electra payload")
if err := DecodeJSON(bytes.NewReader(body), payload); err != nil {
log.Debug("could not decode Electra request payload, attempting to decode body into Deneb payload")
payload := new(eth2ApiV1Deneb.SignedBlindedBeaconBlock)
for _, decoder := range decoders {
payload := decoder.payload
// decode
log.Debugf("attempting to decode body into %v payload", decoder.fork)
if err := DecodeJSON(bytes.NewReader(body), payload); err != nil {
log.Debug("could not decode Deneb request payload")
log.WithError(err).WithField("body", string(body)).Error("could not decode request payload from the beacon-node (signed blinded beacon block)")
m.respondError(w, http.StatusBadRequest, err.Error())
return
log.Debugf("could not decode %v request payload", decoder.fork)
continue
}

result, originalBid := processPayload[*eth2ApiV1Deneb.SignedBlindedBeaconBlock](m, log, userAgent, payload)
// process
result, originalBid := decoder.processor(payload)
m.respondPayload(w, log, result, originalBid)
return
}
result, originalBid := processPayload[*eth2ApiV1Electra.SignedBlindedBeaconBlock](m, log, userAgent, payload)
m.respondPayload(w, log, result, originalBid)
// no decoder was able to decode the body, log error
log.WithError(err).WithField("body", string(body)).Error("could not decode request payload from the beacon-node (signed blinded beacon block)")
m.respondError(w, http.StatusBadRequest, "could not decode body")
}

// CheckRelays sends a request to each one of the relays previously registered to get their status
Expand Down
78 changes: 78 additions & 0 deletions server/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
builderApiDeneb "github.com/attestantio/go-builder-client/api/deneb"
builderApiV1 "github.com/attestantio/go-builder-client/api/v1"
builderSpec "github.com/attestantio/go-builder-client/spec"
eth2ApiV1Capella "github.com/attestantio/go-eth2-client/api/v1/capella"
eth2ApiV1Deneb "github.com/attestantio/go-eth2-client/api/v1/deneb"
eth2ApiV1Electra "github.com/attestantio/go-eth2-client/api/v1/electra"
"github.com/attestantio/go-eth2-client/spec"
Expand Down Expand Up @@ -91,6 +92,27 @@ func (be *testBackend) request(t *testing.T, method, path string, payload any) *
return rr
}

func blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock *eth2ApiV1Capella.SignedBlindedBeaconBlock) *capella.ExecutionPayload {
header := signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader
return &capella.ExecutionPayload{
ParentHash: header.ParentHash,
FeeRecipient: header.FeeRecipient,
StateRoot: header.StateRoot,
ReceiptsRoot: header.ReceiptsRoot,
LogsBloom: header.LogsBloom,
PrevRandao: header.PrevRandao,
BlockNumber: header.BlockNumber,
GasLimit: header.GasLimit,
GasUsed: header.GasUsed,
Timestamp: header.Timestamp,
ExtraData: header.ExtraData,
BaseFeePerGas: header.BaseFeePerGas,
BlockHash: header.BlockHash,
Transactions: make([]bellatrix.Transaction, 0),
Withdrawals: make([]*capella.Withdrawal, 0),
}
}

func blindedBlockContentsToPayloadDeneb(signedBlindedBlockContents *eth2ApiV1Deneb.SignedBlindedBeaconBlock) *builderApiDeneb.ExecutionPayloadAndBlobsBundle {
header := signedBlindedBlockContents.Message.Body.ExecutionPayloadHeader
numBlobs := len(signedBlindedBlockContents.Message.Body.BlobKZGCommitments)
Expand Down Expand Up @@ -795,6 +817,62 @@ func TestEmptyTxRoot(t *testing.T) {
require.Equal(t, "0x7ffe241ea60187fdb0187bfa22de35d1f9bed7ab061d9401fd47e34a54fbede1", txRootHex)
}

func TestGetPayloadWithTestdata(t *testing.T) {
path := "/eth/v1/builder/blinded_blocks"
testPayloadsFiles := []string{
"../testdata/signed-blinded-beacon-block-capella.json",
}
for _, fn := range testPayloadsFiles {
t.Run(fn, func(t *testing.T) {
jsonFile, err := os.Open(fn)
require.NoError(t, err)
defer jsonFile.Close()
signedBlindedBeaconBlock := new(eth2ApiV1Capella.SignedBlindedBeaconBlock)
require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBeaconBlock))
backend := newTestBackend(t, 1, time.Second)
mockResp := builderApi.VersionedSubmitBlindedBlockResponse{
Version: spec.DataVersionCapella,
Capella: &capella.ExecutionPayload{
BlockHash: signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash,
Withdrawals: make([]*capella.Withdrawal, 0),
},
}
backend.relays[0].GetPayloadResponse = &mockResp
rr := backend.request(t, http.MethodPost, path, signedBlindedBeaconBlock)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
require.Equal(t, 1, backend.relays[0].GetRequestCount(path))
resp := new(builderApi.VersionedSubmitBlindedBlockResponse)
err = json.Unmarshal(rr.Body.Bytes(), resp)
require.NoError(t, err)
require.Equal(t, signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Capella.BlockHash)
})
}
}

func TestGetPayloadCapella(t *testing.T) {
// Load the signed blinded beacon block used for getPayload
jsonFile, err := os.Open("../testdata/signed-blinded-beacon-block-capella.json")
require.NoError(t, err)
defer jsonFile.Close()
signedBlindedBeaconBlock := new(eth2ApiV1Capella.SignedBlindedBeaconBlock)
require.NoError(t, DecodeJSON(jsonFile, &signedBlindedBeaconBlock))
backend := newTestBackend(t, 1, time.Second)
// Prepare getPayload response
backend.relays[0].GetPayloadResponse = &builderApi.VersionedSubmitBlindedBlockResponse{
Version: spec.DataVersionCapella,
Capella: blindedBlockToExecutionPayloadCapella(signedBlindedBeaconBlock),
}
// call getPayload, ensure it's only called on relay 0 (origin of the bid)
getPayloadPath := "/eth/v1/builder/blinded_blocks"
rr := backend.request(t, http.MethodPost, getPayloadPath, signedBlindedBeaconBlock)
require.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
require.Equal(t, 1, backend.relays[0].GetRequestCount(getPayloadPath))
resp := new(builderApi.VersionedSubmitBlindedBlockResponse)
err = json.Unmarshal(rr.Body.Bytes(), resp)
require.NoError(t, err)
require.Equal(t, signedBlindedBeaconBlock.Message.Body.ExecutionPayloadHeader.BlockHash, resp.Capella.BlockHash)
}

func TestGetPayloadDeneb(t *testing.T) {
// Load the signed blinded beacon block used for getPayload
jsonFile, err := os.Open("../testdata/signed-blinded-beacon-block-deneb.json")
Expand Down
6 changes: 5 additions & 1 deletion server/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,10 @@ func checkRelaySignature(bid *builderSpec.VersionedSignedBuilderBid, domain phas

func getPayloadResponseIsEmpty(payload *builderApi.VersionedSubmitBlindedBlockResponse) bool {
switch payload.Version {
case spec.DataVersionCapella:
if payload.Capella == nil || payload.Capella.BlockHash == nilHash {
return true
}
case spec.DataVersionDeneb:
if payload.Deneb == nil || payload.Deneb.ExecutionPayload == nil ||
payload.Deneb.ExecutionPayload.BlockHash == nilHash ||
Expand All @@ -254,7 +258,7 @@ func getPayloadResponseIsEmpty(payload *builderApi.VersionedSubmitBlindedBlockRe
payload.Electra.BlobsBundle == nil {
return true
}
case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix, spec.DataVersionCapella:
case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix:
return true
}
return false
Expand Down
Loading
Loading