diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index b3e4611095..6b0ffded72 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -147,14 +147,57 @@ type blobTxMeta struct { evictionBlobFeeJumps float64 // Worse blob fee (converted to fee jumps) across all previous nonces } -// newBlobTxForPool decomposes a blob transaction into BlobTxForPool -// type. -func newBlobTxForPool(tx *types.Transaction) *types.BlobTxForPool { +// blobTxForPool is the storage representation of a blob transaction in the +// blobpool. +type blobTxForPool struct { + Tx *types.Transaction // tx without sidecar + Version byte + Commitments []kzg4844.Commitment + Proofs []kzg4844.Proof + Blobs []kzg4844.Blob +} + +// Sidecar returns BlobTxSidecar of ptx. +func (ptx *blobTxForPool) Sidecar() *types.BlobTxSidecar { + return types.NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs) +} + +// WithSidecar copies the sidecar's fields into the flat fields. +func (ptx *blobTxForPool) WithSidecar(sc *types.BlobTxSidecar) { + ptx.Version = sc.Version + ptx.Commitments = sc.Commitments + ptx.Proofs = sc.Proofs + ptx.Blobs = sc.Blobs +} + +// TxSize returns the transaction size on the network without +// reconstructing the transaction. +func (ptx *blobTxForPool) TxSize() uint64 { + var blobs, commitments, proofs uint64 + for i := range ptx.Blobs { + blobs += rlp.BytesSize(ptx.Blobs[i][:]) + } + for i := range ptx.Commitments { + commitments += rlp.BytesSize(ptx.Commitments[i][:]) + } + for i := range ptx.Proofs { + proofs += rlp.BytesSize(ptx.Proofs[i][:]) + } + return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(blobs)+rlp.ListSize(commitments)+rlp.ListSize(proofs)) +} + +// ToTx reconstructs a full Transaction with the sidecar attached. +func (ptx *blobTxForPool) ToTx() *types.Transaction { + return ptx.Tx.WithBlobTxSidecar(ptx.Sidecar()) +} + +// newBlobTxForPool decomposes a blob transaction into blobTxForPool type. +func newBlobTxForPool(tx *types.Transaction) *blobTxForPool { sc := tx.BlobTxSidecar() if sc == nil { panic("missing blob tx sidecar") } - return &types.BlobTxForPool{ + return &blobTxForPool{ Tx: tx.WithoutBlobTxSidecar(), Version: sc.Version, Commitments: sc.Commitments, @@ -163,9 +206,72 @@ func newBlobTxForPool(tx *types.Transaction) *types.BlobTxForPool { } } +// encodeForNetwork transforms stored blobTxForPool RLP into the standard +// network transaction encoding. This is used for getRLP. +// +// Stored RLP: [type_byte || tx_fields, version, [comms], [proofs], [blobs]] +// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]]) +// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]]) +func encodeForNetwork(storedRLP []byte) ([]byte, error) { + elems, err := rlp.SplitListValues(storedRLP) + if err != nil { + return nil, fmt.Errorf("invalid blobTxForPool RLP: %w", err) + } + if len(elems) < 5 { + return nil, fmt.Errorf("blobTxForPool has %d elements, need at least 5", len(elems)) + } + + // 1. Extract tx byte and other tx fields + txBytes, _, err := rlp.SplitString(elems[0]) + if err != nil { + return nil, fmt.Errorf("invalid tx bytes: %w", err) + } + if len(txBytes) < 2 { + return nil, errors.New("tx bytes too short") + } + typeByte := txBytes[0] + txRLP := txBytes[1:] + + // 2. Find the version of sidecar. + version, _, err := rlp.SplitString(elems[1]) + if err != nil { + return nil, fmt.Errorf("invalid version: %w", err) + } + var versionByte byte + switch len(version) { + case 0: + versionByte = 0 + case 1: + versionByte = version[0] + default: + return nil, fmt.Errorf("invalid version length: %d", len(version)) + } + // 3. Extract sidecar elements. + commitmentsRLP := elems[2] + proofsRLP := elems[3] + blobsRLP := elems[4] + + // 4. Reconstruct into the network format. + var outer [][]byte + if versionByte == types.BlobSidecarVersion0 { + outer = [][]byte{txRLP, blobsRLP, commitmentsRLP, proofsRLP} + } else { + outer = [][]byte{txRLP, elems[1], blobsRLP, commitmentsRLP, proofsRLP} + } + body, err := rlp.MergeListValues(outer) + if err != nil { + return nil, err + } + // Prepend type byte and wrap as an RLP string. + inner := make([]byte, 1+len(body)) + inner[0] = typeByte + copy(inner[1:], body) + return rlp.EncodeToBytes(inner) +} + // newBlobTxMeta retrieves the indexed metadata fields from a pooled blob // transaction and assembles a helper struct to track in memory. -func newBlobTxMeta(id uint64, size uint64, storageSize uint32, ptx *types.BlobTxForPool) *blobTxMeta { +func newBlobTxMeta(id uint64, size uint64, storageSize uint32, ptx *blobTxForPool) *blobTxMeta { meta := &blobTxMeta{ hash: ptx.Tx.Hash(), vhashes: ptx.Tx.BlobHashes(), @@ -608,7 +714,7 @@ func (p *BlobPool) Close() error { // each transaction on disk to create the in-memory metadata index. // Return value `bool` is set to true when the entry has old Transaction type. func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) (bool, error) { - ptx := new(types.BlobTxForPool) + ptx := new(blobTxForPool) if err := rlp.DecodeBytes(blob, ptx); err != nil { tx := new(types.Transaction) if err := rlp.DecodeBytes(blob, tx); err != nil { @@ -916,7 +1022,7 @@ func (p *BlobPool) offload(addr common.Address, nonce uint64, id uint64, inclusi log.Error("Blobs missing for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err) return } - ptx := new(types.BlobTxForPool) + ptx := new(blobTxForPool) if err := rlp.DecodeBytes(data, ptx); err != nil { log.Error("Blobs corrupted for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err) return @@ -1456,7 +1562,7 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction { if len(data) == 0 { return nil } - ptx := new(types.BlobTxForPool) + ptx := new(blobTxForPool) if err := rlp.DecodeBytes(data, ptx); err != nil { id, _ := p.lookup.storeidOfTx(hash) @@ -1470,7 +1576,7 @@ func (p *BlobPool) Get(hash common.Hash) *types.Transaction { // GetRLP returns a RLP-encoded transaction for network if it is contained in the pool. func (p *BlobPool) GetRLP(hash common.Hash) []byte { data := p.getRLP(hash) - rlp, err := types.EncodeForNetwork(data) + rlp, err := encodeForNetwork(data) if err != nil { log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err) return nil @@ -1545,7 +1651,7 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash, version byte) ([]*kzg4844.Blo } // Decode the blob transaction - ptx := new(types.BlobTxForPool) + ptx := new(blobTxForPool) if err := rlp.DecodeBytes(data, ptx); err != nil { log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err) continue diff --git a/core/txpool/blobpool/blobpool_test.go b/core/txpool/blobpool/blobpool_test.go index edb254d333..2f46b2c8ba 100644 --- a/core/txpool/blobpool/blobpool_test.go +++ b/core/txpool/blobpool/blobpool_test.go @@ -235,7 +235,7 @@ func makeTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64, return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx) } -// encodeForPool encodes a blob transaction in the BlobTxForPool storage format. +// encodeForPool encodes a blob transaction in the blobTxForPool storage format. func encodeForPool(tx *types.Transaction) []byte { blob, _ := rlp.EncodeToBytes(newBlobTxForPool(tx)) return blob @@ -2061,6 +2061,32 @@ func TestGetBlobs(t *testing.T) { pool.Close() } +// TestEncodeForNetwork verifies that encodeForNetwork produces output identical +// to rlp.EncodeToBytes on the original transaction, for both V0 and V1 sidecars. +func TestEncodeForNetwork(t *testing.T) { + t.Run("v0", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion0) }) + t.Run("v1", func(t *testing.T) { testEncodeForNetwork(t, types.BlobSidecarVersion1) }) +} + +func testEncodeForNetwork(t *testing.T, version byte) { + key, _ := crypto.GenerateKey() + tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key, version) + + wantRLP, err := rlp.EncodeToBytes(tx) + if err != nil { + t.Fatalf("failed to encode tx: %v", err) + } + storedRLP := encodeForPool(tx) + + gotRLP, err := encodeForNetwork(storedRLP) + if err != nil { + t.Fatalf("encodeForNetwork failed: %v", err) + } + if !bytes.Equal(gotRLP, wantRLP) { + t.Fatalf("network encoding mismatch (version %d): got %d bytes, want %d bytes", version, len(gotRLP), len(wantRLP)) + } +} + // fakeBilly is a billy.Database implementation which just drops data on the floor. type fakeBilly struct { billy.Database diff --git a/core/txpool/blobpool/limbo.go b/core/txpool/blobpool/limbo.go index f95687f0d0..b8bee2f22a 100644 --- a/core/txpool/blobpool/limbo.go +++ b/core/txpool/blobpool/limbo.go @@ -33,7 +33,7 @@ import ( type limboBlob struct { TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs Block uint64 // Block in which the blob transaction was included - Ptx *types.BlobTxForPool + Ptx *blobTxForPool } // limbo is a light, indexed database to temporarily store recently included @@ -146,7 +146,7 @@ func (l *limbo) finalize(final *types.Header) { // push stores a new blob transaction into the limbo, waiting until finality for // it to be automatically evicted. -func (l *limbo) push(ptx *types.BlobTxForPool, block uint64) error { +func (l *limbo) push(ptx *blobTxForPool, block uint64) error { hash := ptx.Tx.Hash() if _, ok := l.index[hash]; ok { log.Error("Limbo cannot push already tracked blobs", "tx", hash) @@ -162,7 +162,7 @@ func (l *limbo) push(ptx *types.BlobTxForPool, block uint64) error { // pull retrieves a previously pushed set of blobs back from the limbo, removing // it at the same time. This method should be used when a previously included blob // transaction gets reorged out. -func (l *limbo) pull(tx common.Hash) (*types.BlobTxForPool, error) { +func (l *limbo) pull(tx common.Hash) (*blobTxForPool, error) { // If the blobs are not tracked by the limbo, there's not much to do. This // can happen for example if a blob transaction is mined without pushing it // into the network first. @@ -239,7 +239,7 @@ func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) { // setAndIndex assembles a limbo blob database entry and stores it, also updating // the in-memory indices. -func (l *limbo) setAndIndex(ptx *types.BlobTxForPool, block uint64) error { +func (l *limbo) setAndIndex(ptx *blobTxForPool, block uint64) error { txhash := ptx.Tx.Hash() item := &limboBlob{ TxHash: txhash, diff --git a/core/types/tx_blob.go b/core/types/tx_blob.go index 0a442659c6..31aadb5419 100644 --- a/core/types/tx_blob.go +++ b/core/types/tx_blob.go @@ -176,112 +176,6 @@ func (sc *BlobTxSidecar) Copy() *BlobTxSidecar { } } -// BlobTxForPool is a type used for blob transaction in the blobpool. -type BlobTxForPool struct { - Tx *Transaction // tx without sidecar - Version byte - Commitments []kzg4844.Commitment - Proofs []kzg4844.Proof - Blobs []kzg4844.Blob -} - -// Sidecar returns BlobTxSidecar of ptx. -func (ptx *BlobTxForPool) Sidecar() *BlobTxSidecar { - return NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs) -} - -// WithSidecar copies the sidecar's fields into the flat fields. -func (ptx *BlobTxForPool) WithSidecar(sc *BlobTxSidecar) { - ptx.Version = sc.Version - ptx.Commitments = sc.Commitments - ptx.Proofs = sc.Proofs - ptx.Blobs = sc.Blobs -} - -// TxSize returns the transaction size on the network without -// reconstructing the transaction. -func (ptx *BlobTxForPool) TxSize() uint64 { - var blobs, commitments, proofs uint64 - for i := range ptx.Blobs { - blobs += rlp.BytesSize(ptx.Blobs[i][:]) - } - for i := range ptx.Commitments { - commitments += rlp.BytesSize(ptx.Commitments[i][:]) - } - for i := range ptx.Proofs { - proofs += rlp.BytesSize(ptx.Proofs[i][:]) - } - return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(blobs)+rlp.ListSize(commitments)+rlp.ListSize(proofs)) -} - -// ToTx reconstructs a full Transaction with the sidecar attached. -func (ptx *BlobTxForPool) ToTx() *Transaction { - return ptx.Tx.WithBlobTxSidecar(ptx.Sidecar()) -} - -// EncodeForNetwork transforms stored BlobTxForPool RLP into the standard -// network transaction encoding. -// -// Stored RLP: [type_byte || tx_fields, version, [comms], [proofs], [blobs]] -// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]]) -// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]]) -func EncodeForNetwork(storedRLP []byte) ([]byte, error) { - elems, err := rlp.SplitListValues(storedRLP) - if err != nil { - return nil, fmt.Errorf("invalid BlobTxForPool RLP: %w", err) - } - if len(elems) < 5 { - return nil, fmt.Errorf("BlobTxForPool has %d elements, need at least 5", len(elems)) - } - - // 1. Extract tx byte and other tx fields - txBytes, _, err := rlp.SplitString(elems[0]) - if err != nil { - return nil, fmt.Errorf("invalid tx bytes: %w", err) - } - if len(txBytes) < 2 { - return nil, errors.New("tx bytes too short") - } - typeByte := txBytes[0] - txRLP := txBytes[1:] - - // 2. Find the version of sidecar. - version, _, err := rlp.SplitString(elems[1]) - if err != nil { - return nil, fmt.Errorf("invalid version: %w", err) - } - var versionByte byte - switch len(version) { - case 0: - versionByte = 0 - case 1: - versionByte = version[0] - default: - return nil, fmt.Errorf("invalid version length: %d", len(version)) - } - // 3. Extract sidecar elements. - commitmentsRLP := elems[2] - proofsRLP := elems[3] - blobsRLP := elems[4] - - // 4. Reconstruct into the network format. - var outer [][]byte - if versionByte == BlobSidecarVersion0 { - outer = [][]byte{txRLP, blobsRLP, commitmentsRLP, proofsRLP} - } else { - outer = [][]byte{txRLP, elems[1], blobsRLP, commitmentsRLP, proofsRLP} - } - body, err := rlp.MergeListValues(outer) - if err != nil { - return nil, err - } - // Prepend type byte and wrap as an RLP string. - inner := make([]byte, 1+len(body)) - inner[0] = typeByte - copy(inner[1:], body) - return rlp.EncodeToBytes(inner) -} - // blobTxWithBlobs represents blob tx with its corresponding sidecar. // This is an interface because sidecars are versioned. type blobTxWithBlobs interface { diff --git a/core/types/tx_blob_test.go b/core/types/tx_blob_test.go index 46fae4d4f8..3b368456a4 100644 --- a/core/types/tx_blob_test.go +++ b/core/types/tx_blob_test.go @@ -17,14 +17,12 @@ package types import ( - "bytes" "crypto/ecdsa" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" - "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" ) @@ -88,50 +86,6 @@ func createEmptyBlobTx(key *ecdsa.PrivateKey, withSidecar bool) *Transaction { return MustSignNewTx(key, signer, blobtx) } -// TestEncodeForNetwork verifies that EncodeForNetwork produces output identical -// to rlp.EncodeToBytes on the original transaction, for both V0 and V1 sidecars. -func TestEncodeForNetwork(t *testing.T) { - t.Run("v0", func(t *testing.T) { testEncodeForNetwork(t, BlobSidecarVersion0) }) - t.Run("v1", func(t *testing.T) { testEncodeForNetwork(t, BlobSidecarVersion1) }) -} - -func testEncodeForNetwork(t *testing.T, version byte) { - key, _ := crypto.GenerateKey() - tx := createEmptyBlobTx(key, true) - if version == BlobSidecarVersion1 { - if err := tx.BlobTxSidecar().ToV1(); err != nil { - t.Fatalf("failed to convert sidecar to v1: %v", err) - } - } - - wantRLP, err := rlp.EncodeToBytes(tx) - if err != nil { - t.Fatalf("failed to encode tx: %v", err) - } - - sc := tx.BlobTxSidecar() - ptx := &BlobTxForPool{ - Tx: tx.WithoutBlobTxSidecar(), - Version: sc.Version, - Commitments: sc.Commitments, - Proofs: sc.Proofs, - Blobs: sc.Blobs, - } - storedRLP, err := rlp.EncodeToBytes(ptx) - if err != nil { - t.Fatalf("failed to encode BlobTxForPool: %v", err) - } - - gotRLP, err := EncodeForNetwork(storedRLP) - if err != nil { - t.Fatalf("EncodeForNetwork failed: %v", err) - } - - if !bytes.Equal(gotRLP, wantRLP) { - t.Fatalf("network encoding mismatch (version %d): got %d bytes, want %d bytes", version, len(gotRLP), len(wantRLP)) - } -} - func createEmptyBlobTxInner(withSidecar bool) *BlobTx { sidecar := NewBlobTxSidecar(BlobSidecarVersion0, []kzg4844.Blob{*emptyBlob}, []kzg4844.Commitment{emptyBlobCommit}, []kzg4844.Proof{emptyBlobProof}) blobtx := &BlobTx{