From 61cc5d0580f2b36e59fbb9ec24524bee97ae168e Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 6 Jun 2025 13:21:48 +0200 Subject: [PATCH] core/types: improve encoding/decoding --- core/types/tx_blob.go | 147 ++++++++++++++++++++++++++++++------------ 1 file changed, 105 insertions(+), 42 deletions(-) diff --git a/core/types/tx_blob.go b/core/types/tx_blob.go index 547103940a..93a76a28b7 100644 --- a/core/types/tx_blob.go +++ b/core/types/tx_blob.go @@ -19,6 +19,7 @@ package types import ( "bytes" "crypto/sha256" + "errors" "fmt" "math/big" @@ -119,15 +120,21 @@ func (sc *BlobTxSidecar) ValidateBlobCommitmentHashes(hashes []common.Hash) erro return nil } -// blobTxWithBlobs is used for encoding of transactions when blobs are present. -type blobTxWithBlobs struct { +// blobTxWithBlobs represents blob tx with its corresponding sidecar. +// This is an interface because sidecars are versioned. +type blobTxWithBlobs interface { + tx() *BlobTx + assign(*BlobTxSidecar) error +} + +type blobTxWithBlobsV0 struct { BlobTx *BlobTx Blobs []kzg4844.Blob Commitments []kzg4844.Commitment Proofs []kzg4844.Proof } -type versionedBlobTxWithBlobs struct { +type blobTxWithBlobsV1 struct { BlobTx *BlobTx Version byte Blobs []kzg4844.Blob @@ -135,6 +142,33 @@ type versionedBlobTxWithBlobs struct { Proofs []kzg4844.Proof } +func (btx *blobTxWithBlobsV0) tx() *BlobTx { + return btx.BlobTx +} + +func (btx *blobTxWithBlobsV0) assign(sc *BlobTxSidecar) error { + sc.Version = 0 + sc.Blobs = btx.Blobs + sc.Commitments = btx.Commitments + sc.Proofs = btx.Proofs + return nil +} + +func (btx *blobTxWithBlobsV1) tx() *BlobTx { + return btx.BlobTx +} + +func (btx *blobTxWithBlobsV1) assign(sc *BlobTxSidecar) error { + if btx.Version != 1 { + return fmt.Errorf("unsupported blob tx version %d", btx.Version) + } + sc.Version = 1 + sc.Blobs = btx.Blobs + sc.Commitments = btx.Commitments + sc.Proofs = btx.Proofs + return nil +} + // copy creates a deep copy of the transaction data and initializes all fields. func (tx *BlobTx) copy() TxData { cpy := &BlobTx{ @@ -240,69 +274,98 @@ func (tx *BlobTx) withSidecar(sideCar *BlobTxSidecar) *BlobTx { } func (tx *BlobTx) encode(b *bytes.Buffer) error { - if tx.Sidecar == nil { + switch { + case tx.Sidecar == nil: return rlp.Encode(b, tx) - } - // Encode a cell proof transaction - if tx.Sidecar.Version != 0 { - inner := &versionedBlobTxWithBlobs{ + + case tx.Sidecar.Version == 0: + return rlp.Encode(b, &blobTxWithBlobsV0{ + BlobTx: tx, + Blobs: tx.Sidecar.Blobs, + Commitments: tx.Sidecar.Commitments, + Proofs: tx.Sidecar.Proofs, + }) + + case tx.Sidecar.Version == 1: + return rlp.Encode(b, &blobTxWithBlobsV1{ BlobTx: tx, Version: tx.Sidecar.Version, Blobs: tx.Sidecar.Blobs, Commitments: tx.Sidecar.Commitments, Proofs: tx.Sidecar.Proofs, - } - return rlp.Encode(b, inner) + }) + + default: + return errors.New("unsupported sidecar version") } - inner := &blobTxWithBlobs{ - BlobTx: tx, - Blobs: tx.Sidecar.Blobs, - Commitments: tx.Sidecar.Commitments, - Proofs: tx.Sidecar.Proofs, - } - return rlp.Encode(b, inner) } func (tx *BlobTx) decode(input []byte) error { - // Here we need to support two formats: the network protocol encoding of the tx (with - // blobs) or the canonical encoding without blobs. + // Here we need to support two outer formats: the network protocol encoding of the tx + // (with blobs) or the canonical encoding without blobs. // - // The two encodings can be distinguished by checking whether the first element of the - // input list is itself a list. + // The canonical encoding is just a list of fields: + // + // [chainID, nonce, ...] + // + // The network encoding is a list where the first element is the tx in the canonical encoding, + // and the remaining elements are the 'sidecar': + // + // [[chainID, nonce, ...], ...] + // + // The two outer encodings can be distinguished by checking whether the first element + // of the input list is itself a list. If it's the canonical encoding, the first + // element is the chainID, which is a number. - outerList, _, err := rlp.SplitList(input) + firstElem, _, err := rlp.SplitList(input) if err != nil { return err } - firstElemKind, _, _, err := rlp.Split(outerList) + firstElemKind, _, secondElem, err := rlp.Split(firstElem) if err != nil { return err } - if firstElemKind != rlp.List { + // Blob tx without blobs. return rlp.DecodeBytes(input, tx) } - // It's a tx with blobs. Try to decode it as version 0. - var inner versionedBlobTxWithBlobs - if err := rlp.DecodeBytes(input, &inner); err != nil { - var innerV0 blobTxWithBlobs - if err := rlp.DecodeBytes(input, &innerV0); err != nil { - return err - } - inner.BlobTx = innerV0.BlobTx - inner.Version = 0 - inner.Blobs = innerV0.Blobs - inner.Commitments = innerV0.Commitments - inner.Proofs = innerV0.Proofs + // Now we know it's the network encoding with the blob sidecar. Here we again need to + // support multiple encodings: legacy sidecars (v0) with a blob proof, and versioned + // sidecars. + // + // The legacy encoding is: + // + // [tx, blobs, commitments, proofs] + // + // The versioned encoding is: + // + // [tx, version, blobs, ...] + // + // We can tell the two apart by checking whether the second element is the version byte. + // For legacy sidecar the second element is a list of blobs. + + secondElemKind, _, _, err := rlp.Split(secondElem) + if err != nil { + return err } - *tx = *inner.BlobTx - tx.Sidecar = &BlobTxSidecar{ - Version: inner.Version, - Blobs: inner.Blobs, - Commitments: inner.Commitments, - Proofs: inner.Proofs, + var payload blobTxWithBlobs + if secondElemKind == rlp.List { + // No version byte: blob sidecar v0. + payload = new(blobTxWithBlobsV0) + } else { + // It has a version byte. Decode as v1, version is checked by assign() + payload = new(blobTxWithBlobsV1) } + if err := rlp.DecodeBytes(input, payload); err != nil { + return err + } + sc := new(BlobTxSidecar) + if err := payload.assign(sc); err != nil { + return err + } + *tx = *payload.tx() + tx.Sidecar = sc return nil }