From fcefb32655a55560d330e31f5285d40e2eaaa8b1 Mon Sep 17 00:00:00 2001 From: MariusVanDerWijden Date: Wed, 17 Jun 2026 11:08:36 +0200 Subject: [PATCH] update to newest spec --- beacon/engine/ssz/envelopes.go | 129 ++++++++++++++++++----- beacon/engine/ssz/forkmap.go | 13 +++ beacon/engine/ssz/payload.go | 7 ++ beacon/engine/ssz/roundtrip_test.go | 155 ++++++++++++++++++++++++++++ beacon/engine/ssz/shared.go | 4 +- eth/engineapi/get_payload.go | 85 +++++++++++---- eth/engineapi/payloads.go | 5 +- eth/engineapi/router_test.go | 4 +- 8 files changed, 349 insertions(+), 53 deletions(-) diff --git a/beacon/engine/ssz/envelopes.go b/beacon/engine/ssz/envelopes.go index 48a7dbfc6a..904da48e65 100644 --- a/beacon/engine/ssz/envelopes.go +++ b/beacon/engine/ssz/envelopes.go @@ -22,36 +22,85 @@ import ( "github.com/karalabe/ssz" ) -// ExecutionPayloadEnvelopeAmsterdam is the request body of -// POST /amsterdam/payloads. +// ExecutionPayloadEnvelopeAmsterdam is the monolithic request body of +// POST /{fork}/payloads spanning every fork from Paris onward. The wire shape +// is fork-driven by the codec (see the per-fork ExecutionPayloadEnvelope +// catalogue in refactor-ssz.md): +// +// - Paris / Shanghai: bare payload, no envelope fields +// - Cancun+: + ParentBeaconBlockRoot +// - Prague+: + ExecutionRequests +// +// ParentBeaconBlockRoot is a pointer so the codec can distinguish "absent for +// this fork" (nil, Paris/Shanghai) from "present and zero". type ExecutionPayloadEnvelopeAmsterdam struct { Payload *ExecutionPayload - ParentBeaconBlockRoot common.Hash - ExecutionRequests [][]byte + ParentBeaconBlockRoot *common.Hash // Cancun+ + ExecutionRequests [][]byte // Prague+ } func (e *ExecutionPayloadEnvelopeAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // offset(payload) + 32(root) + offset(requests) - size := uint32(40) + // offset(payload) is always present; the root and the requests offset only + // exist from Cancun and Prague respectively (matching the OnFork gating). + size := uint32(4) + if siz.Fork() >= ssz.ForkDencun { + size += 32 // ParentBeaconBlockRoot + } + if siz.Fork() >= ssz.ForkPectra { + size += 4 // offset(requests) + } if fixed { return size } size += ssz.SizeDynamicObject(siz, e.Payload) - size += ssz.SizeSliceOfDynamicBytes(siz, e.ExecutionRequests) + if siz.Fork() >= ssz.ForkPectra { + size += ssz.SizeSliceOfDynamicBytes(siz, e.ExecutionRequests) + } return size } func (e *ExecutionPayloadEnvelopeAmsterdam) DefineSSZ(c *ssz.Codec) { ssz.DefineDynamicObjectOffset(c, &e.Payload) - ssz.DefineStaticBytes(c, &e.ParentBeaconBlockRoot) - ssz.DefineSliceOfDynamicBytesOffset(c, &e.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest) + ssz.DefineStaticBytesPointerOnFork(c, &e.ParentBeaconBlockRoot, fromCancun) + ssz.DefineSliceOfDynamicBytesOffsetOnFork(c, &e.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest, fromPrague) ssz.DefineDynamicObjectContent(c, &e.Payload) - ssz.DefineSliceOfDynamicBytesContent(c, &e.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest) + ssz.DefineSliceOfDynamicBytesContentOnFork(c, &e.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest, fromPrague) } -// BlobsBundleV2 is the cell-proof blob bundle carried by BuiltPayload -// from Osaka onwards. +// BlobsBundleV1 is the single-proof blob bundle carried by BuiltPayload for +// Cancun and Prague: one KZG proof per blob. +type BlobsBundleV1 struct { + Commitments [][48]byte + Proofs [][48]byte + Blobs []*Blob +} + +func (b *BlobsBundleV1) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { + // 3 offsets + size := uint32(12) + if fixed { + return size + } + size += ssz.SizeSliceOfStaticBytes(siz, b.Commitments) + size += ssz.SizeSliceOfStaticBytes(siz, b.Proofs) + size += ssz.SizeSliceOfStaticObjects(siz, b.Blobs) + return size +} + +func (b *BlobsBundleV1) DefineSSZ(c *ssz.Codec) { + ssz.DefineSliceOfStaticBytesOffset(c, &b.Commitments, MaxBlobCommitmentsPerBlock) + ssz.DefineSliceOfStaticBytesOffset(c, &b.Proofs, MaxBlobCommitmentsPerBlock) + ssz.DefineSliceOfStaticObjectsOffset(c, &b.Blobs, MaxBlobCommitmentsPerBlock) + + ssz.DefineSliceOfStaticBytesContent(c, &b.Commitments, MaxBlobCommitmentsPerBlock) + ssz.DefineSliceOfStaticBytesContent(c, &b.Proofs, MaxBlobCommitmentsPerBlock) + ssz.DefineSliceOfStaticObjectsContent(c, &b.Blobs, MaxBlobCommitmentsPerBlock) +} + +// BlobsBundleV2 is the cell-proof blob bundle carried by BuiltPayload from Osaka +// onwards: CellsPerExtBlob cell proofs per blob. The wire layout matches +// BlobsBundleV1 (three slices); only the proofs-list bound differs. type BlobsBundleV2 struct { Commitments [][48]byte Proofs [][48]byte @@ -80,37 +129,67 @@ func (b *BlobsBundleV2) DefineSSZ(c *ssz.Codec) { ssz.DefineSliceOfStaticObjectsContent(c, &b.Blobs, MaxBlobCommitmentsPerBlock) } -// BuiltPayloadAmsterdam is the response of GET /amsterdam/payloads/{id}. +// BuiltPayloadAmsterdam is the monolithic response of GET /{fork}/payloads/{id} +// spanning every fork from Paris onward. The wire shape is fork-driven by the +// codec (see the per-fork BuiltPayload catalogue in refactor-ssz.md): +// +// - Paris / Shanghai: Payload, BlockValue only +// - Cancun: + BlobsBundleV1, ShouldOverrideBuilder +// - Prague: + ExecutionRequests (placed before ShouldOverrideBuilder) +// - Osaka+: BlobsBundle revision becomes V2 (cell proofs) +// +// Exactly one of BlobsBundleV1 / BlobsBundleV2 is active for any given fork +// (V1 = Cancun..Prague, V2 = Osaka+); the other is nil and contributes neither +// an offset nor content. ShouldOverrideBuilder is a pointer so the codec can +// distinguish "absent for this fork" (nil, Paris/Shanghai) from "present and +// false". type BuiltPayloadAmsterdam struct { Payload *ExecutionPayload BlockValue *uint256.Int - BlobsBundle *BlobsBundleV2 - ExecutionRequests [][]byte - ShouldOverrideBuilder bool + BlobsBundleV1 *BlobsBundleV1 // Cancun, Prague + BlobsBundleV2 *BlobsBundleV2 // Osaka+ + ExecutionRequests [][]byte // Prague+ + ShouldOverrideBuilder *bool // Cancun+ } func (p *BuiltPayloadAmsterdam) SizeSSZ(siz *ssz.Sizer, fixed bool) uint32 { - // offset(payload) + 32(value) + offset(bundle) + offset(requests) + 1(override) - size := uint32(4 + 32 + 4 + 4 + 1) + // offset(payload) + 32(value) are always present; the bundle offset and the + // override byte exist from Cancun, and the requests offset from Prague. + size := uint32(4 + 32) + if siz.Fork() >= ssz.ForkDencun { + size += 4 // offset(blobs_bundle) + size++ // should_override_builder + } + if siz.Fork() >= ssz.ForkPectra { + size += 4 // offset(execution_requests) + } if fixed { return size } size += ssz.SizeDynamicObject(siz, p.Payload) - size += ssz.SizeDynamicObject(siz, p.BlobsBundle) - size += ssz.SizeSliceOfDynamicBytes(siz, p.ExecutionRequests) + if siz.Fork() >= forkOsaka { + size += ssz.SizeDynamicObject(siz, p.BlobsBundleV2) + } else if siz.Fork() >= ssz.ForkDencun { + size += ssz.SizeDynamicObject(siz, p.BlobsBundleV1) + } + if siz.Fork() >= ssz.ForkPectra { + size += ssz.SizeSliceOfDynamicBytes(siz, p.ExecutionRequests) + } return size } func (p *BuiltPayloadAmsterdam) DefineSSZ(c *ssz.Codec) { ssz.DefineDynamicObjectOffset(c, &p.Payload) ssz.DefineUint256(c, &p.BlockValue) - ssz.DefineDynamicObjectOffset(c, &p.BlobsBundle) - ssz.DefineSliceOfDynamicBytesOffset(c, &p.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest) - ssz.DefineBool(c, &p.ShouldOverrideBuilder) + ssz.DefineDynamicObjectOffsetOnFork(c, &p.BlobsBundleV1, cancunToOsaka) + ssz.DefineDynamicObjectOffsetOnFork(c, &p.BlobsBundleV2, fromOsaka) + ssz.DefineSliceOfDynamicBytesOffsetOnFork(c, &p.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest, fromPrague) + ssz.DefineBoolPointerOnFork(c, &p.ShouldOverrideBuilder, fromCancun) ssz.DefineDynamicObjectContent(c, &p.Payload) - ssz.DefineDynamicObjectContent(c, &p.BlobsBundle) - ssz.DefineSliceOfDynamicBytesContent(c, &p.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest) + ssz.DefineDynamicObjectContentOnFork(c, &p.BlobsBundleV1, cancunToOsaka) + ssz.DefineDynamicObjectContentOnFork(c, &p.BlobsBundleV2, fromOsaka) + ssz.DefineSliceOfDynamicBytesContentOnFork(c, &p.ExecutionRequests, MaxExecutionRequestsPerPayload, MaxBytesPerExecutionRequest, fromPrague) } // ForkchoiceUpdateAmsterdam is the request body of POST /amsterdam/forkchoice. diff --git a/beacon/engine/ssz/forkmap.go b/beacon/engine/ssz/forkmap.go index d66901af00..9242700de3 100644 --- a/beacon/engine/ssz/forkmap.go +++ b/beacon/engine/ssz/forkmap.go @@ -37,6 +37,19 @@ const ( forkAmsterdam = ssz.ForkPectra + 2 ) +// AtLeast reports whether the ssz fork sf is at or beyond the geth fork f in +// the codec's ordering. It encapsulates the local forkOsaka/forkAmsterdam +// aliases so callers outside this package can compare against post-Pectra forks +// without naming the unexported constants. Pre-Engine-API forks (no SSZ wire +// representation) compare as not-reached. +func AtLeast(sf ssz.Fork, f forks.Fork) bool { + target, ok := ForkFor(f) + if !ok { + return false + } + return sf >= target +} + // ForkFor maps a geth params/forks.Fork onto the karalabe/ssz Fork value // the codec multiplexes on. The bool is false for forks that predate the // Engine API (and thus have no SSZ wire representation here). diff --git a/beacon/engine/ssz/payload.go b/beacon/engine/ssz/payload.go index 751d648252..e9bb0704cf 100644 --- a/beacon/engine/ssz/payload.go +++ b/beacon/engine/ssz/payload.go @@ -36,6 +36,13 @@ var ( fromCancun = ssz.ForkFilter{Added: ssz.ForkDencun} // beacon-root attribute, introduced in Cancun (Dencun). // (shares the Cancun boundary but kept named for clarity at call sites) + // execution-requests group, introduced in Prague (Pectra). + fromPrague = ssz.ForkFilter{Added: ssz.ForkPectra} + // single-proof blobs bundle (BlobsBundleV1), active Cancun..Prague; replaced + // by the cell-proof BlobsBundleV2 from Osaka on. + cancunToOsaka = ssz.ForkFilter{Added: ssz.ForkDencun, Removed: forkOsaka} + // cell-proof blobs bundle (BlobsBundleV2), introduced in Osaka. + fromOsaka = ssz.ForkFilter{Added: forkOsaka} // bal + slot group, introduced in Amsterdam. fromAmsterdam = ssz.ForkFilter{Added: forkAmsterdam} ) diff --git a/beacon/engine/ssz/roundtrip_test.go b/beacon/engine/ssz/roundtrip_test.go index 2e5afa23b8..6dd0b7e4b4 100644 --- a/beacon/engine/ssz/roundtrip_test.go +++ b/beacon/engine/ssz/roundtrip_test.go @@ -17,6 +17,7 @@ package ssz import ( + "encoding/binary" "reflect" "testing" @@ -157,3 +158,157 @@ func TestForkchoiceUpdateAmsterdamRoundtrip(t *testing.T) { } encDecOnFork(t, bare, forkAmsterdam, func() *ForkchoiceUpdateAmsterdam { return new(ForkchoiceUpdateAmsterdam) }) } + +// minimalPayload builds an ExecutionPayload carrying exactly the fields the +// given fork's wire shape requires (BaseFeePerGas is always present; blob-gas +// pointers from Cancun; slot from Amsterdam), so it round-trips cleanly at fork. +func minimalPayload(fork ssz.Fork) *ExecutionPayload { + p := &ExecutionPayload{BaseFeePerGas: uint256.NewInt(7e9)} + if fork >= ssz.ForkDencun { + blob, excess := uint64(0), uint64(0) + p.BlobGasUsed, p.ExcessBlobGas = &blob, &excess + } + if fork >= forkAmsterdam { + slot := uint64(0) + p.SlotNumber = &slot + } + return p +} + +// allEngineForks enumerates the codec forks the Engine API v2 spec covers +// (Paris onward), paired with the spec's fixed-part sizes for the envelope and +// built-payload wrappers. +var allEngineForks = []struct { + name string + fork ssz.Fork + envelopeFixed uint32 // ExecutionPayloadEnvelope fixed part + builtPayloadFix uint32 // BuiltPayload fixed part + hasBeaconRoot bool // Cancun+ + hasRequests bool // Prague+ + hasBundle bool // Cancun+ + bundleIsV2 bool // Osaka+ +}{ + // envelope fixed: offset(payload)=4 [+root 32 from Cancun] [+offset(requests) 4 from Prague] + // built fixed: offset(payload)=4 + value 32 [+offset(bundle) 4 + override 1 from Cancun] [+offset(requests) 4 from Prague] + {"paris", ssz.ForkParis, 4, 36, false, false, false, false}, + {"shanghai", ssz.ForkShapella, 4, 36, false, false, false, false}, + {"cancun", ssz.ForkDencun, 36, 41, true, false, true, false}, + {"prague", ssz.ForkPectra, 40, 45, true, true, true, false}, + {"osaka", forkOsaka, 40, 45, true, true, true, true}, + {"amsterdam", forkAmsterdam, 40, 45, true, true, true, true}, +} + +// TestExecutionPayloadEnvelopePerFork verifies the envelope wire shape matches +// the per-fork ExecutionPayloadEnvelope catalogue in refactor-ssz.md for every +// fork the spec covers: bare payload for Paris/Shanghai, +parent_beacon_block_root +// from Cancun, +execution_requests from Prague. +func TestExecutionPayloadEnvelopePerFork(t *testing.T) { + for _, tc := range allEngineForks { + t.Run(tc.name, func(t *testing.T) { + env := &ExecutionPayloadEnvelopeAmsterdam{Payload: minimalPayload(tc.fork)} + if tc.hasBeaconRoot { + env.ParentBeaconBlockRoot = &common.Hash{0x55} + } + if tc.hasRequests { + env.ExecutionRequests = [][]byte{{0x01, 0x02}} + } + encDecOnFork(t, env, tc.fork, func() *ExecutionPayloadEnvelopeAmsterdam { + return new(ExecutionPayloadEnvelopeAmsterdam) + }) + + // Pre-Cancun must not carry a beacon root; pre-Prague must not carry + // requests — the codec drops them, so a decoded copy stays empty. + buf := make([]byte, ssz.SizeOnFork(env, tc.fork)) + if err := ssz.EncodeToBytesOnFork(buf, env, tc.fork); err != nil { + t.Fatalf("encode: %v", err) + } + // payload is the first (dynamic) field, so its offset equals the + // fixed-part length — the spec's per-fork envelope size. + if off := readOffset(buf); off != tc.envelopeFixed { + t.Errorf("envelope fixed size = %d, want %d", off, tc.envelopeFixed) + } + got := new(ExecutionPayloadEnvelopeAmsterdam) + if err := ssz.DecodeFromBytesOnFork(buf, got, tc.fork); err != nil { + t.Fatalf("decode: %v", err) + } + if !tc.hasBeaconRoot && got.ParentBeaconBlockRoot != nil { + t.Errorf("%s: beacon root decoded but fork predates Cancun", tc.name) + } + if tc.hasBeaconRoot && got.ParentBeaconBlockRoot == nil { + t.Errorf("%s: beacon root missing after roundtrip", tc.name) + } + if !tc.hasRequests && len(got.ExecutionRequests) > 0 { + t.Errorf("%s: requests decoded but fork predates Prague", tc.name) + } + }) + } +} + +// TestBuiltPayloadPerFork verifies the BuiltPayload wire shape matches the +// per-fork BuiltPayload catalogue in refactor-ssz.md: {payload, block_value} +// for Paris/Shanghai, +blobs_bundle(V1)+should_override_builder from Cancun, +// +execution_requests from Prague, blobs_bundle→V2 from Osaka. +func TestBuiltPayloadPerFork(t *testing.T) { + for _, tc := range allEngineForks { + t.Run(tc.name, func(t *testing.T) { + bp := &BuiltPayloadAmsterdam{ + Payload: minimalPayload(tc.fork), + BlockValue: uint256.NewInt(1000), + } + if tc.hasBundle { + bundle := func() ([][48]byte, [][48]byte, []*Blob) { + return [][48]byte{{0x01}}, [][48]byte{{0x02}}, []*Blob{{Bytes: make([]byte, BytesPerBlob)}} + } + c, p, b := bundle() + if tc.bundleIsV2 { + bp.BlobsBundleV2 = &BlobsBundleV2{Commitments: c, Proofs: p, Blobs: b} + } else { + bp.BlobsBundleV1 = &BlobsBundleV1{Commitments: c, Proofs: p, Blobs: b} + } + override := true + bp.ShouldOverrideBuilder = &override + } + if tc.hasRequests { + bp.ExecutionRequests = [][]byte{{0xaa}} + } + encDecOnFork(t, bp, tc.fork, func() *BuiltPayloadAmsterdam { return new(BuiltPayloadAmsterdam) }) + + buf := make([]byte, ssz.SizeOnFork(bp, tc.fork)) + if err := ssz.EncodeToBytesOnFork(buf, bp, tc.fork); err != nil { + t.Fatalf("encode: %v", err) + } + // payload is the first (dynamic) field, so its offset equals the + // fixed-part length — the spec's per-fork built-payload size. + if off := readOffset(buf); off != tc.builtPayloadFix { + t.Errorf("built-payload fixed size = %d, want %d", off, tc.builtPayloadFix) + } + got := new(BuiltPayloadAmsterdam) + if err := ssz.DecodeFromBytesOnFork(buf, got, tc.fork); err != nil { + t.Fatalf("decode: %v", err) + } + // Exactly the right bundle revision is populated (or neither pre-Cancun). + if tc.bundleIsV2 && got.BlobsBundleV1 != nil { + t.Errorf("%s: V1 bundle decoded for an Osaka+ fork", tc.name) + } + if tc.hasBundle && !tc.bundleIsV2 && got.BlobsBundleV2 != nil { + t.Errorf("%s: V2 bundle decoded for a pre-Osaka fork", tc.name) + } + if !tc.hasBundle && (got.BlobsBundleV1 != nil || got.BlobsBundleV2 != nil) { + t.Errorf("%s: bundle decoded but fork predates Cancun", tc.name) + } + if !tc.hasBundle && got.ShouldOverrideBuilder != nil { + t.Errorf("%s: should_override_builder decoded but fork predates Cancun", tc.name) + } + if !tc.hasRequests && len(got.ExecutionRequests) > 0 { + t.Errorf("%s: requests decoded but fork predates Prague", tc.name) + } + }) + } +} + +// readOffset reads the first 4-byte little-endian SSZ offset from buf. For a +// container whose first field is dynamic, this offset equals the length of the +// fixed part (where the variable region begins). +func readOffset(buf []byte) uint32 { + return binary.LittleEndian.Uint32(buf[:4]) +} diff --git a/beacon/engine/ssz/shared.go b/beacon/engine/ssz/shared.go index fd774d62d5..583a91d32b 100644 --- a/beacon/engine/ssz/shared.go +++ b/beacon/engine/ssz/shared.go @@ -34,7 +34,7 @@ func (b *Blob) DefineSSZ(c *ssz.Codec) { ssz.DefineCheckedStaticBytes(c, &b.Bytes, BytesPerBlob) } -// BlobCell wraps a single BYTES_PER_CELL = 1024 byte cell. Same wrapper +// BlobCell wraps a single BYTES_PER_CELL = 2048 byte cell. Same wrapper // rationale as Blob. type BlobCell struct { Bytes []byte @@ -59,7 +59,7 @@ func (b *Bitvector128) DefineSSZ(c *ssz.Codec) { } // OptionalBlobCell models `Optional[ByteVector[BYTES_PER_CELL]]` = -// `List[BlobCell, 1]`. Length 0 = absent, length 1 = present (1024 bytes). +// `List[BlobCell, 1]`. Length 0 = absent, length 1 = present (2048 bytes). type OptionalBlobCell struct { Cells []*BlobCell } diff --git a/eth/engineapi/get_payload.go b/eth/engineapi/get_payload.go index 5e7ab3a80a..1108e21a79 100644 --- a/eth/engineapi/get_payload.go +++ b/eth/engineapi/get_payload.go @@ -56,42 +56,83 @@ func (rt *Router) handleGetPayload(w http.ResponseWriter, r *http.Request, fork } // buildBuiltPayloadAmsterdam packages an engine.ExecutionPayloadEnvelope into -// the SSZ BuiltPayload shape. BlockValue/BlobsBundle/Requests come straight -// across; the inner payload goes through the SSZ converter. +// the SSZ BuiltPayload shape for the URL fork. BlockValue/Requests come straight +// across; the inner payload goes through the SSZ converter. The blobs bundle is +// emitted as V1 (Cancun/Prague) or V2 (Osaka+) per the fork; pre-Cancun forks +// carry no bundle (and no should_override_builder), so those fields are left +// nil and the codec drops them. func buildBuiltPayloadAmsterdam(env *engine.ExecutionPayloadEnvelope, sf ssz.Fork) *sszt.BuiltPayloadAmsterdam { out := &sszt.BuiltPayloadAmsterdam{ - Payload: sszt.ExecutionPayloadFromEngine(env.ExecutionPayload, sf), - BlockValue: new(uint256.Int), - ExecutionRequests: env.Requests, - ShouldOverrideBuilder: env.Override, + Payload: sszt.ExecutionPayloadFromEngine(env.ExecutionPayload, sf), + BlockValue: new(uint256.Int), } if env.BlockValue != nil { out.BlockValue.SetFromBig(env.BlockValue) } - if env.BlobsBundle != nil { - out.BlobsBundle = convertBlobsBundle(env.BlobsBundle) - } else { - out.BlobsBundle = new(sszt.BlobsBundleV2) + // execution_requests and should_override_builder exist from Prague and + // Cancun respectively; the codec gates them, so it is safe to always set + // the values the EL produced — the gated-off forks simply ignore them. + if sszt.AtLeast(sf, forks.Prague) { + out.ExecutionRequests = env.Requests + } + if sszt.AtLeast(sf, forks.Cancun) { + override := env.Override + out.ShouldOverrideBuilder = &override + } + // Select the bundle revision. Cancun/Prague use V1 (one proof per blob); + // Osaka+ uses V2 (cell proofs). Pre-Cancun forks have no bundle. + switch { + case sszt.AtLeast(sf, forks.Osaka): + if env.BlobsBundle != nil { + out.BlobsBundleV2 = convertBlobsBundleV2(env.BlobsBundle) + } else { + out.BlobsBundleV2 = new(sszt.BlobsBundleV2) + } + case sszt.AtLeast(sf, forks.Cancun): + if env.BlobsBundle != nil { + out.BlobsBundleV1 = convertBlobsBundleV1(env.BlobsBundle) + } else { + out.BlobsBundleV1 = new(sszt.BlobsBundleV1) + } } return out } -// convertBlobsBundle copies the JSON BlobsBundle into the SSZ V2 layout. -// Inputs are length-validated by the caller's miner pipeline. -func convertBlobsBundle(b *engine.BlobsBundle) *sszt.BlobsBundleV2 { +// convertBlobsBundleV2 copies the JSON BlobsBundle into the SSZ V2 (cell-proof) +// layout. Inputs are length-validated by the caller's miner pipeline. +func convertBlobsBundleV2(b *engine.BlobsBundle) *sszt.BlobsBundleV2 { out := &sszt.BlobsBundleV2{ Commitments: make([][48]byte, len(b.Commitments)), Proofs: make([][48]byte, len(b.Proofs)), Blobs: make([]*sszt.Blob, len(b.Blobs)), } - for i, c := range b.Commitments { - copy(out.Commitments[i][:], c) - } - for i, p := range b.Proofs { - copy(out.Proofs[i][:], p) - } - for i, blob := range b.Blobs { - out.Blobs[i] = &sszt.Blob{Bytes: append([]byte(nil), blob...)} - } + fillBundle(out.Commitments, out.Proofs, out.Blobs, b) return out } + +// convertBlobsBundleV1 copies the JSON BlobsBundle into the SSZ V1 (single-proof) +// layout used by Cancun/Prague. +func convertBlobsBundleV1(b *engine.BlobsBundle) *sszt.BlobsBundleV1 { + out := &sszt.BlobsBundleV1{ + Commitments: make([][48]byte, len(b.Commitments)), + Proofs: make([][48]byte, len(b.Proofs)), + Blobs: make([]*sszt.Blob, len(b.Blobs)), + } + fillBundle(out.Commitments, out.Proofs, out.Blobs, b) + return out +} + +// fillBundle copies the commitments/proofs/blobs from a JSON BlobsBundle into +// the destination SSZ slices (shared by the V1 and V2 converters, whose wire +// layout is identical). +func fillBundle(commitments, proofs [][48]byte, blobs []*sszt.Blob, b *engine.BlobsBundle) { + for i, c := range b.Commitments { + copy(commitments[i][:], c) + } + for i, p := range b.Proofs { + copy(proofs[i][:], p) + } + for i, blob := range b.Blobs { + blobs[i] = &sszt.Blob{Bytes: append([]byte(nil), blob...)} + } +} diff --git a/eth/engineapi/payloads.go b/eth/engineapi/payloads.go index 7fa92d0eb5..627d4bc2b4 100644 --- a/eth/engineapi/payloads.go +++ b/eth/engineapi/payloads.go @@ -50,8 +50,9 @@ func (rt *Router) handleNewPayload(w http.ResponseWriter, r *http.Request, fork writeProblem(w, http.StatusUnprocessableEntity, ErrInvalidBody, err.Error()) return } - root := env.ParentBeaconBlockRoot - status, err := rt.backend.NewPayload(r.Context(), *data, versionedHashes, &root, env.ExecutionRequests) + // ParentBeaconBlockRoot is absent (nil) for Paris/Shanghai and present from + // Cancun on; pass it through as-is so the EL sees the correct optionality. + status, err := rt.backend.NewPayload(r.Context(), *data, versionedHashes, env.ParentBeaconBlockRoot, env.ExecutionRequests) if err != nil { mapBackendErr(w, err) return diff --git a/eth/engineapi/router_test.go b/eth/engineapi/router_test.go index 2e55c31006..4b229d0637 100644 --- a/eth/engineapi/router_test.go +++ b/eth/engineapi/router_test.go @@ -149,7 +149,7 @@ func TestRouterNewPayload(t *testing.T) { ExcessBlobGas: &excess, SlotNumber: &slot, }, - ParentBeaconBlockRoot: common.Hash{0x55}, + ParentBeaconBlockRoot: &common.Hash{0x55}, } resp := sszPost(t, srv, "/amsterdam/payloads", env, amsterdam) if resp.StatusCode != 200 { @@ -257,7 +257,7 @@ func TestRouterAdvertisedForkRoutable(t *testing.T) { BlobGasUsed: &blob, ExcessBlobGas: &excess, }, - ParentBeaconBlockRoot: common.Hash{0x55}, + ParentBeaconBlockRoot: &common.Hash{0x55}, } resp = sszPost(t, srv, "/osaka/payloads", penv, osaka) if resp.StatusCode != 200 {