From 78905ec2f447794cb8625e95acc2f54d518a6d96 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 1 Jul 2026 14:16:26 +0200 Subject: [PATCH 1/4] fix: set Eth-Execution-Payload-Blinded header on envelope publish Buildoor always publishes full execution payload envelope contents (never a blinded/payload_root body) to POST /eth/v1/beacon/execution_payload_envelopes, but did not send the Eth-Execution-Payload-Blinded header. Per beacon-APIs #580 the request body is selected by this header; lenient CLs (Lighthouse, Lodestar) infer it from the JSON body, but strict ones (Prysm) reject a missing header with 400, so our reveals fail against Prysm-backed nodes. Set the header statically to "false" since we only ever send full contents. --- pkg/rpc/beacon/api.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/rpc/beacon/api.go b/pkg/rpc/beacon/api.go index ca15be2..3650fbb 100644 --- a/pkg/rpc/beacon/api.go +++ b/pkg/rpc/beacon/api.go @@ -94,6 +94,10 @@ func (c *Client) SubmitExecutionPayloadEnvelope(ctx context.Context, envelope js req.Header.Set("Content-Type", "application/json") req.Header.Set("Eth-Consensus-Version", "gloas") + // We always publish full contents (never a blinded/payload_root body), so this is + // statically "false". The header discriminates the request body per beacon-APIs #580; + // lenient CLs infer it from the JSON, but strict ones (e.g. Prysm) reject a missing header. + req.Header.Set("Eth-Execution-Payload-Blinded", "false") httpClient := &http.Client{} From 1491ab25c69fdaac551daf9a00e4b2ba272d52b3 Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 1 Jul 2026 14:17:41 +0200 Subject: [PATCH 2/4] trim comment --- pkg/rpc/beacon/api.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/rpc/beacon/api.go b/pkg/rpc/beacon/api.go index 3650fbb..d61753f 100644 --- a/pkg/rpc/beacon/api.go +++ b/pkg/rpc/beacon/api.go @@ -94,9 +94,7 @@ func (c *Client) SubmitExecutionPayloadEnvelope(ctx context.Context, envelope js req.Header.Set("Content-Type", "application/json") req.Header.Set("Eth-Consensus-Version", "gloas") - // We always publish full contents (never a blinded/payload_root body), so this is - // statically "false". The header discriminates the request body per beacon-APIs #580; - // lenient CLs infer it from the JSON, but strict ones (e.g. Prysm) reject a missing header. + // We always publish full contents, so this is statically "false" (strict CLs reject a missing header). req.Header.Set("Eth-Execution-Payload-Blinded", "false") httpClient := &http.Client{} From fc82c5eb25888778036180b2e9485cd7c7b5e26a Mon Sep 17 00:00:00 2001 From: Barnabas Busa Date: Wed, 1 Jul 2026 14:43:24 +0200 Subject: [PATCH 3/4] fix comment: header is false because body is never blinded (not 'always contents') --- pkg/rpc/beacon/api.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/rpc/beacon/api.go b/pkg/rpc/beacon/api.go index d61753f..2294689 100644 --- a/pkg/rpc/beacon/api.go +++ b/pkg/rpc/beacon/api.go @@ -94,7 +94,8 @@ func (c *Client) SubmitExecutionPayloadEnvelope(ctx context.Context, envelope js req.Header.Set("Content-Type", "application/json") req.Header.Set("Eth-Consensus-Version", "gloas") - // We always publish full contents, so this is statically "false" (strict CLs reject a missing header). + // Buildoor only ever sends an unblinded body (never a payload_root form), so this is + // always "false". Strict CLs (Prysm) reject the request when the header is missing. req.Header.Set("Eth-Execution-Payload-Blinded", "false") httpClient := &http.Client{} From c279ebcd35a6b3b300e74021e7ea27ad91032ef2 Mon Sep 17 00:00:00 2001 From: pk910 Date: Thu, 2 Jul 2026 18:10:49 +0200 Subject: [PATCH 4/4] make payload reveal fully stateless --- pkg/rpc/beacon/api.go | 59 +++++++++++++++++-------------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/pkg/rpc/beacon/api.go b/pkg/rpc/beacon/api.go index 2294689..55abaae 100644 --- a/pkg/rpc/beacon/api.go +++ b/pkg/rpc/beacon/api.go @@ -48,41 +48,36 @@ func (c *Client) SubmitExecutionPayloadBid(ctx context.Context, bid *eth2all.Sig } // signedExecutionPayloadEnvelopeContents is the stateless publish body: the signed -// envelope nested under "signed_execution_payload_envelope" alongside kzg_proofs and blobs -// (mirrors Prysm's SignedExecutionPayloadEnvelopeContents struct). +// envelope nested under "signed_execution_payload_envelope" alongside kzg_proofs and blobs. +// All three fields are required by the beacon-API schema, so kzg_proofs and blobs must be +// present (as empty arrays) even when the payload carries no blobs. type signedExecutionPayloadEnvelopeContents struct { SignedExecutionPayloadEnvelope json.RawMessage `json:"signed_execution_payload_envelope"` - KzgProofs []string `json:"kzg_proofs,omitempty"` - Blobs []string `json:"blobs,omitempty"` + KzgProofs []string `json:"kzg_proofs"` + Blobs []string `json:"blobs"` } -// SubmitExecutionPayloadEnvelope submits a signed execution payload envelope. -// When blobs and kzg proofs are provided they are wrapped in the -// SignedExecutionPayloadEnvelopeContents body so the beacon node can derive -// and broadcast data column sidecars; otherwise the bare signed envelope is sent. +// SubmitExecutionPayloadEnvelope submits a signed execution payload envelope using the +// stateless flow (SignedExecutionPayloadEnvelopeContents body, Eth-Execution-Payload-Blinded +// false). The stateful/blinded flow only works when the beacon node cached the full envelope +// from its own block production (produceBlockV4); buildoor builds payloads externally, so the +// beacon node never has them cached and the stateless form is the only valid one. func (c *Client) SubmitExecutionPayloadEnvelope(ctx context.Context, envelope json.RawMessage, blobs [][]byte, kzgProofs [][]byte) error { url := fmt.Sprintf("%s/eth/v1/beacon/execution_payload_envelopes", c.baseURL) - var bodyJSON []byte - var err error - - if len(blobs) > 0 { - contents := signedExecutionPayloadEnvelopeContents{ - SignedExecutionPayloadEnvelope: envelope, - } - contents.Blobs = make([]string, len(blobs)) - for i, b := range blobs { - contents.Blobs[i] = fmt.Sprintf("0x%x", b) - } - contents.KzgProofs = make([]string, len(kzgProofs)) - for i, p := range kzgProofs { - contents.KzgProofs[i] = fmt.Sprintf("0x%x", p) - } - bodyJSON, err = json.Marshal(contents) - } else { - bodyJSON = envelope + contents := signedExecutionPayloadEnvelopeContents{ + SignedExecutionPayloadEnvelope: envelope, + KzgProofs: make([]string, len(kzgProofs)), + Blobs: make([]string, len(blobs)), + } + for i, b := range blobs { + contents.Blobs[i] = fmt.Sprintf("0x%x", b) + } + for i, p := range kzgProofs { + contents.KzgProofs[i] = fmt.Sprintf("0x%x", p) } + bodyJSON, err := json.Marshal(contents) if err != nil { return fmt.Errorf("failed to marshal publish request: %w", err) } @@ -94,8 +89,8 @@ func (c *Client) SubmitExecutionPayloadEnvelope(ctx context.Context, envelope js req.Header.Set("Content-Type", "application/json") req.Header.Set("Eth-Consensus-Version", "gloas") - // Buildoor only ever sends an unblinded body (never a payload_root form), so this is - // always "false". Strict CLs (Prysm) reject the request when the header is missing. + // Always the stateless form: header "false" selects the Contents body schema. Strict + // CLs (Prysm) reject the request when the header is missing. req.Header.Set("Eth-Execution-Payload-Blinded", "false") httpClient := &http.Client{} @@ -117,7 +112,6 @@ func (c *Client) SubmitExecutionPayloadEnvelope(ctx context.Context, envelope js // PayloadEnvelopeInfo contains key fields from a fetched execution payload envelope. type PayloadEnvelopeInfo struct { - Slot phase0.Slot BlockRoot phase0.Root BlockHash phase0.Hash32 BuilderIndex uint64 @@ -158,7 +152,6 @@ func (c *Client) GetExecutionPayloadEnvelope( } `json:"payload"` BuilderIndex string `json:"builder_index"` BeaconBlockRoot string `json:"beacon_block_root"` - Slot string `json:"slot"` } `json:"message"` } `json:"data"` } @@ -169,11 +162,6 @@ func (c *Client) GetExecutionPayloadEnvelope( msg := &response.Data.Message - slot, err := strconv.ParseUint(msg.Slot, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid slot: %w", err) - } - blockRoot, err := parseRoot(msg.BeaconBlockRoot) if err != nil { return nil, fmt.Errorf("invalid beacon_block_root: %w", err) @@ -190,7 +178,6 @@ func (c *Client) GetExecutionPayloadEnvelope( } return &PayloadEnvelopeInfo{ - Slot: phase0.Slot(slot), BlockRoot: blockRoot, BlockHash: blockHash, BuilderIndex: builderIndex,