Skip to content

Commit c7e6c08

Browse files
eth/catalyst: implement getBlobsV2 (#31791)
Implements `engine_getBlobsV2` which is needed for PeerDAS. --------- Co-authored-by: Felix Lange <fjl@twurst.com>
1 parent 35dd84c commit c7e6c08

File tree

17 files changed

+543
-126
lines changed

17 files changed

+543
-126
lines changed

beacon/engine/types.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ type BlobAndProofV1 struct {
123123
Proof hexutil.Bytes `json:"proof"`
124124
}
125125

126+
type BlobAndProofV2 struct {
127+
Blob hexutil.Bytes `json:"blob"`
128+
CellProofs []hexutil.Bytes `json:"proofs"`
129+
}
130+
126131
// JSON type overrides for ExecutionPayloadEnvelope.
127132
type executionPayloadEnvelopeMarshaling struct {
128133
BlockValue *hexutil.Big
@@ -331,7 +336,9 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.
331336
for j := range sidecar.Blobs {
332337
bundle.Blobs = append(bundle.Blobs, hexutil.Bytes(sidecar.Blobs[j][:]))
333338
bundle.Commitments = append(bundle.Commitments, hexutil.Bytes(sidecar.Commitments[j][:]))
334-
bundle.Proofs = append(bundle.Proofs, hexutil.Bytes(sidecar.Proofs[j][:]))
339+
}
340+
for _, proof := range sidecar.Proofs {
341+
bundle.Proofs = append(bundle.Proofs, hexutil.Bytes(proof[:]))
335342
}
336343
}
337344

beacon/engine/types_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package engine
18+
19+
import (
20+
"testing"
21+
22+
"github.com/ethereum/go-ethereum/common"
23+
"github.com/ethereum/go-ethereum/core/types"
24+
"github.com/ethereum/go-ethereum/crypto/kzg4844"
25+
)
26+
27+
func TestBlobs(t *testing.T) {
28+
var (
29+
emptyBlob = new(kzg4844.Blob)
30+
emptyBlobCommit, _ = kzg4844.BlobToCommitment(emptyBlob)
31+
emptyBlobProof, _ = kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit)
32+
emptyCellProof, _ = kzg4844.ComputeCellProofs(emptyBlob)
33+
)
34+
header := types.Header{}
35+
block := types.NewBlock(&header, &types.Body{}, nil, nil)
36+
37+
sidecarWithoutCellProofs := &types.BlobTxSidecar{
38+
Blobs: []kzg4844.Blob{*emptyBlob},
39+
Commitments: []kzg4844.Commitment{emptyBlobCommit},
40+
Proofs: []kzg4844.Proof{emptyBlobProof},
41+
}
42+
env := BlockToExecutableData(block, common.Big0, []*types.BlobTxSidecar{sidecarWithoutCellProofs}, nil)
43+
if len(env.BlobsBundle.Proofs) != 1 {
44+
t.Fatalf("Expect 1 proof in blobs bundle, got %v", len(env.BlobsBundle.Proofs))
45+
}
46+
47+
sidecarWithCellProofs := &types.BlobTxSidecar{
48+
Blobs: []kzg4844.Blob{*emptyBlob},
49+
Commitments: []kzg4844.Commitment{emptyBlobCommit},
50+
Proofs: emptyCellProof,
51+
}
52+
env = BlockToExecutableData(block, common.Big0, []*types.BlobTxSidecar{sidecarWithCellProofs}, nil)
53+
if len(env.BlobsBundle.Proofs) != 128 {
54+
t.Fatalf("Expect 128 proofs in blobs bundle, got %v", len(env.BlobsBundle.Proofs))
55+
}
56+
}

core/txpool/blobpool/blobpool.go

Lines changed: 21 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ import (
3636
"github.com/ethereum/go-ethereum/core/state"
3737
"github.com/ethereum/go-ethereum/core/txpool"
3838
"github.com/ethereum/go-ethereum/core/types"
39-
"github.com/ethereum/go-ethereum/crypto/kzg4844"
4039
"github.com/ethereum/go-ethereum/event"
4140
"github.com/ethereum/go-ethereum/log"
4241
"github.com/ethereum/go-ethereum/metrics"
@@ -1302,27 +1301,13 @@ func (p *BlobPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
13021301
}
13031302
}
13041303

1305-
// GetBlobs returns a number of blobs are proofs for the given versioned hashes.
1304+
// GetBlobs returns a number of blobs and proofs for the given versioned hashes.
13061305
// This is a utility method for the engine API, enabling consensus clients to
13071306
// retrieve blobs from the pools directly instead of the network.
1308-
func (p *BlobPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof) {
1309-
// Create a map of the blob hash to indices for faster fills
1310-
var (
1311-
blobs = make([]*kzg4844.Blob, len(vhashes))
1312-
proofs = make([]*kzg4844.Proof, len(vhashes))
1313-
)
1314-
index := make(map[common.Hash]int)
1315-
for i, vhash := range vhashes {
1316-
index[vhash] = i
1317-
}
1318-
// Iterate over the blob hashes, pulling transactions that fill it. Take care
1319-
// to also fill anything else the transaction might include (probably will).
1320-
for i, vhash := range vhashes {
1321-
// If already filled by a previous fetch, skip
1322-
if blobs[i] != nil {
1323-
continue
1324-
}
1325-
// Unfilled, retrieve the datastore item (in a short lock)
1307+
func (p *BlobPool) GetBlobs(vhashes []common.Hash) []*types.BlobTxSidecar {
1308+
sidecars := make([]*types.BlobTxSidecar, len(vhashes))
1309+
for idx, vhash := range vhashes {
1310+
// Retrieve the datastore item (in a short lock)
13261311
p.lock.RLock()
13271312
id, exists := p.lookup.storeidOfBlob(vhash)
13281313
if !exists {
@@ -1342,16 +1327,24 @@ func (p *BlobPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.
13421327
log.Error("Blobs corrupted for traced transaction", "id", id, "err", err)
13431328
continue
13441329
}
1345-
// Fill anything requested, not just the current versioned hash
1346-
sidecar := item.BlobTxSidecar()
1347-
for j, blobhash := range item.BlobHashes() {
1348-
if idx, ok := index[blobhash]; ok {
1349-
blobs[idx] = &sidecar.Blobs[j]
1350-
proofs[idx] = &sidecar.Proofs[j]
1351-
}
1330+
sidecars[idx] = item.BlobTxSidecar()
1331+
}
1332+
return sidecars
1333+
}
1334+
1335+
// AvailableBlobs returns the number of blobs that are available in the subpool.
1336+
func (p *BlobPool) AvailableBlobs(vhashes []common.Hash) int {
1337+
available := 0
1338+
for _, vhash := range vhashes {
1339+
// Retrieve the datastore item (in a short lock)
1340+
p.lock.RLock()
1341+
_, exists := p.lookup.storeidOfBlob(vhash)
1342+
p.lock.RUnlock()
1343+
if exists {
1344+
available++
13521345
}
13531346
}
1354-
return blobs, proofs
1347+
return available
13551348
}
13561349

13571350
// Add inserts a set of blob transactions into the pool if they pass validation (both

core/txpool/blobpool/blobpool_test.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,23 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
417417
for i := range testBlobVHashes {
418418
copy(hashes[i][:], testBlobVHashes[i][:])
419419
}
420-
blobs, proofs := pool.GetBlobs(hashes)
421-
420+
sidecars := pool.GetBlobs(hashes)
421+
var blobs []*kzg4844.Blob
422+
var proofs []*kzg4844.Proof
423+
for idx, sidecar := range sidecars {
424+
if sidecar == nil {
425+
blobs = append(blobs, nil)
426+
proofs = append(proofs, nil)
427+
continue
428+
}
429+
blobHashes := sidecar.BlobHashes()
430+
for i, hash := range blobHashes {
431+
if hash == hashes[idx] {
432+
blobs = append(blobs, &sidecar.Blobs[i])
433+
proofs = append(proofs, &sidecar.Proofs[i])
434+
}
435+
}
436+
}
422437
// Cross validate what we received vs what we wanted
423438
if len(blobs) != len(hashes) || len(proofs) != len(hashes) {
424439
t.Errorf("retrieved blobs/proofs size mismatch: have %d/%d, want %d", len(blobs), len(proofs), len(hashes))

core/txpool/legacypool/legacypool.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import (
3535
"github.com/ethereum/go-ethereum/core/state"
3636
"github.com/ethereum/go-ethereum/core/txpool"
3737
"github.com/ethereum/go-ethereum/core/types"
38-
"github.com/ethereum/go-ethereum/crypto/kzg4844"
3938
"github.com/ethereum/go-ethereum/event"
4039
"github.com/ethereum/go-ethereum/log"
4140
"github.com/ethereum/go-ethereum/metrics"
@@ -1063,12 +1062,6 @@ func (pool *LegacyPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
10631062
}
10641063
}
10651064

1066-
// GetBlobs is not supported by the legacy transaction pool, it is just here to
1067-
// implement the txpool.SubPool interface.
1068-
func (pool *LegacyPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof) {
1069-
return nil, nil
1070-
}
1071-
10721065
// Has returns an indicator whether txpool has a transaction cached with the
10731066
// given hash.
10741067
func (pool *LegacyPool) Has(hash common.Hash) bool {

core/txpool/subpool.go

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"github.com/ethereum/go-ethereum/common"
2424
"github.com/ethereum/go-ethereum/core"
2525
"github.com/ethereum/go-ethereum/core/types"
26-
"github.com/ethereum/go-ethereum/crypto/kzg4844"
2726
"github.com/ethereum/go-ethereum/event"
2827
"github.com/holiman/uint256"
2928
)
@@ -133,11 +132,6 @@ type SubPool interface {
133132
// given transaction hash.
134133
GetMetadata(hash common.Hash) *TxMetadata
135134

136-
// GetBlobs returns a number of blobs are proofs for the given versioned hashes.
137-
// This is a utility method for the engine API, enabling consensus clients to
138-
// retrieve blobs from the pools directly instead of the network.
139-
GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof)
140-
141135
// ValidateTxBasics checks whether a transaction is valid according to the consensus
142136
// rules, but does not check state-dependent validation such as sufficient balance.
143137
// This check is meant as a static check which can be performed without holding the

core/txpool/txpool.go

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"github.com/ethereum/go-ethereum/core"
2727
"github.com/ethereum/go-ethereum/core/state"
2828
"github.com/ethereum/go-ethereum/core/types"
29-
"github.com/ethereum/go-ethereum/crypto/kzg4844"
3029
"github.com/ethereum/go-ethereum/event"
3130
"github.com/ethereum/go-ethereum/log"
3231
"github.com/ethereum/go-ethereum/params"
@@ -308,22 +307,6 @@ func (p *TxPool) GetMetadata(hash common.Hash) *TxMetadata {
308307
return nil
309308
}
310309

311-
// GetBlobs returns a number of blobs are proofs for the given versioned hashes.
312-
// This is a utility method for the engine API, enabling consensus clients to
313-
// retrieve blobs from the pools directly instead of the network.
314-
func (p *TxPool) GetBlobs(vhashes []common.Hash) ([]*kzg4844.Blob, []*kzg4844.Proof) {
315-
for _, subpool := range p.subpools {
316-
// It's an ugly to assume that only one pool will be capable of returning
317-
// anything meaningful for this call, but anythingh else requires merging
318-
// partial responses and that's too annoying to do until we get a second
319-
// blobpool (probably never).
320-
if blobs, proofs := subpool.GetBlobs(vhashes); blobs != nil {
321-
return blobs, proofs
322-
}
323-
}
324-
return nil, nil
325-
}
326-
327310
// Add enqueues a batch of transactions into the pool if they are valid. Due
328311
// to the large transaction churn, add may postpone fully integrating the tx
329312
// to a later point to batch multiple ones together.

core/txpool/validation.go

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -138,28 +138,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
138138
return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip)
139139
}
140140
if tx.Type() == types.BlobTxType {
141-
// Ensure the blob fee cap satisfies the minimum blob gas price
142-
if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 {
143-
return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice)
144-
}
145-
sidecar := tx.BlobTxSidecar()
146-
if sidecar == nil {
147-
return errors.New("missing sidecar in blob transaction")
148-
}
149-
// Ensure the number of items in the blob transaction and various side
150-
// data match up before doing any expensive validations
151-
hashes := tx.BlobHashes()
152-
if len(hashes) == 0 {
153-
return errors.New("blobless blob transaction")
154-
}
155-
maxBlobs := eip4844.MaxBlobsPerBlock(opts.Config, head.Time)
156-
if len(hashes) > maxBlobs {
157-
return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), maxBlobs)
158-
}
159-
// Ensure commitments, proofs and hashes are valid
160-
if err := validateBlobSidecar(hashes, sidecar); err != nil {
161-
return err
162-
}
141+
return validateBlobTx(tx, head, opts)
163142
}
164143
if tx.Type() == types.SetCodeTxType {
165144
if len(tx.SetCodeAuthorizations()) == 0 {
@@ -169,18 +148,46 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
169148
return nil
170149
}
171150

172-
func validateBlobSidecar(hashes []common.Hash, sidecar *types.BlobTxSidecar) error {
151+
// validateBlobTx implements the blob-transaction specific validations.
152+
func validateBlobTx(tx *types.Transaction, head *types.Header, opts *ValidationOptions) error {
153+
sidecar := tx.BlobTxSidecar()
154+
if sidecar == nil {
155+
return errors.New("missing sidecar in blob transaction")
156+
}
157+
// Ensure the blob fee cap satisfies the minimum blob gas price
158+
if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 {
159+
return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice)
160+
}
161+
// Ensure the number of items in the blob transaction and various side
162+
// data match up before doing any expensive validations
163+
hashes := tx.BlobHashes()
164+
if len(hashes) == 0 {
165+
return errors.New("blobless blob transaction")
166+
}
167+
maxBlobs := eip4844.MaxBlobsPerBlock(opts.Config, head.Time)
168+
if len(hashes) > maxBlobs {
169+
return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), maxBlobs)
170+
}
173171
if len(sidecar.Blobs) != len(hashes) {
174172
return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes))
175173
}
176-
if len(sidecar.Proofs) != len(hashes) {
177-
return fmt.Errorf("invalid number of %d blob proofs compared to %d blob hashes", len(sidecar.Proofs), len(hashes))
178-
}
179174
if err := sidecar.ValidateBlobCommitmentHashes(hashes); err != nil {
180175
return err
181176
}
182-
// Blob commitments match with the hashes in the transaction, verify the
183-
// blobs themselves via KZG
177+
// Fork-specific sidecar checks, including proof verification.
178+
if opts.Config.IsOsaka(head.Number, head.Time) {
179+
return validateBlobSidecarOsaka(sidecar, hashes)
180+
}
181+
return validateBlobSidecarLegacy(sidecar, hashes)
182+
}
183+
184+
func validateBlobSidecarLegacy(sidecar *types.BlobTxSidecar, hashes []common.Hash) error {
185+
if sidecar.Version != 0 {
186+
return fmt.Errorf("invalid sidecar version pre-osaka: %v", sidecar.Version)
187+
}
188+
if len(sidecar.Proofs) != len(hashes) {
189+
return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes))
190+
}
184191
for i := range sidecar.Blobs {
185192
if err := kzg4844.VerifyBlobProof(&sidecar.Blobs[i], sidecar.Commitments[i], sidecar.Proofs[i]); err != nil {
186193
return fmt.Errorf("invalid blob %d: %v", i, err)
@@ -189,6 +196,16 @@ func validateBlobSidecar(hashes []common.Hash, sidecar *types.BlobTxSidecar) err
189196
return nil
190197
}
191198

199+
func validateBlobSidecarOsaka(sidecar *types.BlobTxSidecar, hashes []common.Hash) error {
200+
if sidecar.Version != 1 {
201+
return fmt.Errorf("invalid sidecar version post-osaka: %v", sidecar.Version)
202+
}
203+
if len(sidecar.Proofs) != len(hashes)*kzg4844.CellProofsPerBlob {
204+
return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes)*kzg4844.CellProofsPerBlob)
205+
}
206+
return kzg4844.VerifyCellProofs(sidecar.Blobs, sidecar.Commitments, sidecar.Proofs)
207+
}
208+
192209
// ValidationOptionsWithState define certain differences between stateful transaction
193210
// validation across the different pools without having to duplicate those checks.
194211
type ValidationOptionsWithState struct {

0 commit comments

Comments
 (0)