From df8fab51f09855db5923fd458648f79859e7ea74 Mon Sep 17 00:00:00 2001 From: Giulio Date: Fri, 6 Mar 2026 18:10:39 +0100 Subject: [PATCH] feat: update SSZ engine and REST transport Co-Authored-By: Claude Sonnet 4.6 (1M context) --- beacon/engine/ssz.go | 66 ++++++++++++++++++---------------------- eth/catalyst/ssz_rest.go | 14 +++++---- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/beacon/engine/ssz.go b/beacon/engine/ssz.go index 7e9500d187..a3b4cfbdec 100644 --- a/beacon/engine/ssz.go +++ b/beacon/engine/ssz.go @@ -76,15 +76,11 @@ func SSZToEngineStatus(status uint8) string { const payloadStatusFixedSize = 9 // status(1) + hash_offset(4) + err_offset(4) // EncodePayloadStatusSSZ encodes a PayloadStatusV1 to SSZ bytes per EIP-8161. +// Uses List[Hash32, 1] encoding: 0 bytes if nil, 32 bytes if present. func EncodePayloadStatusSSZ(ps *PayloadStatusV1) []byte { - // Build Union[None, Hash32] for latest_valid_hash - var hashUnion []byte + var hashData []byte if ps.LatestValidHash != nil { - hashUnion = make([]byte, 33) // selector(1) + hash(32) - hashUnion[0] = 1 - copy(hashUnion[1:33], ps.LatestValidHash[:]) - } else { - hashUnion = []byte{0} + hashData = ps.LatestValidHash[:] } var errorBytes []byte @@ -92,13 +88,13 @@ func EncodePayloadStatusSSZ(ps *PayloadStatusV1) []byte { errorBytes = []byte(*ps.ValidationError) } - buf := make([]byte, payloadStatusFixedSize+len(hashUnion)+len(errorBytes)) + buf := make([]byte, payloadStatusFixedSize+len(hashData)+len(errorBytes)) buf[0] = EngineStatusToSSZ(ps.Status) binary.LittleEndian.PutUint32(buf[1:5], uint32(payloadStatusFixedSize)) - binary.LittleEndian.PutUint32(buf[5:9], uint32(payloadStatusFixedSize+len(hashUnion))) + binary.LittleEndian.PutUint32(buf[5:9], uint32(payloadStatusFixedSize+len(hashData))) - copy(buf[payloadStatusFixedSize:], hashUnion) - copy(buf[payloadStatusFixedSize+len(hashUnion):], errorBytes) + copy(buf[payloadStatusFixedSize:], hashData) + copy(buf[payloadStatusFixedSize+len(hashData):], errorBytes) return buf } @@ -119,16 +115,16 @@ func DecodePayloadStatusSSZ(buf []byte) (*PayloadStatusV1, error) { return nil, fmt.Errorf("PayloadStatusSSZ: offsets out of bounds") } - // Decode Union[None, Hash32] - unionData := buf[hashOffset:errOffset] - if len(unionData) > 0 { - if unionData[0] == 1 { - if len(unionData) < 33 { - return nil, fmt.Errorf("PayloadStatusSSZ: Union hash data too short") - } - hash := common.BytesToHash(unionData[1:33]) - ps.LatestValidHash = &hash - } + // Decode List[Hash32, 1]: 0 bytes = nil, 32 bytes = hash present + hashData := buf[hashOffset:errOffset] + switch len(hashData) { + case 0: + ps.LatestValidHash = nil + case 32: + hash := common.BytesToHash(hashData) + ps.LatestValidHash = &hash + default: + return nil, fmt.Errorf("PayloadStatusSSZ: invalid hash list length %d (expected 0 or 32)", len(hashData)) } // Decode validation_error @@ -175,22 +171,18 @@ const forkchoiceUpdatedResponseFixedSize = 8 func EncodeForkChoiceResponseSSZ(resp *ForkChoiceResponse) []byte { psBytes := EncodePayloadStatusSSZ(&resp.PayloadStatus) - // Build Union[None, uint64] for payload ID - var pidUnion []byte + // Build List[Bytes8, 1] for payload ID: 0 bytes if nil, 8 bytes if present + var pidData []byte if resp.PayloadID != nil { - pidUnion = make([]byte, 9) // selector(1) + 8 bytes - pidUnion[0] = 1 - copy(pidUnion[1:9], resp.PayloadID[:]) - } else { - pidUnion = []byte{0} + pidData = resp.PayloadID[:] } - buf := make([]byte, forkchoiceUpdatedResponseFixedSize+len(psBytes)+len(pidUnion)) + buf := make([]byte, forkchoiceUpdatedResponseFixedSize+len(psBytes)+len(pidData)) binary.LittleEndian.PutUint32(buf[0:4], uint32(forkchoiceUpdatedResponseFixedSize)) binary.LittleEndian.PutUint32(buf[4:8], uint32(forkchoiceUpdatedResponseFixedSize+len(psBytes))) copy(buf[forkchoiceUpdatedResponseFixedSize:], psBytes) - copy(buf[forkchoiceUpdatedResponseFixedSize+len(psBytes):], pidUnion) + copy(buf[forkchoiceUpdatedResponseFixedSize+len(psBytes):], pidData) return buf } @@ -215,15 +207,17 @@ func DecodeForkChoiceResponseSSZ(buf []byte) (*ForkChoiceResponse, error) { } resp.PayloadStatus = *ps - // Decode Union[None, PayloadID] + // Decode List[Bytes8, 1]: 0 bytes = nil, 8 bytes = payload ID present pidData := buf[pidOffset:] - if len(pidData) > 0 && pidData[0] == 1 { - if len(pidData) < 9 { - return nil, fmt.Errorf("ForkChoiceResponseSSZ: Union payload_id data too short") - } + switch len(pidData) { + case 0: + resp.PayloadID = nil + case 8: var pid PayloadID - copy(pid[:], pidData[1:9]) + copy(pid[:], pidData) resp.PayloadID = &pid + default: + return nil, fmt.Errorf("ForkChoiceResponseSSZ: invalid payload_id list length %d (expected 0 or 8)", len(pidData)) } return resp, nil diff --git a/eth/catalyst/ssz_rest.go b/eth/catalyst/ssz_rest.go index 9b9c2bcd80..e3fa3a9884 100644 --- a/eth/catalyst/ssz_rest.go +++ b/eth/catalyst/ssz_rest.go @@ -270,12 +270,14 @@ func (s *SszRestServer) handleForkchoiceUpdated(w http.ResponseWriter, r *http.R if len(body) >= fixedSize { attrOffset := binary.LittleEndian.Uint32(body[96:100]) - if attrOffset <= uint32(len(body)) && attrOffset < uint32(len(body)) { - unionData := body[attrOffset:] - if len(unionData) > 0 { - selector := unionData[0] - if selector == 1 && len(unionData) > 1 { - pa, err := engine.DecodePayloadAttributesSSZ(unionData[1:], version) + if attrOffset <= uint32(len(body)) { + // List[PayloadAttributesV3SSZ, 1]: for variable-size containers, + // the list data starts with a 4-byte offset to the first element. + listData := body[attrOffset:] + if len(listData) > 4 { + elemOffset := binary.LittleEndian.Uint32(listData[0:4]) + if elemOffset <= uint32(len(listData)) { + pa, err := engine.DecodePayloadAttributesSSZ(listData[elemOffset:], version) if err != nil { sszErrorResponse(w, http.StatusBadRequest, -32602, err.Error()) return