Implement EIP 8070

This commit is contained in:
healthykim 2026-06-26 13:47:31 +02:00
parent cda717581d
commit 49e199bbfb
57 changed files with 10023 additions and 4602 deletions

View file

@ -174,6 +174,11 @@ type BlobAndProofV2 struct {
CellProofs []hexutil.Bytes `json:"proofs"` // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs. CellProofs []hexutil.Bytes `json:"proofs"` // proofs MUST contain exactly CELLS_PER_EXT_BLOB cell proofs.
} }
type BlobCellsAndProofsV1 struct {
BlobCells []*hexutil.Bytes `json:"blob_cells"`
Proofs []*hexutil.Bytes `json:"proofs"`
}
// BlobAndProofListV2 is a list of BlobAndProofV2 with a hand-rolled JSON marshaler // BlobAndProofListV2 is a list of BlobAndProofV2 with a hand-rolled JSON marshaler
// that avoids the overhead of encoding/json for large blob payloads. // that avoids the overhead of encoding/json for large blob payloads.
type BlobAndProofListV2 []*BlobAndProofV2 type BlobAndProofListV2 []*BlobAndProofV2

View file

@ -66,10 +66,11 @@ func (s *Suite) dialAs(key *ecdsa.PrivateKey) (*Conn, error) {
return nil, err return nil, err
} }
conn.caps = []p2p.Cap{ conn.caps = []p2p.Cap{
{Name: "eth", Version: 72},
{Name: "eth", Version: 70}, {Name: "eth", Version: 70},
{Name: "eth", Version: 69}, {Name: "eth", Version: 69},
} }
conn.ourHighestProtoVersion = 70 conn.ourHighestProtoVersion = 72
return &conn, nil return &conn, nil
} }
@ -106,6 +107,10 @@ type Conn struct {
ourHighestProtoVersion uint ourHighestProtoVersion uint
ourHighestSnapProtoVersion uint ourHighestSnapProtoVersion uint
caps []p2p.Cap caps []p2p.Cap
// pending holds messages received by readUntil that did not match the
// caller's expected type.
pending []any
} }
// Read reads a packet from the connection. // Read reads a packet from the connection.
@ -181,11 +186,15 @@ func (c *Conn) ReadEth() (any, error) {
case eth.TransactionsMsg: case eth.TransactionsMsg:
msg = new(eth.TransactionsPacket) msg = new(eth.TransactionsPacket)
case eth.NewPooledTransactionHashesMsg: case eth.NewPooledTransactionHashesMsg:
msg = new(eth.NewPooledTransactionHashesPacket) msg = new(eth.NewPooledTransactionHashesPacket72)
case eth.GetPooledTransactionsMsg: case eth.GetPooledTransactionsMsg:
msg = new(eth.GetPooledTransactionsPacket) msg = new(eth.GetPooledTransactionsPacket)
case eth.PooledTransactionsMsg: case eth.PooledTransactionsMsg:
msg = new(eth.PooledTransactionsPacket) msg = new(eth.PooledTransactionsPacket)
case eth.GetCellsMsg:
msg = new(eth.GetCellsRequestPacket)
case eth.CellsMsg:
msg = new(eth.CellsPacket)
default: default:
panic(fmt.Sprintf("unhandled eth msg code %d", code)) panic(fmt.Sprintf("unhandled eth msg code %d", code))
} }

View file

@ -18,31 +18,32 @@ package ethtest
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"os"
"path/filepath"
"time" "time"
"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/golang-jwt/jwt/v4" "github.com/golang-jwt/jwt/v4"
) )
// EngineClient is a wrapper around engine-related data. // EngineClient is a wrapper around engine-related data.
type EngineClient struct { type EngineClient struct {
url string url string
jwt [32]byte jwt [32]byte
headfcu []byte chain *Chain
http *http.Client
} }
// NewEngineClient creates a new engine client. // NewEngineClient creates a new engine client.
func NewEngineClient(dir, url, jwt string) (*EngineClient, error) { func NewEngineClient(url, jwtSecret string, chain *Chain) *EngineClient {
headfcu, err := os.ReadFile(filepath.Join(dir, "headfcu.json")) return &EngineClient{
if err != nil { url: url,
return nil, fmt.Errorf("failed to read headfcu: %w", err) jwt: common.HexToHash(jwtSecret),
chain: chain,
http: &http.Client{Timeout: 10 * time.Second},
} }
return &EngineClient{url, common.HexToHash(jwt), headfcu}, nil
} }
// token returns the jwt claim token for authorization. // token returns the jwt claim token for authorization.
@ -52,18 +53,41 @@ func (ec *EngineClient) token() string {
return token return token
} }
// sendForkchoiceUpdated sends an fcu for the head of the generated chain. // rpcRequest marshals a JSON-RPC 2.0 request body for the given method and params.
func (ec *EngineClient) sendForkchoiceUpdated() error { func rpcRequest(method string, params ...any) ([]byte, error) {
var ( p, err := json.Marshal(params)
req, _ = http.NewRequest(http.MethodPost, ec.url, io.NopCloser(bytes.NewReader(ec.headfcu))) if err != nil {
header = make(http.Header) return nil, err
) }
// Set header return fmt.Appendf(nil, `{"jsonrpc":"2.0","id":1,"method":%q,"params":%s}`, method, p), nil
header.Set("accept", "application/json") }
header.Set("content-type", "application/json")
header.Set("Authorization", fmt.Sprintf("Bearer %v", ec.token()))
req.Header = header
_, err := new(http.Client).Do(req) // call sends an authenticated Engine API JSON-RPC request. Response body is
// not inspected — only transport errors are returned.
func (ec *EngineClient) call(method string, params ...any) error {
body, err := rpcRequest(method, params...)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost, ec.url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Set("accept", "application/json")
req.Header.Set("content-type", "application/json")
req.Header.Set("Authorization", "Bearer "+ec.token())
_, err = ec.http.Do(req)
return err return err
} }
// sendForkchoiceUpdated sends an fcu for the head of the generated chain.
func (ec *EngineClient) sendForkchoiceUpdated() error {
head := ec.chain.Head().Hash()
state := engine.ForkchoiceStateV1{
HeadBlockHash: head,
SafeBlockHash: head,
FinalizedBlockHash: head,
}
return ec.call("engine_forkchoiceUpdatedV3", state, nil)
}

2
cmd/devp2p/internal/ethtest/mkchain.sh Normal file → Executable file
View file

@ -6,5 +6,5 @@ hivechain generate \
--tx-interval 1 \ --tx-interval 1 \
--length 600 \ --length 600 \
--outdir testdata \ --outdir testdata \
--lastfork prague \ --lastfork osaka \
--outputs accounts,genesis,chain,headstate,txinfo,headblock,headfcu,newpayload,forkenv --outputs accounts,genesis,chain,headstate,txinfo,headblock,headfcu,newpayload,forkenv

View file

@ -32,7 +32,7 @@ const (
// Unexported devp2p protocol lengths from p2p package. // Unexported devp2p protocol lengths from p2p package.
const ( const (
baseProtoLen = 16 baseProtoLen = 16
ethProtoLen = 18 ethProtoLen = 22
// snapProtoLen accommodates snap/2 (EIP-8189) which extends snap/1 with two // snapProtoLen accommodates snap/2 (EIP-8189) which extends snap/1 with two
// additional message codes (GetBlockAccessLists=0x08, BlockAccessLists=0x09). // additional message codes (GetBlockAccessLists=0x08, BlockAccessLists=0x09).
// Using 10 is safe for snap/1 connections because the extra codes are simply // Using 10 is safe for snap/1 connections because the extra codes are simply

View file

@ -21,6 +21,7 @@ import (
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt" "fmt"
"os"
"reflect" "reflect"
"sync" "sync"
"time" "time"
@ -55,10 +56,7 @@ func NewSuite(dest *enode.Node, chainDir, engineURL, jwt string) (*Suite, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
engine, err := NewEngineClient(chainDir, engineURL, jwt) engine := NewEngineClient(engineURL, jwt, chain)
if err != nil {
return nil, err
}
return &Suite{ return &Suite{
Dest: dest, Dest: dest,
@ -93,6 +91,10 @@ func (s *Suite) EthTests() []utesting.Test {
{Name: "BlobViolations", Fn: s.TestBlobViolations}, {Name: "BlobViolations", Fn: s.TestBlobViolations},
{Name: "TestBlobTxWithoutSidecar", Fn: s.TestBlobTxWithoutSidecar}, {Name: "TestBlobTxWithoutSidecar", Fn: s.TestBlobTxWithoutSidecar},
{Name: "TestBlobTxWithMismatchedSidecar", Fn: s.TestBlobTxWithMismatchedSidecar}, {Name: "TestBlobTxWithMismatchedSidecar", Fn: s.TestBlobTxWithMismatchedSidecar},
// test eth/72 blob txs
{Name: "BlobTxAvailabilityFailure", Fn: s.TestBlobTxAvailabilityFailure},
{Name: "GetCells", Fn: s.TestGetCells},
{Name: "BlobTxWithInvalidCells", Fn: s.TestBlobTxWithInvalidCells},
} }
} }
@ -976,7 +978,7 @@ the transactions using a GetPooledTransactions request.`)
} }
// Send announcement. // Send announcement.
ann := eth.NewPooledTransactionHashesPacket{Types: txTypes, Sizes: sizes, Hashes: hashes} ann := eth.NewPooledTransactionHashesPacket72{Types: txTypes, Sizes: sizes, Hashes: hashes}
err = conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann) err = conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann)
if err != nil { if err != nil {
t.Fatalf("failed to write to connection: %v", err) t.Fatalf("failed to write to connection: %v", err)
@ -994,7 +996,7 @@ the transactions using a GetPooledTransactions request.`)
t.Fatalf("unexpected number of txs requested: wanted %d, got %d", len(hashes), len(msg.GetPooledTransactionsRequest)) t.Fatalf("unexpected number of txs requested: wanted %d, got %d", len(hashes), len(msg.GetPooledTransactionsRequest))
} }
return return
case *eth.NewPooledTransactionHashesPacket: case *eth.NewPooledTransactionHashesPacket72:
continue continue
case *eth.TransactionsPacket: case *eth.TransactionsPacket:
continue continue
@ -1020,15 +1022,16 @@ func makeSidecar(data ...byte) *types.BlobTxSidecar {
return types.NewBlobTxSidecar(types.BlobSidecarVersion1, blobs, commitments, proofs) return types.NewBlobTxSidecar(types.BlobSidecarVersion1, blobs, commitments, proofs)
} }
func (s *Suite) makeBlobTxs(count, blobs int, discriminator byte) (txs types.Transactions) { func (s *Suite) makeBlobTxs(txCount, blobCount int, discriminator byte) (txs types.Transactions, blobs [][]kzg4844.Blob) {
from, nonce := s.chain.GetSender(5) from, nonce := s.chain.GetSender(5)
for i := 0; i < count; i++ { for i := 0; i < txCount; i++ {
// Make blob data, max of 2 blobs per tx. // Make blob data, max of 2 blobs per tx.
blobdata := make([]byte, min(blobs, 2)) blobdata := make([]byte, min(blobCount, 2))
for i := range blobdata { for i := range blobdata {
blobdata[i] = discriminator blobdata[i] = discriminator
blobs -= 1 blobCount -= 1
} }
sidecar := makeSidecar(blobdata...)
inner := &types.BlobTx{ inner := &types.BlobTx{
ChainID: uint256.MustFromBig(s.chain.config.ChainID), ChainID: uint256.MustFromBig(s.chain.config.ChainID),
Nonce: nonce + uint64(i), Nonce: nonce + uint64(i),
@ -1036,16 +1039,19 @@ func (s *Suite) makeBlobTxs(count, blobs int, discriminator byte) (txs types.Tra
GasFeeCap: uint256.MustFromBig(s.chain.Head().BaseFee()), GasFeeCap: uint256.MustFromBig(s.chain.Head().BaseFee()),
Gas: 100000, Gas: 100000,
BlobFeeCap: uint256.MustFromBig(eip4844.CalcBlobFee(s.chain.config, s.chain.Head().Header())), BlobFeeCap: uint256.MustFromBig(eip4844.CalcBlobFee(s.chain.config, s.chain.Head().Header())),
BlobHashes: makeSidecar(blobdata...).BlobHashes(), BlobHashes: sidecar.BlobHashes(),
Sidecar: makeSidecar(blobdata...), Sidecar: sidecar,
} }
tx, err := s.chain.SignTx(from, types.NewTx(inner)) tx, err := s.chain.SignTx(from, types.NewTx(inner))
if err != nil { if err != nil {
panic("blob tx signing failed") panic("blob tx signing failed")
} }
txs = append(txs, tx) blobs = append(blobs, sidecar.Blobs)
scNoBlob := sidecar.Copy()
scNoBlob.Blobs = nil
txs = append(txs, tx.WithBlobTxSidecar(scNoBlob))
} }
return txs return txs, blobs
} }
func (s *Suite) TestBlobViolations(t *utesting.T) { func (s *Suite) TestBlobViolations(t *utesting.T) {
@ -1056,28 +1062,30 @@ func (s *Suite) TestBlobViolations(t *utesting.T) {
} }
// Create blob txs for each tests with unique tx hashes. // Create blob txs for each tests with unique tx hashes.
var ( var (
t1 = s.makeBlobTxs(2, 3, 0x1) t1, _ = s.makeBlobTxs(2, 3, 0x1)
t2 = s.makeBlobTxs(2, 3, 0x2) t2, _ = s.makeBlobTxs(2, 3, 0x2)
) )
for _, test := range []struct { for _, test := range []struct {
ann eth.NewPooledTransactionHashesPacket ann eth.NewPooledTransactionHashesPacket72
resp eth.PooledTransactionsResponse resp eth.PooledTransactionsResponse
}{ }{
// Invalid tx size. // Invalid tx size.
{ {
ann: eth.NewPooledTransactionHashesPacket{ ann: eth.NewPooledTransactionHashesPacket72{
Types: []byte{types.BlobTxType, types.BlobTxType}, Types: []byte{types.BlobTxType, types.BlobTxType},
Sizes: []uint32{uint32(t1[0].Size()), uint32(t1[1].Size() + 10)}, Sizes: []uint32{uint32(t1[0].Size()), uint32(t1[1].Size() + 10)},
Hashes: []common.Hash{t1[0].Hash(), t1[1].Hash()}, Hashes: []common.Hash{t1[0].Hash(), t1[1].Hash()},
Mask: types.CustodyBitmapAll,
}, },
resp: eth.PooledTransactionsResponse(t1), resp: eth.PooledTransactionsResponse(t1),
}, },
// Wrong tx type. // Wrong tx type.
{ {
ann: eth.NewPooledTransactionHashesPacket{ ann: eth.NewPooledTransactionHashesPacket72{
Types: []byte{types.DynamicFeeTxType, types.BlobTxType}, Types: []byte{types.DynamicFeeTxType, types.BlobTxType},
Sizes: []uint32{uint32(t2[0].Size()), uint32(t2[1].Size())}, Sizes: []uint32{uint32(t2[0].Size()), uint32(t2[1].Size())},
Hashes: []common.Hash{t2[0].Hash(), t2[1].Hash()}, Hashes: []common.Hash{t2[0].Hash(), t2[1].Hash()},
Mask: types.CustodyBitmapAll,
}, },
resp: eth.PooledTransactionsResponse(t2), resp: eth.PooledTransactionsResponse(t2),
}, },
@ -1105,15 +1113,21 @@ func (s *Suite) TestBlobViolations(t *utesting.T) {
if code, _, err := conn.Read(); err != nil { if code, _, err := conn.Read(); err != nil {
t.Fatalf("expected disconnect on blob violation, got err: %v", err) t.Fatalf("expected disconnect on blob violation, got err: %v", err)
} else if code != discMsg { } else if code != discMsg {
if code == protoOffset(ethProto)+eth.NewPooledTransactionHashesMsg { for {
// sometimes we'll get a blob transaction hashes announcement before the disconnect code, _, err := conn.Read()
// because blob transactions are scheduled to be fetched right away. if err != nil {
if code, _, err = conn.Read(); err != nil { t.Fatalf("expected disconnect on blob violation, got err: %v", err)
t.Fatalf("expected disconnect on blob violation, got err on second read: %v", err) }
if code == discMsg {
break
}
switch code {
case protoOffset(ethProto) + eth.NewPooledTransactionHashesMsg,
protoOffset(ethProto) + eth.GetCellsMsg:
continue
default:
t.Fatalf("expected disconnect on blob violation, got msg code: %d", code)
} }
}
if code != discMsg {
t.Fatalf("expected disconnect on blob violation, got msg code: %d", code)
} }
} }
conn.Close() conn.Close()
@ -1132,22 +1146,29 @@ func mangleSidecar(tx *types.Transaction) *types.Transaction {
func (s *Suite) TestBlobTxWithoutSidecar(t *utesting.T) { func (s *Suite) TestBlobTxWithoutSidecar(t *utesting.T) {
t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`) t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`)
tx := s.makeBlobTxs(1, 2, 42)[0] tx, _ := s.makeBlobTxs(1, 2, 42)
badTx := tx.WithoutBlobTxSidecar() badTx := tx[0].WithoutBlobTxSidecar()
s.testBadBlobTx(t, tx, badTx) s.testBadBlobTx(t, tx[0], badTx)
} }
func (s *Suite) TestBlobTxWithMismatchedSidecar(t *utesting.T) { func (s *Suite) TestBlobTxWithMismatchedSidecar(t *utesting.T) {
t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs, whose commitment don't correspond to the blob_versioned_hashes in the transaction, will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`) t.Log(`This test checks that a blob transaction first advertised/transmitted without blobs, whose commitment don't correspond to the blob_versioned_hashes in the transaction, will result in the sending peer being disconnected, and the full transaction should be successfully retrieved from another peer.`)
tx := s.makeBlobTxs(1, 2, 43)[0] tx, _ := s.makeBlobTxs(1, 2, 43)
badTx := mangleSidecar(tx) badTx := mangleSidecar(tx[0])
s.testBadBlobTx(t, tx, badTx) s.testBadBlobTx(t, tx[0], badTx)
} }
// readUntil reads eth protocol messages until a message of the target type is // readUntil reads eth protocol messages until a message of the target type is
// received. It returns an error if there is a disconnect, or if the context // received. It returns an error if there is a disconnect, or if the context
// is cancelled before a message of the desired type can be read. // is cancelled before a message of the desired type can be read.
func readUntil[T any](ctx context.Context, conn *Conn) (*T, error) { func readUntil[T any](ctx context.Context, conn *Conn) (*T, error) {
// First check the buffer for a previously-stashed match.
for i, msg := range conn.pending {
if t, ok := msg.(*T); ok {
conn.pending = append(conn.pending[:i], conn.pending[i+1:]...)
return t, nil
}
}
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@ -1161,11 +1182,10 @@ func readUntil[T any](ctx context.Context, conn *Conn) (*T, error) {
} }
continue continue
} }
if t, ok := received.(*T); ok {
switch res := received.(type) { return t, nil
case *T:
return res, nil
} }
conn.pending = append(conn.pending, received)
} }
} }
@ -1203,10 +1223,11 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types
return return
} }
ann := eth.NewPooledTransactionHashesPacket{ ann := eth.NewPooledTransactionHashesPacket72{
Types: []byte{types.BlobTxType}, Types: []byte{types.BlobTxType},
Sizes: []uint32{uint32(badTx.Size())}, Sizes: []uint32{uint32(badTx.Size())},
Hashes: []common.Hash{badTx.Hash()}, Hashes: []common.Hash{badTx.Hash()},
Mask: types.CustodyBitmapAll,
} }
if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil { if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
@ -1254,14 +1275,15 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types
return return
} }
ann := eth.NewPooledTransactionHashesPacket{ ann := eth.NewPooledTransactionHashesPacket72{
Types: []byte{types.BlobTxType}, Types: []byte{types.BlobTxType},
Sizes: []uint32{uint32(tx.Size())}, Sizes: []uint32{uint32(tx.Size())},
Hashes: []common.Hash{tx.Hash()}, Hashes: []common.Hash{tx.Hash()},
Mask: types.CustodyBitmapAll,
} }
if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil { if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
errc <- fmt.Errorf("sending announcement failed: %v", err) errc <- fmt.Errorf("sending first announcement failed: %v", err)
return return
} }
@ -1311,3 +1333,292 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types
t.Fatalf("%v", err) t.Fatalf("%v", err)
} }
} }
func (s *Suite) TestBlobTxAvailabilityFailure(t *utesting.T) {
t.Log(`This test announces 4 blob txs from a single peer. With fetchProbability 0.15,
there will be at least one partial fetch (1-0.15^4). When only 1 peer announced availability,
partial fetch GetCells should never arrive. Any GetCells that does arrive must be a full fetch.`)
if err := s.engine.sendForkchoiceUpdated(); err != nil {
t.Fatalf("send fcu failed: %v", err)
}
txs, _ := s.makeBlobTxs(4, 4, 0x30)
conn, err := s.dial()
if err != nil {
t.Fatalf("dial failed: %v", err)
}
defer conn.Close()
if err := conn.peer(s.chain, nil); err != nil {
t.Fatalf("peering failed: %v", err)
}
// Announce all 4 txs from a single peer.
hashes := make([]common.Hash, len(txs))
txTypes := make([]byte, len(txs))
sizes := make([]uint32, len(txs))
for i, tx := range txs {
hashes[i] = tx.Hash()
txTypes[i] = types.BlobTxType
sizes[i] = uint32(tx.Size())
}
ann := eth.NewPooledTransactionHashesPacket72{
Types: txTypes,
Sizes: sizes,
Hashes: hashes,
Mask: types.CustodyBitmapAll,
}
if err := conn.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
t.Fatalf("announce failed: %v", err)
}
// Read messages for a short period. Any GetCells that arrives must be
// a full fetch request (mask >= DataPerBlob), not a partial fetch.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
for {
select {
case <-ctx.Done():
return
default:
}
msg, err := conn.ReadEth()
if err != nil {
if errors.Is(err, os.ErrDeadlineExceeded) {
return // timeout, test passed
}
t.Fatalf("unexpected error: %v", err)
}
switch req := msg.(type) {
case *eth.GetCellsRequestPacket:
if req.Mask.OneCount() < kzg4844.DataPerBlob {
t.Fatalf("received partial GetCells request with only %d cells from single peer announcement", req.Mask.OneCount())
}
case *eth.GetPooledTransactionsPacket:
encTxs, _ := rlp.EncodeToRawList(txs)
conn.Write(ethProto, eth.PooledTransactionsMsg, eth.PooledTransactionsPacket{
RequestId: req.RequestId,
List: encTxs,
})
}
}
}
// buildCells extracts cells at mask indices from the original tx's blobs
func buildCells(blobs []kzg4844.Blob, mask types.CustodyBitmap) []kzg4844.Cell {
allCells, _ := kzg4844.ComputeCells(blobs)
indices := mask.Indices()
result := make([]kzg4844.Cell, 0, len(blobs)*len(indices))
for b := 0; b < len(blobs); b++ {
for _, idx := range indices {
result = append(result, allCells[b*kzg4844.CellsPerBlob+int(idx)])
}
}
return result
}
// readAnyFrom waits for a message of type T on any of the given conns
// and returns the packet and the conn it came from.
func readAnyFrom[T any](conns ...*Conn) (*T, *Conn, error) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
type result struct {
pkt *T
c *Conn
}
ch := make(chan result, len(conns))
errCh := make(chan error, len(conns))
for _, c := range conns {
go func(c *Conn) {
pkt, err := readUntil[T](ctx, c)
if err != nil {
if !errors.Is(err, context.Canceled) {
errCh <- err
}
return
}
ch <- result{pkt, c}
}(c)
}
select {
case r := <-ch:
return r.pkt, r.c, nil
case err := <-errCh:
return nil, nil, err
}
}
func (s *Suite) TestGetCells(t *utesting.T) {
t.Log(`This test checks that blob tx announcements trigger GetCells requests,
and that providing valid cells causes the tx to enter the pool.`)
if err := s.engine.sendForkchoiceUpdated(); err != nil {
t.Fatalf("send fcu failed: %v", err)
}
txs, blobs := s.makeBlobTxs(1, 1, 0x31)
tx := txs[0]
blob := blobs[0]
// Two peers ensure GetCells arrives regardless of full/partial fetch path.
conn1, err := s.dial()
if err != nil {
t.Fatalf("dial failed: %v", err)
}
defer conn1.Close()
if err := conn1.peer(s.chain, nil); err != nil {
t.Fatalf("peering failed: %v", err)
}
conn2, err := s.dial()
if err != nil {
t.Fatalf("dial failed: %v", err)
}
defer conn2.Close()
if err := conn2.peer(s.chain, nil); err != nil {
t.Fatalf("peering failed: %v", err)
}
ann := eth.NewPooledTransactionHashesPacket72{
Types: []byte{types.BlobTxType},
Sizes: []uint32{uint32(tx.Size())},
Hashes: []common.Hash{tx.Hash()},
Mask: types.CustodyBitmapAll,
}
if err := conn1.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
t.Fatalf("conn1 announce failed: %v", err)
}
if err := conn2.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
t.Fatalf("conn2 announce failed: %v", err)
}
// Wait for GetPooledTransactions on either conn, respond with tx (without blobs).
pooledReq, pc, err := readAnyFrom[eth.GetPooledTransactionsPacket](conn1, conn2)
if err != nil {
t.Fatalf("failed to read GetPooledTransactions: %v", err)
}
encTxs, _ := rlp.EncodeToRawList([]*types.Transaction{tx})
resp := eth.PooledTransactionsPacket{RequestId: pooledReq.RequestId, List: encTxs}
if err := pc.Write(ethProto, eth.PooledTransactionsMsg, resp); err != nil {
t.Fatalf("writing pooled tx response failed: %v", err)
}
// Wait for GetCells request on either conn.
cellsReq, cc, err := readAnyFrom[eth.GetCellsRequestPacket](conn1, conn2)
if err != nil {
t.Fatalf("failed to read GetCells: %v", err)
}
if len(cellsReq.Hashes) == 0 || cellsReq.Hashes[0] != tx.Hash() {
t.Fatalf("GetCells for wrong hash: %v", cellsReq.Hashes)
}
// Respond with valid cells matching the requested mask.
cells := buildCells(blob, cellsReq.Mask)
cellsResp := eth.CellsPacket{
RequestId: cellsReq.RequestId,
CellsResponse: eth.CellsResponse{
Hashes: []common.Hash{tx.Hash()},
Cells: [][]kzg4844.Cell{cells},
Mask: cellsReq.Mask,
},
}
if err := cc.Write(ethProto, eth.CellsMsg, cellsResp); err != nil {
t.Fatalf("writing cells response failed: %v", err)
}
// Either peer should not be disconnected after providing valid data.
if readUntilDisconnect(cc) {
t.Fatalf("unexpected disconnect on cells-providing peer")
}
}
func (s *Suite) TestBlobTxWithInvalidCells(t *utesting.T) {
t.Log(`This test checks that a peer responding to GetCells with invalid cells is disconnected,
while the other peer is not.`)
if err := s.engine.sendForkchoiceUpdated(); err != nil {
t.Fatalf("send fcu failed: %v", err)
}
txs, blobs := s.makeBlobTxs(1, 1, 0x32)
tx := txs[0]
blob := blobs[0]
conn1, err := s.dial()
if err != nil {
t.Fatalf("dial failed: %v", err)
}
defer conn1.Close()
if err := conn1.peer(s.chain, nil); err != nil {
t.Fatalf("peering failed: %v", err)
}
conn2, err := s.dial()
if err != nil {
t.Fatalf("dial failed: %v", err)
}
defer conn2.Close()
if err := conn2.peer(s.chain, nil); err != nil {
t.Fatalf("peering failed: %v", err)
}
ann := eth.NewPooledTransactionHashesPacket72{
Types: []byte{types.BlobTxType},
Sizes: []uint32{uint32(tx.Size())},
Hashes: []common.Hash{tx.Hash()},
Mask: types.CustodyBitmapAll,
}
if err := conn1.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
t.Fatalf("conn1 announce failed: %v", err)
}
if err := conn2.Write(ethProto, eth.NewPooledTransactionHashesMsg, ann); err != nil {
t.Fatalf("conn2 announce failed: %v", err)
}
pooledReq, pc, err := readAnyFrom[eth.GetPooledTransactionsPacket](conn1, conn2)
if err != nil {
t.Fatalf("failed to read GetPooledTransactions: %v", err)
}
encTxs, _ := rlp.EncodeToRawList([]*types.Transaction{tx})
if err := pc.Write(ethProto, eth.PooledTransactionsMsg,
eth.PooledTransactionsPacket{RequestId: pooledReq.RequestId, List: encTxs}); err != nil {
t.Fatalf("writing pooled tx response failed: %v", err)
}
cellsReq, cc, err := readAnyFrom[eth.GetCellsRequestPacket](conn1, conn2)
if err != nil {
t.Fatalf("failed to read GetCells: %v", err)
}
// Respond with corrupted cells (all zero bytes).
blobCount := len(blob)
corrupted := make([]kzg4844.Cell, blobCount*cellsReq.Mask.OneCount())
badResp := eth.CellsPacket{
RequestId: cellsReq.RequestId,
CellsResponse: eth.CellsResponse{
Hashes: []common.Hash{tx.Hash()},
Cells: [][]kzg4844.Cell{corrupted},
Mask: cellsReq.Mask,
},
}
if err := cc.Write(ethProto, eth.CellsMsg, badResp); err != nil {
t.Fatalf("writing bad cells response failed: %v", err)
}
// The peer that sent corrupted cells must be disconnected.
if !readUntilDisconnect(cc) {
t.Fatalf("expected peer to be disconnected after invalid cells")
}
// The innocent peer must stay connected.
otherConn := conn1
if cc == conn1 {
otherConn = conn2
}
if readUntilDisconnect(otherConn) {
t.Fatalf("innocent peer should not be disconnected")
}
}

Binary file not shown.

View file

@ -4,6 +4,7 @@
"HIVE_CANCUN_BLOB_TARGET": "3", "HIVE_CANCUN_BLOB_TARGET": "3",
"HIVE_CANCUN_TIMESTAMP": "60", "HIVE_CANCUN_TIMESTAMP": "60",
"HIVE_CHAIN_ID": "3503995874084926", "HIVE_CHAIN_ID": "3503995874084926",
"HIVE_DEPOSIT_CONTRACT_ADDRESS": "0x0000000000000000000000000000000000000000",
"HIVE_FORK_ARROW_GLACIER": "0", "HIVE_FORK_ARROW_GLACIER": "0",
"HIVE_FORK_BERLIN": "0", "HIVE_FORK_BERLIN": "0",
"HIVE_FORK_BYZANTIUM": "0", "HIVE_FORK_BYZANTIUM": "0",

View file

@ -18,6 +18,7 @@
"shanghaiTime": 0, "shanghaiTime": 0,
"cancunTime": 60, "cancunTime": 60,
"pragueTime": 120, "pragueTime": 120,
"osakaTime": 180,
"terminalTotalDifficulty": 131072, "terminalTotalDifficulty": 131072,
"depositContractAddress": "0x0000000000000000000000000000000000000000", "depositContractAddress": "0x0000000000000000000000000000000000000000",
"ethash": {}, "ethash": {},
@ -31,13 +32,18 @@
"target": 6, "target": 6,
"max": 9, "max": 9,
"baseFeeUpdateFraction": 5007716 "baseFeeUpdateFraction": 5007716
},
"osaka": {
"target": 6,
"max": 9,
"baseFeeUpdateFraction": 5007716
} }
} }
}, },
"nonce": "0x0", "nonce": "0x0",
"timestamp": "0x0", "timestamp": "0x0",
"extraData": "0x68697665636861696e", "extraData": "0x68697665636861696e",
"gasLimit": "0x11e1a300", "gasLimit": "0x5f5e100",
"difficulty": "0x20000", "difficulty": "0x20000",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000", "coinbase": "0x0000000000000000000000000000000000000000",
@ -141,5 +147,6 @@
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"baseFeePerGas": "0x3b9aca00", "baseFeePerGas": "0x3b9aca00",
"excessBlobGas": null, "excessBlobGas": null,
"blobGasUsed": null "blobGasUsed": null,
"slotNumber": null
} }

View file

@ -1,14 +1,14 @@
{ {
"parentHash": "0x7e80093a491eba0e5b2c1895837902f64f514100221801318fe391e1e09c96a6", "parentHash": "0xc1c57b56e4bcda7b97cb6e2c3a8be7f1bb5212973718d692641ee6957722468e",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"miner": "0x0000000000000000000000000000000000000000", "miner": "0x0000000000000000000000000000000000000000",
"stateRoot": "0x8fcfb02cfca007773bd55bc1c3e50a3c8612a59c87ce057e5957e8bf17c1728b", "stateRoot": "0x8b65aad07dfb184a5977da83d1cc2101d35edd6e8d0e8b7bb2711be3b2c163d8",
"transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"difficulty": "0x0", "difficulty": "0x0",
"number": "0x258", "number": "0x258",
"gasLimit": "0x11e1a300", "gasLimit": "0x5f5e100",
"gasUsed": "0x0", "gasUsed": "0x0",
"timestamp": "0x1770", "timestamp": "0x1770",
"extraData": "0x", "extraData": "0x",
@ -20,5 +20,6 @@
"excessBlobGas": "0x0", "excessBlobGas": "0x0",
"parentBeaconBlockRoot": "0xf5003fc8f92358e790a114bce93ce1d9c283c85e1787f8d7d56714d3489b49e6", "parentBeaconBlockRoot": "0xf5003fc8f92358e790a114bce93ce1d9c283c85e1787f8d7d56714d3489b49e6",
"requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"hash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a" "slotNumber": null,
"hash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09"
} }

View file

@ -4,9 +4,9 @@
"method": "engine_forkchoiceUpdatedV3", "method": "engine_forkchoiceUpdatedV3",
"params": [ "params": [
{ {
"headBlockHash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a", "headBlockHash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09",
"safeBlockHash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a", "safeBlockHash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09",
"finalizedBlockHash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a" "finalizedBlockHash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09"
}, },
null null
] ]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -17,11 +17,11 @@
"tx-eip7702": { "tx-eip7702": {
"account": "0xeda8645ba6948855e3b3cd596bbb07596d59c603", "account": "0xeda8645ba6948855e3b3cd596bbb07596d59c603",
"proxyAddr": "0x4dc5e971f8b11ace4f21d40b0ede74a07940f356", "proxyAddr": "0x4dc5e971f8b11ace4f21d40b0ede74a07940f356",
"authorizeTx": "0x64c47531984e4099548b480b5af0bce04ab89d29f2fe7ae36e97ba68d688539f" "authorizeTx": "0xa9f4f425f5ce4ed0d9d8d30a58cf206f1d3a3f0da7ad96134576e92540cc0d78"
}, },
"tx-emit-eip1559": [ "tx-emit-eip1559": [
{ {
"txhash": "0xf3b025eaf924500c53bc0b9f0f5733d56b59b7dade1df336ba8b39c83d92ac57", "txhash": "0xd4266bd85926a3521918c6e492cfc71264b881b8674c2e6f83d7422b3688b894",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x7", "block": "0x7",
"indexInBlock": 0, "indexInBlock": 0,
@ -29,7 +29,7 @@
"logtopic1": "0x3569f740e521d8bb11c5b72660dc96272ad66bfd811ed918c3a9e02acd4ade8f" "logtopic1": "0x3569f740e521d8bb11c5b72660dc96272ad66bfd811ed918c3a9e02acd4ade8f"
}, },
{ {
"txhash": "0x62da81199d8ac0d4231dec90c6085f1153a58eba6bbac04f71da4d5076c9b7c1", "txhash": "0x0b2a5113ce4638598727c4c96441c0dd52b637c1f5f2846ac6ec15089c31dd26",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x15", "block": "0x15",
"indexInBlock": 0, "indexInBlock": 0,
@ -37,7 +37,7 @@
"logtopic1": "0xf77c749ecb156f605e2334b14caea388100bed09b4c16579c952a96e90355629" "logtopic1": "0xf77c749ecb156f605e2334b14caea388100bed09b4c16579c952a96e90355629"
}, },
{ {
"txhash": "0x7a8c4456e604a0b6210f29c6e68405d2ca5425fdce16bfcc6dfa3d3904b32740", "txhash": "0x61640b02a288372b365c490669125c7ca739aef03798943785d512e454c80284",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x20", "block": "0x20",
"indexInBlock": 0, "indexInBlock": 0,
@ -45,7 +45,7 @@
"logtopic1": "0x415feb809041baabc4d9246223e40f1083963cbe1ef6dedb8b153e49d02ee7ce" "logtopic1": "0x415feb809041baabc4d9246223e40f1083963cbe1ef6dedb8b153e49d02ee7ce"
}, },
{ {
"txhash": "0xd1ec37a2bc2841c4dc61de447dbe772a92284714f03752bc4056eafdac74dc21", "txhash": "0xf3a593ff76f4517239d0d721aa0b734b5bc4242e804ec92984539ddab1d88c37",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x2b", "block": "0x2b",
"indexInBlock": 0, "indexInBlock": 0,
@ -53,7 +53,7 @@
"logtopic1": "0x89c17d9392b73a55738ba19aae192f2f9c5612dc8bd803ca23b9c2fb9c309e56" "logtopic1": "0x89c17d9392b73a55738ba19aae192f2f9c5612dc8bd803ca23b9c2fb9c309e56"
}, },
{ {
"txhash": "0xa2b3275339e1237e6918726e0f0906f7f6a70a069cfefd3d9b5380e67c2d1305", "txhash": "0x9197e83d462cdd6e5df80f38c93f0e04a95f9b0b2db05d2f2774cedbc42a888e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x36", "block": "0x36",
"indexInBlock": 0, "indexInBlock": 0,
@ -61,7 +61,7 @@
"logtopic1": "0x9038344c39b01167bfa8e99a6425d34bca24c27ceb191e8eba70ab5a8f719ce5" "logtopic1": "0x9038344c39b01167bfa8e99a6425d34bca24c27ceb191e8eba70ab5a8f719ce5"
}, },
{ {
"txhash": "0xc488c5aac30312fe8365ed78fc6cc12b531f9cfc315e6ec5ddb610eac4a48976", "txhash": "0x258f7587ac5c4e8df9d1c15dae6362132eac6c9678cb135ea884a7bf4fc4bb48",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x41", "block": "0x41",
"indexInBlock": 0, "indexInBlock": 0,
@ -69,7 +69,7 @@
"logtopic1": "0x4b3120af8064823e074758c51cd6cd0954587c0d94b5b37b336261fc7aa2ddb3" "logtopic1": "0x4b3120af8064823e074758c51cd6cd0954587c0d94b5b37b336261fc7aa2ddb3"
}, },
{ {
"txhash": "0x7b43a1b8ddf97ab6ccb6dcab385184d1a1165e3c221013b34448223af45fc832", "txhash": "0x243b74358431651d598b6bb5dcdf7d9d1d034528bed76cfcbde96adf2fb4c995",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x4c", "block": "0x4c",
"indexInBlock": 0, "indexInBlock": 0,
@ -77,7 +77,7 @@
"logtopic1": "0x41565ae6f06f2555139f444c467d6b709b45180aa0c6b15bb5b1388d55ef952c" "logtopic1": "0x41565ae6f06f2555139f444c467d6b709b45180aa0c6b15bb5b1388d55ef952c"
}, },
{ {
"txhash": "0x8954b9d95e7b0d5a726847449419e1cf77a624199e1fa89f311150dcac94b361", "txhash": "0x0fb1554b5657396671c88eb70d62f1555d4f268fc1f8cb08a5afbe47f24ad1c3",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x57", "block": "0x57",
"indexInBlock": 0, "indexInBlock": 0,
@ -85,7 +85,7 @@
"logtopic1": "0x00f7ca033c24d91f8fc39cbf0edc8a43192507f93d7316f311b05eeb85921eed" "logtopic1": "0x00f7ca033c24d91f8fc39cbf0edc8a43192507f93d7316f311b05eeb85921eed"
}, },
{ {
"txhash": "0xb2749d3a5d1b062f85a4717ee5c3a7a686a31b03ecb2e29394bce32e15bc3e08", "txhash": "0xc976b09bc8c092788dba199637de6d3749e87a34d39e349588fdc62bdbc6ad90",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x62", "block": "0x62",
"indexInBlock": 0, "indexInBlock": 0,
@ -93,7 +93,7 @@
"logtopic1": "0x761bf5fb1730fee0e499bb1806b9ae14394e673ab9c1dc12e95b9d3f1647cecd" "logtopic1": "0x761bf5fb1730fee0e499bb1806b9ae14394e673ab9c1dc12e95b9d3f1647cecd"
}, },
{ {
"txhash": "0xbf2b014779d3872e5b8ae89c65d8c5912e60c459698f44f7281b9f14e03e6baf", "txhash": "0x4fdc7d1c163e836542e16c678e3d1adf2ec1d1411feac7eb93139b0181dfcef7",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x6d", "block": "0x6d",
"indexInBlock": 0, "indexInBlock": 0,
@ -101,7 +101,7 @@
"logtopic1": "0x468eae0ffdb87a4dc081a86c494969801637f690e1e1da15fb4a9d2c78272da8" "logtopic1": "0x468eae0ffdb87a4dc081a86c494969801637f690e1e1da15fb4a9d2c78272da8"
}, },
{ {
"txhash": "0x1d27789de7fbb33eed6c51da18ee5e7cef7b242046842d42141cfb7f4893b624", "txhash": "0xf1acc26aefda71041e3eaa74484ba9ca857ae1e0b08df6771c689e9ef9fe09ae",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x78", "block": "0x78",
"indexInBlock": 0, "indexInBlock": 0,
@ -109,7 +109,7 @@
"logtopic1": "0xdbc7a073eb54d33d8e6dec5b0b635a874204bda1c23234ff0cca057ff8ed77f5" "logtopic1": "0xdbc7a073eb54d33d8e6dec5b0b635a874204bda1c23234ff0cca057ff8ed77f5"
}, },
{ {
"txhash": "0x48369062975939c17e4695b1368d7a1e0b60d145662ef9ba0b7a61b39c3f2cd4", "txhash": "0xe13b0234e1257800d844c0580a9f1cf1752be7a664597fc02963470f5da8aa58",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x83", "block": "0x83",
"indexInBlock": 0, "indexInBlock": 0,
@ -117,7 +117,7 @@
"logtopic1": "0x7a9cae3647128ba14914f547c5f27444cd7325bbc37e5038abc31eea45003034" "logtopic1": "0x7a9cae3647128ba14914f547c5f27444cd7325bbc37e5038abc31eea45003034"
}, },
{ {
"txhash": "0x7fbb4a27af5e59af9219a6b3a63b8cedf03d7f4601f1cdb83d12ccb85f50d315", "txhash": "0xf8ad019fc3453ddf04e81cebba81c0a907f99503558b76af5001093aa07ba258",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x8e", "block": "0x8e",
"indexInBlock": 0, "indexInBlock": 0,
@ -455,7 +455,7 @@
], ],
"tx-emit-eip2930": [ "tx-emit-eip2930": [
{ {
"txhash": "0x76f2917dfdad8c636c493258e672938661a2044a05338bbad7d14d446fae4d45", "txhash": "0xbc3032e0d2a47879f46ae9b732fed10707ace1b240537f7a096fc23192def461",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x8", "block": "0x8",
"indexInBlock": 0, "indexInBlock": 0,
@ -463,7 +463,7 @@
"logtopic1": "0x881a8434f98b103a2ee48727304618ca54234f1474c44bef70c21accc4dbc0a7" "logtopic1": "0x881a8434f98b103a2ee48727304618ca54234f1474c44bef70c21accc4dbc0a7"
}, },
{ {
"txhash": "0x30fc0bab35d4325db37b953f5cf26e05f2c18593adcc4c14c8fb04b28501c2c5", "txhash": "0x82d6115a8bbeeb9db51bfb660f1876cc8a0ee29944b7c61aa1bbb5ceda8d5ff7",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x16", "block": "0x16",
"indexInBlock": 0, "indexInBlock": 0,
@ -471,7 +471,7 @@
"logtopic1": "0xa41cb4f2ab2731a8889754ae1a340c666cb8107b497b922073df80a9b255e31b" "logtopic1": "0xa41cb4f2ab2731a8889754ae1a340c666cb8107b497b922073df80a9b255e31b"
}, },
{ {
"txhash": "0x07032ce65db029864ffcc0c3b00ed01bf8fa90da0efc781ecf517920ed762304", "txhash": "0x0b75c2ab2ba045d613d83f4d48ea6e286a31f41b56e6e841e97af2d0f21e5aeb",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x21", "block": "0x21",
"indexInBlock": 0, "indexInBlock": 0,
@ -479,7 +479,7 @@
"logtopic1": "0xb2416e7ca12669406e6cd5154ad5177841b7d0cddeb2760249c28e1aa151f970" "logtopic1": "0xb2416e7ca12669406e6cd5154ad5177841b7d0cddeb2760249c28e1aa151f970"
}, },
{ {
"txhash": "0x61a1ed16c11ef4e4c8036233b87753a761be8c9702514ea87fe3bc82345bbbba", "txhash": "0xa0fd707e7ec4a17c09bf0652b4dd2800aa759241b95d4ae14af954c0a0348ebb",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x2c", "block": "0x2c",
"indexInBlock": 0, "indexInBlock": 0,
@ -487,7 +487,7 @@
"logtopic1": "0x11f0a8ac2adda075c95bbf6be534e3254dafa759f62cbcf0e91bc6f0335e70aa" "logtopic1": "0x11f0a8ac2adda075c95bbf6be534e3254dafa759f62cbcf0e91bc6f0335e70aa"
}, },
{ {
"txhash": "0x24a0922096c6a6b34668d0b06c4724db961e3b4c462c6fa645517e19293ec37d", "txhash": "0x79a667af722e7558a947286047c69df7b3e8844f1ef37512e3ed2a642d7c6579",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x37", "block": "0x37",
"indexInBlock": 0, "indexInBlock": 0,
@ -495,7 +495,7 @@
"logtopic1": "0x8460e232c64e6cd9f816c02d855c892755984ebbb91592e683cda80aaba4ba22" "logtopic1": "0x8460e232c64e6cd9f816c02d855c892755984ebbb91592e683cda80aaba4ba22"
}, },
{ {
"txhash": "0xdce27eb7c2b44d61022e4923329a9b18f0dd26093a88dd5efa772fb1e52273eb", "txhash": "0x727925c9a811cbcf1d005949ccec1a75391c7ee32513080a4d60789af589519e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x42", "block": "0x42",
"indexInBlock": 0, "indexInBlock": 0,
@ -503,7 +503,7 @@
"logtopic1": "0xe7d55978188f31ab090b1f10d8d401a66356b11ca8c296384a0a51e36e6ec11f" "logtopic1": "0xe7d55978188f31ab090b1f10d8d401a66356b11ca8c296384a0a51e36e6ec11f"
}, },
{ {
"txhash": "0xdb7b8df9ad36fc8838cfedc2f272cb205f7ad2860cd1f5ac5e739ed1a2973784", "txhash": "0x23660f1fb5a9f1da011df2e52a020854546433daa90df0ee8fd96c2e5180864e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x4d", "block": "0x4d",
"indexInBlock": 0, "indexInBlock": 0,
@ -511,7 +511,7 @@
"logtopic1": "0x24a4daf5b3cac3bf3066902cda09da0fc862e0a6723c47981ed601782ad69079" "logtopic1": "0x24a4daf5b3cac3bf3066902cda09da0fc862e0a6723c47981ed601782ad69079"
}, },
{ {
"txhash": "0xccf0da8511e51af458ef4ea7734531fdb77b8bb48191efd8257cf6ac16daf3f6", "txhash": "0x93f5c00dce3a4fe9c5f52f133e58ba0175a68c8834bc6b5b69ee82edb284e235",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x58", "block": "0x58",
"indexInBlock": 0, "indexInBlock": 0,
@ -519,7 +519,7 @@
"logtopic1": "0x7c24a68c92e3b68daa153ae82eff9be1ebbab973384e0f4b256f158f93c5d525" "logtopic1": "0x7c24a68c92e3b68daa153ae82eff9be1ebbab973384e0f4b256f158f93c5d525"
}, },
{ {
"txhash": "0xe78dac2c962f7fc240909233c4b8bbf47a6ab4b9b3b4376ecb92a4eff4e147e3", "txhash": "0x99fb0fa518cd6f627acb5d3a0315e0bfb2b1240c86a183ce3e88d495ccecd996",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x63", "block": "0x63",
"indexInBlock": 0, "indexInBlock": 0,
@ -527,7 +527,7 @@
"logtopic1": "0x02bd9d62880450596e11c3417f2644a81f7cc233a05394bbbfb58428ed53f413" "logtopic1": "0x02bd9d62880450596e11c3417f2644a81f7cc233a05394bbbfb58428ed53f413"
}, },
{ {
"txhash": "0x2e5f23d99b09fd9f81f2fc7013c3a3bd8fea88bd6df07e2b236addad4e9e94e3", "txhash": "0xf9435fd557962bd981ceab8bf73f461911cfb6c6f858ae0f24a073dccc7e5b4a",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x6e", "block": "0x6e",
"indexInBlock": 0, "indexInBlock": 0,
@ -535,7 +535,7 @@
"logtopic1": "0x0dcf6219856f226889a2440b388d8e15f5df0eb64a7b443f3a7a5dca7b87b0f2" "logtopic1": "0x0dcf6219856f226889a2440b388d8e15f5df0eb64a7b443f3a7a5dca7b87b0f2"
}, },
{ {
"txhash": "0x37010507fe716034116f8d945ce2bc35d3718e7c10b78ba34e4ddf780c6063b7", "txhash": "0xfb39aee0fb8ca73b28c536b84611084acefa97131136506677fee0028c5529a6",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x79", "block": "0x79",
"indexInBlock": 0, "indexInBlock": 0,
@ -543,7 +543,7 @@
"logtopic1": "0x0f624930606bfcd2386d583abca6ab10227d71fc1633fea53f94bd146c152b8f" "logtopic1": "0x0f624930606bfcd2386d583abca6ab10227d71fc1633fea53f94bd146c152b8f"
}, },
{ {
"txhash": "0xb53c6450c32830cf3be18b73370a0a290a12acbf4d734e8ad8785d4742e8035b", "txhash": "0xc6241637f7a892ef60a47f0022820001b7d8844408f1dee98280e1b45ab7e5c2",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x84", "block": "0x84",
"indexInBlock": 0, "indexInBlock": 0,
@ -551,7 +551,7 @@
"logtopic1": "0x2daaea9286d7edb7568e0803a61bfdb1e1506156d27e93bdf1942564850646c6" "logtopic1": "0x2daaea9286d7edb7568e0803a61bfdb1e1506156d27e93bdf1942564850646c6"
}, },
{ {
"txhash": "0xb7b48df8d1f44601669025fe789c2a5aa05771ac35ea90bd5bef536ecdc4f92e", "txhash": "0x6d595d077cd7ef22edcdebac2c6c88884d78c06bd126cfd0386b429920eafd5c",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x8f", "block": "0x8f",
"indexInBlock": 0, "indexInBlock": 0,
@ -889,7 +889,7 @@
], ],
"tx-emit-eip4844": [ "tx-emit-eip4844": [
{ {
"txhash": "0x87630fa02828dad0c04ea8ffa7c5d742bf6aac72b8b0a7e72a28b7d2c0d9fdcb", "txhash": "0xcae406f12b53284afa64066b311c5a2dda207a29d613a13cdb40e69972b7c807",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x9", "block": "0x9",
"indexInBlock": 0, "indexInBlock": 0,
@ -897,7 +897,7 @@
"logtopic1": "0x63cde520fb894276a981d2c9099bef9beb949121c1be98f3abe1b721d880899f" "logtopic1": "0x63cde520fb894276a981d2c9099bef9beb949121c1be98f3abe1b721d880899f"
}, },
{ {
"txhash": "0x75118b029d56f192f458f1a2a38238237f19c11f88eb97e0483ad26e021ae15c", "txhash": "0xe5a5c803e6d246a5e3be1ec77345e229ab5002f19fba46e77ebbe2e532f1568e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x17", "block": "0x17",
"indexInBlock": 0, "indexInBlock": 0,
@ -905,7 +905,7 @@
"logtopic1": "0xa6d01173df2aa437fb0118d181e64a8f8e05713fc01c42fbfd2250516639ae95" "logtopic1": "0xa6d01173df2aa437fb0118d181e64a8f8e05713fc01c42fbfd2250516639ae95"
}, },
{ {
"txhash": "0x99ba6434209d91a5fc015f2c409877d1d5567287b64c46f984a64d631efa866d", "txhash": "0x71c6f6ae274695f1f7c4cee7a0811766a98c1f418984da6cf149881fe882183a",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x22", "block": "0x22",
"indexInBlock": 0, "indexInBlock": 0,
@ -913,7 +913,7 @@
"logtopic1": "0xe94d0b2545ec05c3ce3431c4d45c3b62fcab156563e8308fae1ebd27a2810c1a" "logtopic1": "0xe94d0b2545ec05c3ce3431c4d45c3b62fcab156563e8308fae1ebd27a2810c1a"
}, },
{ {
"txhash": "0x17eced62882b234b280f2382a0225cf218e59de7124bd3d7214fa2da8b766188", "txhash": "0x3510d0abeb988608581261e85d509ac14c63039ed361106eb59a397facf2dd6e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x2d", "block": "0x2d",
"indexInBlock": 0, "indexInBlock": 0,
@ -921,7 +921,7 @@
"logtopic1": "0x6551251b96ca27f3af8a2c500d6dd1ea5b9ab7002b3d923b66db0493f4a7123e" "logtopic1": "0x6551251b96ca27f3af8a2c500d6dd1ea5b9ab7002b3d923b66db0493f4a7123e"
}, },
{ {
"txhash": "0x7cb0ae261889695ae99b64ea9f6468a493f87b90fccf073761daf15ed4868551", "txhash": "0x1898080cc14ae4c05be4a395ebfeda29658c72a537ee2f2f812d9c735b803ef9",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x38", "block": "0x38",
"indexInBlock": 0, "indexInBlock": 0,
@ -929,7 +929,7 @@
"logtopic1": "0xfa29cff134420b6526f434ab690a9c3a140aa27b8479ae3d8d83b6c799acbc23" "logtopic1": "0xfa29cff134420b6526f434ab690a9c3a140aa27b8479ae3d8d83b6c799acbc23"
}, },
{ {
"txhash": "0x0e6a717775a24331736f5062466715bdf7a4330466d235a6ffdd746bffdcaf22", "txhash": "0xc8517fc20db042e5211607716c47aea078ab33262243858499c3a1755dc3e5d3",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x43", "block": "0x43",
"indexInBlock": 0, "indexInBlock": 0,
@ -937,7 +937,7 @@
"logtopic1": "0x412379b7f583981ea6e84408cba75ced69039e07ce9cdaa32a8a9dac997aaafb" "logtopic1": "0x412379b7f583981ea6e84408cba75ced69039e07ce9cdaa32a8a9dac997aaafb"
}, },
{ {
"txhash": "0x153401e45e9053f121b36d88763bec1515553e3762cf5f46f4d125b0700435aa", "txhash": "0xf817888429c51c567d479b9bd40bbdfb93fd2ee61b19bddc52551c22fb71fae5",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x4e", "block": "0x4e",
"indexInBlock": 0, "indexInBlock": 0,
@ -945,7 +945,7 @@
"logtopic1": "0x87dfa85154edde1626e3a09196eab4b60f71887ec7b50ccbbe7ec76c0be6bdff" "logtopic1": "0x87dfa85154edde1626e3a09196eab4b60f71887ec7b50ccbbe7ec76c0be6bdff"
}, },
{ {
"txhash": "0x559f888f55e908ab3f4ad9d0d693b4d572ff9ab00058df6465cfe43722b45c6b", "txhash": "0xa2517a73503fe83df09db0aab07a4ee54eb0ed86b4b0436093417286a4148d12",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x59", "block": "0x59",
"indexInBlock": 0, "indexInBlock": 0,
@ -953,7 +953,7 @@
"logtopic1": "0x6e2466f20ef20cb42d216dbf4a0d934199213e9b8d75bedc9c2d3e038a587474" "logtopic1": "0x6e2466f20ef20cb42d216dbf4a0d934199213e9b8d75bedc9c2d3e038a587474"
}, },
{ {
"txhash": "0x154de45bd8bde35c256318cb983131890d97b99689ec175d686cf88742e82fc9", "txhash": "0x4f12711cb97d5d47be629e8b3f0738db8f16ae39461b6b9184108d20a0057153",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x64", "block": "0x64",
"indexInBlock": 0, "indexInBlock": 0,
@ -961,7 +961,7 @@
"logtopic1": "0xd75c9abb1414054ca164bba2f8c09917fb90c24789feaa311ee34a0b3f4a82f0" "logtopic1": "0xd75c9abb1414054ca164bba2f8c09917fb90c24789feaa311ee34a0b3f4a82f0"
}, },
{ {
"txhash": "0x67de1da7ebd65a1f384814112a0ef54c3907e09e18c8a0ddb5630a6883a3c8c5", "txhash": "0x663ef65cec978da040307899739da355fac5aad6feb15df48becb3f64033f76f",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x6f", "block": "0x6f",
"indexInBlock": 0, "indexInBlock": 0,
@ -969,7 +969,7 @@
"logtopic1": "0x165e0e0cc13ca53c5af4860637550364c5c90a512906490ace14efb534873741" "logtopic1": "0x165e0e0cc13ca53c5af4860637550364c5c90a512906490ace14efb534873741"
}, },
{ {
"txhash": "0x478293db9f43eec14e4a54b90cb07d6e85e72095f3729d3f80c7e8c33f4b0f19", "txhash": "0xbda32538691913b1eae9a4d22083143adadbfd374d15cd5393c39e1d27137a73",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x7a", "block": "0x7a",
"indexInBlock": 0, "indexInBlock": 0,
@ -977,7 +977,7 @@
"logtopic1": "0x16bee816935475cd45501fc5fd01bf913f8ef54330a43d80ef73101a4c728b34" "logtopic1": "0x16bee816935475cd45501fc5fd01bf913f8ef54330a43d80ef73101a4c728b34"
}, },
{ {
"txhash": "0xd229b870b66a6c5452c88260ef6cc32ffa71363ddbd09340ccda6173af447fc8", "txhash": "0xc35946ee7a8d5c109f7925c287ed8b54819d58d09f7aec26e3516f0ac171ec9d",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x85", "block": "0x85",
"indexInBlock": 0, "indexInBlock": 0,
@ -985,7 +985,7 @@
"logtopic1": "0xaf1f0d50933e49dd24b61a24c670809a5b875e3b746862636288dead8579dc4e" "logtopic1": "0xaf1f0d50933e49dd24b61a24c670809a5b875e3b746862636288dead8579dc4e"
}, },
{ {
"txhash": "0xf70f764ffe1e6c2e2770eab2bf0137ece8563607cc3578c8b0e76709105553fd", "txhash": "0x57c99e9085ec43eb172d51b4d535fb50529ce90dba93a2dd984d6e6b235b8f73",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x90", "block": "0x90",
"indexInBlock": 0, "indexInBlock": 0,
@ -1323,7 +1323,7 @@
], ],
"tx-emit-legacy": [ "tx-emit-legacy": [
{ {
"txhash": "0xc9e729de373f2fcab9ddd36402c047cb117522ef48b25435bd415b9bcf794bb9", "txhash": "0x7dcf6bcb38bc1286315b603160051cb35d0a582df99ac8901c6dd640aa14b0e9",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0xa", "block": "0xa",
"indexInBlock": 0, "indexInBlock": 0,
@ -1331,7 +1331,7 @@
"logtopic1": "0xb8d28e7b703baf999848ecbba44026cb6479b3f0466037bcf2221ffc3f8549f9" "logtopic1": "0xb8d28e7b703baf999848ecbba44026cb6479b3f0466037bcf2221ffc3f8549f9"
}, },
{ {
"txhash": "0xfe4c001ff9cbf3338dd712296b718a6467ab734617ca225ce7ce4e3a9547be41", "txhash": "0x90929818f801394ad3c7d13d2bcb9409442f04e569ebc049825686c37c12cbe2",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x18", "block": "0x18",
"indexInBlock": 0, "indexInBlock": 0,
@ -1339,7 +1339,7 @@
"logtopic1": "0x95104e47e1982aba633477f377b1511396c3fe83600224bcb0c78949be705b33" "logtopic1": "0x95104e47e1982aba633477f377b1511396c3fe83600224bcb0c78949be705b33"
}, },
{ {
"txhash": "0xffde4e2fd1cd2f5e51e564b08216b9ef80978c586098221cd19f1ee742db1c03", "txhash": "0xe78692021910fd245d1829d0a9417a24812988f6eccc0ec010faa282d613fd5d",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x23", "block": "0x23",
"indexInBlock": 0, "indexInBlock": 0,
@ -1347,7 +1347,7 @@
"logtopic1": "0x2acfc92a1cc51397c95e434631e449d83a81de91964ed735a8c8b71b35e1a626" "logtopic1": "0x2acfc92a1cc51397c95e434631e449d83a81de91964ed735a8c8b71b35e1a626"
}, },
{ {
"txhash": "0x965195498584dd1a4ee5ae7d60e5bb4dae1254b2ec61023f7b86678d75643201", "txhash": "0x3b54d3d700d5822e6c67ac0f5e4340168864808dd28f7802cedfff4c85eaf9f5",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x2e", "block": "0x2e",
"indexInBlock": 0, "indexInBlock": 0,
@ -1355,7 +1355,7 @@
"logtopic1": "0x5b6618f0def0d634d51118d232eadc26ecbc8d54a7efaa225afc472f0a611c69" "logtopic1": "0x5b6618f0def0d634d51118d232eadc26ecbc8d54a7efaa225afc472f0a611c69"
}, },
{ {
"txhash": "0x209a012091e6c869a96f21aee8de7ca44490a593d0f619793b961b2ba4a777b2", "txhash": "0xd153e85fb648f702df7ae218351dd0769a49b2fc48dafd7bd6f4aff55c701c80",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x39", "block": "0x39",
"indexInBlock": 0, "indexInBlock": 0,
@ -1363,7 +1363,7 @@
"logtopic1": "0x349c26db328204bd2527eb45003b0039d5a636f76c8849bca0b34e8fb134f505" "logtopic1": "0x349c26db328204bd2527eb45003b0039d5a636f76c8849bca0b34e8fb134f505"
}, },
{ {
"txhash": "0xff76c1915b804a9f4abe17f6564b278ece6cbc1ceca0604856e7f440cbd6d9c8", "txhash": "0x50ad4ad01002d9962c6b65c8e161c6bf70ebbad6ca1bd4066dfaa099bc8fb159",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x44", "block": "0x44",
"indexInBlock": 0, "indexInBlock": 0,
@ -1371,7 +1371,7 @@
"logtopic1": "0xb66fab7dddd4d16174b227a6f958d7ba2ae8ebc52d763b02c1ff944362755e6e" "logtopic1": "0xb66fab7dddd4d16174b227a6f958d7ba2ae8ebc52d763b02c1ff944362755e6e"
}, },
{ {
"txhash": "0x39c37d8cff047f5047fce694f7703b57b08cf414710cd94fc3daea5902330559", "txhash": "0xca23bea2a2bf588eea4377dd466cf398ac58a788eca44b49701cc1d7e5d7c55d",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x4f", "block": "0x4f",
"indexInBlock": 0, "indexInBlock": 0,
@ -1379,7 +1379,7 @@
"logtopic1": "0x780fa5a814a83baf682b2f170be956308be6ce1bf84ce68ca5f3c59cc41c7c28" "logtopic1": "0x780fa5a814a83baf682b2f170be956308be6ce1bf84ce68ca5f3c59cc41c7c28"
}, },
{ {
"txhash": "0xbac05861c5a87e48448ca240659aca18658a08c7d0b6318690809b0c227470db", "txhash": "0xfae95cd71c7a11c43addca3cc1defb2ca7fc42874fbc2b19563267fc13a0b3f5",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x5a", "block": "0x5a",
"indexInBlock": 0, "indexInBlock": 0,
@ -1387,7 +1387,7 @@
"logtopic1": "0x9739ae7192ad23a41778719941582886701a0830589c7ebfc5db094037635d82" "logtopic1": "0x9739ae7192ad23a41778719941582886701a0830589c7ebfc5db094037635d82"
}, },
{ {
"txhash": "0xf24d6d5bd0136dc7a2cf4ce1e2daf6191b415af43dcaf6bf1d43ae69ef2a306c", "txhash": "0x3de36576222ddf2638908d967047eb6f2ee53ddb914d048963e369eeee787f41",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x65", "block": "0x65",
"indexInBlock": 0, "indexInBlock": 0,
@ -1395,7 +1395,7 @@
"logtopic1": "0xf5acd98a17a3425f113b869e0dd03f82ee696401d2e7f59e8902610150a95a20" "logtopic1": "0xf5acd98a17a3425f113b869e0dd03f82ee696401d2e7f59e8902610150a95a20"
}, },
{ {
"txhash": "0xaf83fcaed15f46e777c9cb54fd6465f00dd6d929c556bd5644ea39fce51be74a", "txhash": "0xfcb2270a6f82e1c8af14f5bd12fb9dd37f4b5ea9cf40afb80ec4f2028231e31a",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x70", "block": "0x70",
"indexInBlock": 0, "indexInBlock": 0,
@ -1403,7 +1403,7 @@
"logtopic1": "0x5cd04d660080fb51a0cc8df0d716e1bff4eff98c887cf3274aabe7ec53dc3615" "logtopic1": "0x5cd04d660080fb51a0cc8df0d716e1bff4eff98c887cf3274aabe7ec53dc3615"
}, },
{ {
"txhash": "0x7382ad28a6589fcf07195874a36eee8f4c5fa22f3288a1979b374d80e7c6fb88", "txhash": "0x60459b81f4643d1658d49b39dcf461115315623564c2f9933786ea93e7dc6dd9",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x7b", "block": "0x7b",
"indexInBlock": 0, "indexInBlock": 0,
@ -1411,7 +1411,7 @@
"logtopic1": "0xf199b2d65bb711d578312320d210574bcc79d63c841d7dcf96ee3604140a7353" "logtopic1": "0xf199b2d65bb711d578312320d210574bcc79d63c841d7dcf96ee3604140a7353"
}, },
{ {
"txhash": "0x730f124954d2b20c9ebfdf6715da7192a570450b485ae60c12a102fecd80d21d", "txhash": "0x911b91ea0109bdb6b7e799f7709eca8e3c618040cde2b4940cccb8bb1b6aaa41",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x86", "block": "0x86",
"indexInBlock": 0, "indexInBlock": 0,
@ -1419,7 +1419,7 @@
"logtopic1": "0xe9bc0af4e1917255f83262d4d61622be8b86fcb24a5810c7b592dd6da6861d56" "logtopic1": "0xe9bc0af4e1917255f83262d4d61622be8b86fcb24a5810c7b592dd6da6861d56"
}, },
{ {
"txhash": "0x69e193cbcc3231501957748043aa48e52b141d0fa38dc6f5027e06790e3088fe", "txhash": "0x95cb4ad3fc4adef8939ec2fb223fece8763016de2b1f2ec49e15837988d651c5",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x91", "block": "0x91",
"indexInBlock": 0, "indexInBlock": 0,
@ -1757,78 +1757,78 @@
], ],
"tx-largereceipt": 11, "tx-largereceipt": 11,
"tx-request-eip7002": { "tx-request-eip7002": {
"txhash": "0x27d8ca12587f28aa0184966fabcbeb321b438a6495d5e8294dc9c62525f41b8c", "txhash": "0xb42e37aec62317cd546baa1a89ce962434f44f9ea6a6a41a8dabff9c4a19e6c8",
"block": "0xd" "block": "0xd"
}, },
"tx-transfer-eip1559": [ "tx-transfer-eip1559": [
{ {
"txhash": "0x4feb87725c3d45aef205359d7d200f523004b2961282c47736c49e3a453a3ab8", "txhash": "0xcedc0fddca312530a8b5651470fb15360ed9b305ef689c2b39e8a8f9fae734de",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0xe", "block": "0xe",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xf282b242af9a863a650850cd7390dc3b3b485845e6290ee171edffab96a7c6dc", "txhash": "0x53b3a68202af8f71fecda9dbdce6bc339fd2b1ab74db91a1e17674e845f9031d",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x19", "block": "0x19",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xebf2517adcc7342a874af927a17590156aa85059fc903ab1b09f97c34c8b8b4e", "txhash": "0xdc35055a250fa2d2e831885cb3c46c3812f9e758daf507c98550d5146aa2635d",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x24", "block": "0x24",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x926f6bb97f760b2ac6c523e975d2697276ee3d881dd8348a4e7b7f2c846da188", "txhash": "0xa4ec63edd50e820f2324532f814bf7d1511e44e5c3a594c4a2d727ad2b0f8915",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x2f", "block": "0x2f",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xef9dea375d62f1406d8777e70192ba24af24044c356da063eb71712eb7499356", "txhash": "0x562f9ac6a63e6ae37a85aa33ca288f54e32af561c3217b52fecfcf0bc291da2e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x3a", "block": "0x3a",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x0816a60a0e6fd2afe45e7db877e33d055c3e4549037eb1e5209a21c4018250bb", "txhash": "0xd0092f480fffa51f656c71e7957d47cac2e424baa4d279706409b9c3573c904e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x45", "block": "0x45",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x9720170ff561812f8bc717c251fbc942af99eefb8b5be1296c0dc3b331da7b33", "txhash": "0x98fd345c029085610b9e2ec506cf7af62dd030eb7879b4727b5dc5bf98ba48e4",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x50", "block": "0x50",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xceda26628e2eecad063f6a4341ea4043fcce307a748b5a41c2a3d0e45721bb0f", "txhash": "0x6f35a7b982016ca589c769b04d3569cfd689a4ececd953f690a1eafbcd1af0ca",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x5b", "block": "0x5b",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x0d8c55478b560a6305ea886fbbfcb36441767c65270c9618ceb33b22f2ee4eef", "txhash": "0xe8e51ddc809af0bcda8dcef19d25e052edcbc727507f0cb631a3100e33ce9ca6",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x66", "block": "0x66",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x12b7508b1d9a5f9afb9de821961aca87f93d97849b8a1b5065a0e50cfc2e8a5e", "txhash": "0xe395cb327beb13b61c158edc2245e55f32d2d039059fd42a266b967c16e8c35c",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x71", "block": "0x71",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xebe88d489dad8695b60b9e8370ce0e389e7cb9e49670980919aac829e2abb83b", "txhash": "0xbdd4c75b7c14d106d77035e0a2eebf332a3e315d4ce0349c00770f2bab8bf050",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x7c", "block": "0x7c",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xdd63b0ac29f1b5b9911a159e704a148fa190ae321158a23e2910b52b69f810ce", "txhash": "0xe5ca0952b6f89e9996f714e69dc89395ec547617ee051c7ce00684f2911c7aba",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x87", "block": "0x87",
"indexInBlock": 0 "indexInBlock": 0
@ -2088,73 +2088,73 @@
], ],
"tx-transfer-eip2930": [ "tx-transfer-eip2930": [
{ {
"txhash": "0xf278a97c1c92c517531ae9f24179862518cf6397cd3615deebe37049d5a6c695", "txhash": "0xb6ad3492b1c07503a7d15857c641c16f9250482f645ecc84efc83c88c54ba2e1",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0xf", "block": "0xf",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xc33009465ef1eb2ff62d02d50cbaaf73fee9dff8be51f71ac7976367ddfc4541", "txhash": "0x7c0c5c8ed14c8ec3d60997eb6e92830c31921d9344eca774c2c571bc8c2b0bdc",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x1a", "block": "0x1a",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xa6598b072ed0598260358ed568e3c907d36e422b14e35ab459e9e24ae98251ef", "txhash": "0x973b494d5bfa55b1fbf4c72122d42ceb8b8667a598948aa24d6a6026de4a7999",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x25", "block": "0x25",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x61aa0de65eb4a0db81573b043c35a4de3dcfabf37117a3e67905c7d3038dad23", "txhash": "0x93de7571e134adbcd98920fb7fbaf0a62b2881896adf2b22f4191bed28ef9e98",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x30", "block": "0x30",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xdcf01b9729cddeefec7be5348f0d364ee1295831f999c0f5a49b047c9694e970", "txhash": "0x79dbf4a386f7865dcafb0d03bd8ff6c0faa570a5ca70eb1b70e656b2d1b1615a",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x3b", "block": "0x3b",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x7867b39e0b2f176fbd61364bce00443bd3f2ca5d743d3ab95104cb13a5cf5208", "txhash": "0x66354d493e12db97bd095e4e038b1e685dbdaf4162582b32d8d336463a7951e7",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x46", "block": "0x46",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x2ba84167cbba25e83e98cbca146d39a4a36d29dc2c9d971b41bf5ea1b9c9425a", "txhash": "0xe6dd0491c7bbc402007f13a14c2ed84f2811cf77f8aeb825551cf00daf942d81",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x51", "block": "0x51",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x203b1d7997f674fd45fe2eb6af5f0a88b59eaa8598b86f408ca44763e034bd36", "txhash": "0x1fb266110683f2166d5eb642c320a536b19768fe304f6594f2596d3258094774",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x5c", "block": "0x5c",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x8fd08a67b065db1005917aea2934104c6132cad339c2fe07f51e7717bbe4ffe7", "txhash": "0x2eabf1863fd795c62b57ca18f88e0a5f4975daf8af8a125bd61c1d10ed910cfa",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x67", "block": "0x67",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xf0a9ae994a7741fcbe854f2d4a684f5fd434d40badc2ae36611f5764aaeb08b3", "txhash": "0x382f8b760cf6025b553959a625090243dc294eec54e3d6e667c031bc9af679b6",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x72", "block": "0x72",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x090fcb3259cccab1bad9dd572493e4ee05df3f53137ca16b4e7ea2f5b5c1d7d3", "txhash": "0x7e930c8cb726972a14b38961c161efdfd5e088f0796ba1e8008869655200a192",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x7d", "block": "0x7d",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xe79b88caea408e8be7e3ac3269889b8f59fd4c1ca612213b9a7999ffe5b72e52", "txhash": "0xbb0ec7a4e53d83b5c746a097cd57ec5607cf0770832f15c390bb08a5a362f17f",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x88", "block": "0x88",
"indexInBlock": 0 "indexInBlock": 0
@ -2414,73 +2414,73 @@
], ],
"tx-transfer-legacy": [ "tx-transfer-legacy": [
{ {
"txhash": "0x4845dfdee733cb11e204253aa9b2e0a9597c9b7f700642d084fd5aad365dbeff", "txhash": "0xacbd2cf02cbc7346d3f58be2c8d7fe1bd7d63608b93048fae4f41ccb8d732a16",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x10", "block": "0x10",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xe4ba4ed6784f2f452274186824174874661f76c1961d34fee86371da194a428f", "txhash": "0x5058b2f5d42ca2af87b9e6a322ec27058e3ab8efa6de89147228c1d07526e667",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x1b", "block": "0x1b",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xe30c98691cc89f2db31aec5c2b3cdfd720015210fce4b48a8ce561d7f179af8b", "txhash": "0x085533a791ba8c5f51334ed52f0544884cbb7816c262176808a9aa8a496e4d60",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x26", "block": "0x26",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x7538bb702539b67fbb8d44d2e946d1d241ddc4c2943e48ecb3107c8868184775", "txhash": "0xc8f4003509b7bad353fe440fd24bd3982e2661c16d93c75140cca6735c46014b",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x31", "block": "0x31",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x88edfa85d3e3bab608081a9421e0eab5fbd09ad10db58c07c178c6ac74617233", "txhash": "0xd2ba3607ed8239e11b1cc2c53bd45694601ec7a6d1020a5e7f2c5ebf1206073b",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x3c", "block": "0x3c",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x41aec073b267edc44e28b0c33f8c20e7619687ca5d7f09faa30e49621cdd3e70", "txhash": "0xd06ab6b82ec91be99bea3bedeb4929233c14650c6ef76fe9d6fbe782dc94205e",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x47", "block": "0x47",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xe0854da80105ea343c29cb7a8db30413b59f5b62a1f42793ce635be4e7536cc9", "txhash": "0x93e8dc866ce419e01221ff657eb79ac65a4361499d6cea117ca4de09023c1527",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x52", "block": "0x52",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x0cf942350189ee11294379c2a16c9744bc7502f127f4f71ac95cf0a2f681aa3a", "txhash": "0x965ddac3d0550607ba9e6e8bec74c3d8f881ee65758e38306790707cf27956df",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x5d", "block": "0x5d",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0xd521a3fccc17113eac1a50bd0a34a3ca87ee5234c51fe41ba8eb8c26ad142363", "txhash": "0x265395037938c0d7312559ea4c10c211be89d04008dd7fc5752b3f633cc22404",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x68", "block": "0x68",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x1bcb6ff54f0f244c3b7390b488e76c3d1d95ff7f340adb81f785820930edc010", "txhash": "0x0dae10c3d6a4640ec7a257131ba77a6aaf5e00e9e3dc09b291dfba51dd0134c8",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x73", "block": "0x73",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x7571a5b0881a76054142ad7af3924bd225a67a4705479f4f87e6a68388a5f0aa", "txhash": "0x629e56214deda1fdc4ad3db433c3034816e93752258037bf6d5fce4525061d03",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x7e", "block": "0x7e",
"indexInBlock": 0 "indexInBlock": 0
}, },
{ {
"txhash": "0x999f17b911d92aaed0c83e37a545cf44f1effaf1245bddc7c63aae3c092e8200", "txhash": "0x60b9a9cff166acc4822d235ae6c636dd7a9394ffc9ebc30ce2769c9bd02ae2fc",
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f", "sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
"block": "0x89", "block": "0x89",
"indexInBlock": 0 "indexInBlock": 0

View file

@ -74,7 +74,7 @@ func (s *Suite) sendTxs(t *utesting.T, txs []*types.Transaction) error {
for _, tx := range txs { for _, tx := range txs {
got[tx.Hash()] = true got[tx.Hash()] = true
} }
case *eth.NewPooledTransactionHashesPacket: case *eth.NewPooledTransactionHashesPacket72:
for _, hash := range msg.Hashes { for _, hash := range msg.Hashes {
got[hash] = true got[hash] = true
} }
@ -160,7 +160,7 @@ func (s *Suite) sendInvalidTxs(t *utesting.T, txs []*types.Transaction) error {
return fmt.Errorf("received bad tx: %s", tx.Hash()) return fmt.Errorf("received bad tx: %s", tx.Hash())
} }
} }
case *eth.NewPooledTransactionHashesPacket: case *eth.NewPooledTransactionHashesPacket72:
for _, hash := range msg.Hashes { for _, hash := range msg.Hashes {
if _, ok := invalids[hash]; ok { if _, ok := invalids[hash]; ok {
return fmt.Errorf("received bad tx: %s", hash) return fmt.Errorf("received bad tx: %s", hash)

View file

@ -75,6 +75,7 @@ var (
utils.BlobPoolDataDirFlag, utils.BlobPoolDataDirFlag,
utils.BlobPoolDataCapFlag, utils.BlobPoolDataCapFlag,
utils.BlobPoolPriceBumpFlag, utils.BlobPoolPriceBumpFlag,
utils.BlobPoolFetchProbabilityFlag,
utils.SyncModeFlag, utils.SyncModeFlag,
utils.SyncTargetFlag, utils.SyncTargetFlag,
utils.ExitWhenSyncedFlag, utils.ExitWhenSyncedFlag,

View file

@ -49,6 +49,7 @@ import (
"github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/eth"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/fetcher"
"github.com/ethereum/go-ethereum/eth/filters" "github.com/ethereum/go-ethereum/eth/filters"
"github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/eth/syncer" "github.com/ethereum/go-ethereum/eth/syncer"
@ -514,6 +515,12 @@ var (
Value: ethconfig.Defaults.BlobPool.PriceBump, Value: ethconfig.Defaults.BlobPool.PriceBump,
Category: flags.BlobPoolCategory, Category: flags.BlobPoolCategory,
} }
BlobPoolFetchProbabilityFlag = &cli.Uint64Flag{
Name: "blobpool.fetchprobability",
Usage: "Probability of fetching the full blob payload for sparse blobpool (min=15, max=100)",
Value: fetcher.DefaultFetchProbability,
Category: flags.BlobPoolCategory,
}
// Performance tuning settings // Performance tuning settings
CacheFlag = &cli.IntFlag{ CacheFlag = &cli.IntFlag{
Name: "cache", Name: "cache",
@ -1692,6 +1699,9 @@ func setBlobPool(ctx *cli.Context, cfg *blobpool.Config) {
if ctx.IsSet(BlobPoolPriceBumpFlag.Name) { if ctx.IsSet(BlobPoolPriceBumpFlag.Name) {
cfg.PriceBump = ctx.Uint64(BlobPoolPriceBumpFlag.Name) cfg.PriceBump = ctx.Uint64(BlobPoolPriceBumpFlag.Name)
} }
if ctx.IsSet(BlobPoolFetchProbabilityFlag.Name) {
cfg.FetchProbability = ctx.Uint64(BlobPoolFetchProbabilityFlag.Name)
}
} }
func setMiner(ctx *cli.Context, cfg *miner.Config) { func setMiner(ctx *cli.Context, cfg *miner.Config) {

View file

@ -129,9 +129,12 @@ type blobTxMeta struct {
announced bool // Whether the tx has been announced to listeners announced bool // Whether the tx has been announced to listeners
id uint64 // Storage ID in the pool's persistent store id uint64 // Storage ID in the pool's persistent store
storageSize uint32 // Byte size in the pool's persistent store storageSize uint32 // Byte size in the pool's persistent store
size uint64 // RLP-encoded size of transaction including the attached blob size uint64 // RLP-encoded size of transaction including the attached blob
sizeWithoutBlob uint64 // RLP-encoded size of transaction without blob data (for ETH/72)
custody *types.CustodyBitmap
nonce uint64 // Needed to prioritize inclusion order within an account nonce uint64 // Needed to prioritize inclusion order within an account
costCap *uint256.Int // Needed to validate cumulative balance sufficiency costCap *uint256.Int // Needed to validate cumulative balance sufficiency
@ -149,70 +152,109 @@ type blobTxMeta struct {
evictionBlobFeeJumps float64 // Worse blob fee (converted to fee jumps) across all previous nonces evictionBlobFeeJumps float64 // Worse blob fee (converted to fee jumps) across all previous nonces
} }
// blobTxForPool is the storage representation of a blob transaction in the // BlobTxForPool is the storage representation of a blob transaction in the
// blobpool. // blobpool.
type blobTxForPool struct { type BlobTxForPool struct {
Tx *types.Transaction // tx without sidecar Tx *types.Transaction // tx without sidecar
Version byte CellSidecar *types.BlobTxCellSidecar
Commitments []kzg4844.Commitment
Proofs []kzg4844.Proof
Blobs []kzg4844.Blob
} }
// Sidecar returns BlobTxSidecar of ptx. // Sidecar returns BlobTxSidecar of pooled transaction. Since this function
func (ptx *blobTxForPool) Sidecar() *types.BlobTxSidecar { // recovers the blob field in sidecar, it is expansive and needs to be
return types.NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs) // avoided if possible. Returns error if recovery fails (e.g. insufficient cells).
func (ptx *BlobTxForPool) sidecar() (*types.BlobTxSidecar, error) {
sidecar := ptx.CellSidecar
blobs, err := kzg4844.RecoverBlobs(sidecar.Cells, sidecar.Custody.Indices())
if err != nil {
return nil, err
}
return types.NewBlobTxSidecar(sidecar.Version, blobs, sidecar.Commitments, sidecar.Proofs), nil
} }
// TxSize returns the transaction size on the network without // TxSize returns the transaction size on the network without
// reconstructing the transaction. // reconstructing the transaction.
func (ptx *blobTxForPool) TxSize() uint64 { func (ptx *BlobTxForPool) txSize() uint64 {
var blobs, commitments, proofs uint64 sidecar := ptx.CellSidecar
for i := range ptx.Blobs {
blobs += rlp.BytesSize(ptx.Blobs[i][:]) var commitments, proofs uint64
for i := range sidecar.Commitments {
commitments += rlp.BytesSize(sidecar.Commitments[i][:])
} }
for i := range ptx.Commitments { for i := range sidecar.Proofs {
commitments += rlp.BytesSize(ptx.Commitments[i][:]) proofs += rlp.BytesSize(sidecar.Proofs[i][:])
}
for i := range ptx.Proofs {
proofs += rlp.BytesSize(ptx.Proofs[i][:])
} }
var blob kzg4844.Blob
blobs := uint64(len(sidecar.Commitments)) * rlp.BytesSize(blob[:])
return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(blobs)+rlp.ListSize(commitments)+rlp.ListSize(proofs)) return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(blobs)+rlp.ListSize(commitments)+rlp.ListSize(proofs))
} }
func (ptx *BlobTxForPool) txSizeWithoutBlob() uint64 {
sidecar := ptx.CellSidecar
var commitments, proofs uint64
for i := range sidecar.Commitments {
commitments += rlp.BytesSize(sidecar.Commitments[i][:])
}
for i := range sidecar.Proofs {
proofs += rlp.BytesSize(sidecar.Proofs[i][:])
}
return ptx.Tx.Size() + rlp.ListSize(rlp.ListSize(0)+rlp.ListSize(commitments)+rlp.ListSize(proofs))
}
// ToTx reconstructs a full Transaction with the sidecar attached. // ToTx reconstructs a full Transaction with the sidecar attached.
func (ptx *blobTxForPool) ToTx() *types.Transaction { func (ptx *BlobTxForPool) toTx() (*types.Transaction, error) {
return ptx.Tx.WithBlobTxSidecar(ptx.Sidecar()) sc, err := ptx.sidecar()
if err != nil {
return nil, err
}
return ptx.Tx.WithBlobTxSidecar(sc), nil
} }
// newBlobTxForPool decomposes a blob transaction into blobTxForPool type. // newBlobTxForPool decomposes a blob transaction into blobTxForPool type.
func newBlobTxForPool(tx *types.Transaction) *blobTxForPool { func newBlobTxForPool(tx *types.Transaction) (*BlobTxForPool, error) {
sc := tx.BlobTxSidecar() sc := tx.BlobTxSidecar()
if sc == nil { if sc == nil {
panic("missing blob tx sidecar") return nil, errors.New("missing blob tx sidecar")
} }
return &blobTxForPool{ cells, err := kzg4844.ComputeCells(sc.Blobs)
Tx: tx.WithoutBlobTxSidecar(), if err != nil {
return nil, err
}
sidecar := types.BlobTxCellSidecar{
Version: sc.Version, Version: sc.Version,
Commitments: sc.Commitments, Commitments: sc.Commitments,
Proofs: sc.Proofs, Proofs: sc.Proofs,
Blobs: sc.Blobs, Cells: cells,
Custody: types.CustodyBitmapAll,
} }
return &BlobTxForPool{
Tx: tx.WithoutBlobTxSidecar(),
CellSidecar: &sidecar,
}, nil
} }
// encodeForNetwork transforms stored blobTxForPool RLP into the standard // encodeForNetwork transforms stored BlobTxForPool RLP into the network
// network transaction encoding. This is used for getRLP. // transaction encoding for the given eth protocol version. Used for getRLP.
// //
// Stored RLP: [type_byte || tx_fields, version, [comms], [proofs], [blobs]] // Stored RLP: [type_byte || tx_fields, [version, [cells], [comms], [proofs], custody]]
// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]]) //
// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]]) // eth/69, eth/70: [blobs] is recovered from stored cells via kzg.
func encodeForNetwork(storedRLP []byte) ([]byte, error) { //
// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]])
// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]])
//
// eth/72: [blobs] is replaced by an empty list (cells are fetched separately
//
// via GetCells).
// V0: type_byte || rlp([tx_fields, [], [comms], [proofs]])
// V1: type_byte || rlp([tx_fields, version, [], [comms], [proofs]])
func encodeForNetwork(storedRLP []byte, version uint) ([]byte, error) {
elems, err := rlp.SplitListValues(storedRLP) elems, err := rlp.SplitListValues(storedRLP)
if err != nil { if err != nil {
return nil, fmt.Errorf("invalid blobTxForPool RLP: %w", err) return nil, fmt.Errorf("invalid BlobTxForPool RLP: %w", err)
} }
if len(elems) < 5 { if len(elems) < 2 {
return nil, fmt.Errorf("blobTxForPool has %d elements, need at least 5", len(elems)) return nil, fmt.Errorf("BlobTxForPool has %d elements, need at least 2", len(elems))
} }
// 1. Extract tx byte and other tx fields // 1. Extract tx byte and other tx fields
@ -226,19 +268,53 @@ func encodeForNetwork(storedRLP []byte) ([]byte, error) {
typeByte := txBytes[0] typeByte := txBytes[0]
txRLP := txBytes[1:] txRLP := txBytes[1:]
// 2. Find the version of sidecar. // 2. Split the nested CellSidecar list.
version, _, err := rlp.SplitUint64(elems[1]) sidecarElems, err := rlp.SplitListValues(elems[1])
if err != nil || version > 255 || version == 0 { if err != nil {
return nil, fmt.Errorf("invalid CellSidecar RLP: %w", err)
}
if len(sidecarElems) < 5 {
return nil, fmt.Errorf("CellSidecar has %d elements, need at least 5", len(sidecarElems))
}
// 3. Find the version of sidecar.
sidecarVersion, _, err := rlp.SplitUint64(sidecarElems[0])
if err != nil || sidecarVersion > 255 || sidecarVersion == 0 {
return nil, fmt.Errorf("invalid version: %w", err) return nil, fmt.Errorf("invalid version: %w", err)
} }
// 3. Extract sidecar elements.
commitmentsRLP := elems[2]
proofsRLP := elems[3]
blobsRLP := elems[4]
// 4. Reconstruct into the network format. // 4. Extract sidecar elements.
outer := [][]byte{txRLP, elems[1], blobsRLP, commitmentsRLP, proofsRLP} commitmentsRLP := sidecarElems[2]
proofsRLP := sidecarElems[3]
// 5. Build the [blobs] field for the wire format.
var blobsField []byte
// todo - Didn't use eth.ETH72 due to circular import error in test
if version >= 72 {
// eth/72 omits the blob payload; peers fetch cells separately via GetCells.
blobsField = []byte{0xc0} // RLP-encoded empty list
} else {
// eth/69, eth/70 need actual blobs: recover them from stored cells.
var cells []kzg4844.Cell
if err := rlp.DecodeBytes(sidecarElems[1], &cells); err != nil {
return nil, fmt.Errorf("invalid cells RLP: %w", err)
}
var custody types.CustodyBitmap
if err := rlp.DecodeBytes(sidecarElems[4], &custody); err != nil {
return nil, fmt.Errorf("invalid custody RLP: %w", err)
}
blobs, err := kzg4844.RecoverBlobs(cells, custody.Indices())
if err != nil {
return nil, fmt.Errorf("failed to recover blobs: %w", err)
}
blobsField, err = rlp.EncodeToBytes(blobs)
if err != nil {
return nil, fmt.Errorf("failed to encode blobs: %w", err)
}
}
// 6. Reconstruct into the network format.
outer := [][]byte{txRLP, sidecarElems[0], blobsField, commitmentsRLP, proofsRLP}
body, err := rlp.MergeListValues(outer) body, err := rlp.MergeListValues(outer)
if err != nil { if err != nil {
return nil, err return nil, err
@ -252,21 +328,23 @@ func encodeForNetwork(storedRLP []byte) ([]byte, error) {
// newBlobTxMeta retrieves the indexed metadata fields from a pooled blob // newBlobTxMeta retrieves the indexed metadata fields from a pooled blob
// transaction and assembles a helper struct to track in memory. // transaction and assembles a helper struct to track in memory.
func newBlobTxMeta(id uint64, size uint64, storageSize uint32, ptx *blobTxForPool) *blobTxMeta { func newBlobTxMeta(id uint64, storageSize uint32, ptx *BlobTxForPool) *blobTxMeta {
meta := &blobTxMeta{ meta := &blobTxMeta{
hash: ptx.Tx.Hash(), hash: ptx.Tx.Hash(),
vhashes: ptx.Tx.BlobHashes(), vhashes: ptx.Tx.BlobHashes(),
version: ptx.Version, version: ptx.CellSidecar.Version,
id: id, id: id,
storageSize: storageSize, storageSize: storageSize,
size: size, size: ptx.txSize(),
nonce: ptx.Tx.Nonce(), sizeWithoutBlob: ptx.txSizeWithoutBlob(),
costCap: uint256.MustFromBig(ptx.Tx.Cost()), nonce: ptx.Tx.Nonce(),
execTipCap: uint256.MustFromBig(ptx.Tx.GasTipCap()), costCap: uint256.MustFromBig(ptx.Tx.Cost()),
execFeeCap: uint256.MustFromBig(ptx.Tx.GasFeeCap()), execTipCap: uint256.MustFromBig(ptx.Tx.GasTipCap()),
blobFeeCap: uint256.MustFromBig(ptx.Tx.BlobGasFeeCap()), execFeeCap: uint256.MustFromBig(ptx.Tx.GasFeeCap()),
execGas: ptx.Tx.Gas(), blobFeeCap: uint256.MustFromBig(ptx.Tx.BlobGasFeeCap()),
blobGas: ptx.Tx.BlobGas(), execGas: ptx.Tx.Gas(),
blobGas: ptx.Tx.BlobGas(),
custody: &ptx.CellSidecar.Custody,
} }
meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap) meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap)
meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap) meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap)
@ -465,8 +543,10 @@ type BlobPool struct {
stored uint64 // Useful data size of all transactions on disk stored uint64 // Useful data size of all transactions on disk
limbo *limbo // Persistent data store for the non-finalized blobs limbo *limbo // Persistent data store for the non-finalized blobs
gapped map[common.Address][]*types.Transaction // Transactions that are currently gapped (nonce too high) cQueue *conversionQueue
gappedSource map[common.Hash]common.Address // Source of gapped transactions to allow rechecking on inclusion
gapped map[common.Address][]*BlobTxForPool // Transactions that are currently gapped (nonce too high)
gappedSource map[common.Hash]common.Address // Source of gapped transactions to allow rechecking on inclusion
signer types.Signer // Transaction signer to use for sender recovery signer types.Signer // Transaction signer to use for sender recovery
chain BlockChain // Chain object to access the state through chain BlockChain // Chain object to access the state through
@ -501,7 +581,7 @@ func New(config Config, chain BlockChain, hasPendingAuth func(common.Address) bo
lookup: newLookup(), lookup: newLookup(),
index: make(map[common.Address][]*blobTxMeta), index: make(map[common.Address][]*blobTxMeta),
spent: make(map[common.Address]*uint256.Int), spent: make(map[common.Address]*uint256.Int),
gapped: make(map[common.Address][]*types.Transaction), gapped: make(map[common.Address][]*BlobTxForPool),
gappedSource: make(map[common.Hash]common.Address), gappedSource: make(map[common.Hash]common.Address),
} }
} }
@ -564,11 +644,14 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
) )
index := func(id uint64, size uint32, blob []byte) { index := func(id uint64, size uint32, blob []byte) {
err := p.parseTransaction(id, size, blob) err := p.parseTransaction(id, size, blob)
if err != nil { // Transactions in legacy format will be queued for cell computation.
toDelete = append(toDelete, id) // This entry will be swapped from the store after the conversion.
}
if errors.Is(err, errLegacyTx) { if errors.Is(err, errLegacyTx) {
convertTxs = append(convertTxs, id) convertTxs = append(convertTxs, id)
return
}
if err != nil {
toDelete = append(toDelete, id)
} }
} }
store, err := billy.Open(billy.Options{Path: queuedir, Repair: true}, slotter, index) store, err := billy.Open(billy.Options{Path: queuedir, Repair: true}, slotter, index)
@ -577,45 +660,7 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
} }
p.store = store p.store = store
// Migrate legacy transactions (types.Transaction) to pooledBlobTx format. p.cQueue = newConversionQueue()
if len(convertTxs) > 0 {
for _, id := range convertTxs {
var tx types.Transaction
data, err := p.store.Get(id)
if err != nil {
continue
}
err = rlp.DecodeBytes(data, &tx)
if err != nil {
continue
}
if tx.BlobTxSidecar() == nil {
continue
}
ptx := newBlobTxForPool(&tx)
blob, err := rlp.EncodeToBytes(ptx)
if err != nil {
continue
}
id, err := p.store.Put(blob)
if err != nil {
continue
}
meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx)
// If the newly inserted transaction fails to be tracked,
// it should also be removed with those in `toDelete`
sender, err := types.Sender(p.signer, ptx.Tx)
if err != nil {
toDelete = append(toDelete, id)
continue
}
if err := p.trackTransaction(meta, sender); err != nil {
toDelete = append(toDelete, id)
continue
}
}
}
if len(toDelete) > 0 { if len(toDelete) > 0 {
log.Warn("Dropping invalidated blob transactions", "ids", toDelete) log.Warn("Dropping invalidated blob transactions", "ids", toDelete)
@ -659,7 +704,8 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
// Pool initialized, attach the blob limbo to it to track blobs included // Pool initialized, attach the blob limbo to it to track blobs included
// recently but not yet finalized // recently but not yet finalized
p.limbo, err = newLimbo(p.chain.Config(), limbodir) var convertLimbo []uint64
p.limbo, convertLimbo, err = newLimbo(p.chain.Config(), limbodir)
if err != nil { if err != nil {
p.Close() p.Close()
return err return err
@ -678,12 +724,22 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
// Update the metrics and return the constructed pool // Update the metrics and return the constructed pool
datacapGauge.Update(int64(p.config.Datacap)) datacapGauge.Update(int64(p.config.Datacap))
p.updateStorageMetrics() p.updateStorageMetrics()
if len(convertTxs) > 0 {
p.cQueue.launchConversion(func() { p.convertLegacyTxs(convertTxs) })
}
if len(convertLimbo) > 0 {
p.cQueue.launchConversion(func() { p.convertLegacyLimbo(convertLimbo) })
}
return nil return nil
} }
// Close closes down the underlying persistent store. // Close closes down the underlying persistent store.
func (p *BlobPool) Close() error { func (p *BlobPool) Close() error {
var errs []error var errs []error
if p.cQueue != nil {
p.cQueue.close()
}
if p.limbo != nil { // Close might be invoked due to error in constructor, before p,limbo is set if p.limbo != nil { // Close might be invoked due to error in constructor, before p,limbo is set
if err := p.limbo.Close(); err != nil { if err := p.limbo.Close(); err != nil {
errs = append(errs, err) errs = append(errs, err)
@ -704,25 +760,119 @@ func (p *BlobPool) Close() error {
// parseTransaction is a callback method on pool creation that gets called for // parseTransaction is a callback method on pool creation that gets called for
// each transaction on disk to create the in-memory metadata index. // 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. // Announced state is not initialized here, it needs to be initialized separately.
//
// If a legacy types.Transaction is found on disk, it is returned for migration
// in Init (the old ID will be deleted and a new pooledBlobTx written).
// If a pooledBlobTx is found, it is indexed directly and nil is returned.
func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error { func (p *BlobPool) parseTransaction(id uint64, size uint32, blob []byte) error {
var ptx blobTxForPool var ptx BlobTxForPool
if err := rlp.DecodeBytes(blob, &ptx); err != nil { if err := rlp.DecodeBytes(blob, &ptx); err != nil {
kind, content, _, splitErr := rlp.Split(blob) kind, content, _, splitErr := rlp.Split(blob)
// check whether it is legacy tx type // check whether it is legacy tx type
if splitErr == nil && kind == rlp.String && len(content) > 1 && content[0] == 3 { if splitErr == nil && kind == rlp.String && len(content) > 1 && content[0] == 3 {
return errLegacyTx return errLegacyTx
} }
log.Error("Failed to decode blob pool entry", "id", id, "err", err)
return err return err
} }
meta := newBlobTxMeta(id, ptx.TxSize(), size, &ptx) meta := newBlobTxMeta(id, size, &ptx)
sender, err := types.Sender(p.signer, ptx.Tx) sender, err := types.Sender(p.signer, ptx.Tx)
if err != nil { if err != nil {
// This path is impossible unless the signature validity changes across
// restarts. For that ever improbable case, recover gracefully by ignoring
// this data entry.
log.Error("Failed to recover blob tx sender", "id", id, "hash", ptx.Tx.Hash(), "err", err)
return err return err
} }
return p.trackTransaction(meta, sender) return p.trackTransaction(meta, sender)
} }
func (p *BlobPool) convertLegacyTxs(ids []uint64) {
start := time.Now()
var converted, discarded int
for _, id := range ids {
p.lock.Lock()
data, err := p.store.Get(id)
if err != nil {
p.lock.Unlock()
continue
}
if derr := p.store.Delete(id); derr != nil {
log.Error("Failed to delete legacy blob tx", "id", id, "err", derr)
p.lock.Unlock()
continue
}
p.lock.Unlock()
var tx types.Transaction
if err := rlp.DecodeBytes(data, &tx); err != nil {
log.Error("Failed to decode legacy blob tx", "id", id, "err", err)
continue
}
ptx, err := newBlobTxForPool(&tx)
if err != nil {
log.Error("Failed to convert legacy blob tx", "hash", tx.Hash(), "err", err)
continue
}
if err := p.AddPooledTx(ptx); err != nil {
log.Debug("Discarded converted blob tx", "hash", tx.Hash(), "err", err)
discarded++
} else {
converted++
}
}
log.Info("Completed blob transaction conversion", "converted", converted, "discarded", discarded, "elapsed", common.PrettyDuration(time.Since(start)))
}
func (p *BlobPool) convertLegacyLimbo(ids []uint64) {
start := time.Now()
var converted, discarded int
for _, id := range ids {
p.lock.Lock()
data, err := p.limbo.store.Get(id)
if err != nil {
p.lock.Unlock()
continue
}
if derr := p.limbo.store.Delete(id); derr != nil {
log.Error("Failed to delete legacy blob tx", "id", id, "err", derr)
p.lock.Unlock()
continue
}
p.lock.Unlock()
var legacy struct {
TxHash common.Hash
Block uint64
Tx *types.Transaction
}
if err := rlp.DecodeBytes(data, &legacy); err != nil {
log.Error("Failed to decode legacy limbo entry", "id", id, "err", err)
continue
}
if legacy.Tx == nil || legacy.Tx.BlobTxSidecar() == nil {
continue
}
ptx, err := newBlobTxForPool(legacy.Tx)
if err != nil {
log.Error("Failed to convert legacy limbo entry", "hash", legacy.TxHash, "err", err)
continue
}
p.lock.Lock()
if err := p.limbo.setAndIndex(ptx, legacy.Block); err != nil {
log.Error("Failed to re-store converted limbo entry", "hash", legacy.TxHash, "err", err)
discarded++
} else {
converted++
}
p.lock.Unlock()
}
log.Info("Completed limbo blob conversion", "converted", converted, "discarded", discarded, "elapsed", common.PrettyDuration(time.Since(start)))
}
// trackTransaction registers a transaction's metadata in the pool's indices. // trackTransaction registers a transaction's metadata in the pool's indices.
func (p *BlobPool) trackTransaction(meta *blobTxMeta, sender common.Address) error { func (p *BlobPool) trackTransaction(meta *blobTxMeta, sender common.Address) error {
if p.lookup.exists(meta.hash) { if p.lookup.exists(meta.hash) {
@ -1006,7 +1156,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) log.Error("Blobs missing for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err)
return return
} }
var ptx blobTxForPool var ptx BlobTxForPool
if err := rlp.DecodeBytes(data, &ptx); err != nil { if err := rlp.DecodeBytes(data, &ptx); err != nil {
log.Error("Blobs corrupted for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err) log.Error("Blobs corrupted for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err)
return return
@ -1094,7 +1244,7 @@ func (p *BlobPool) Reset(oldHead, newHead *types.Header) {
log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) log.Error("Blobs missing for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err)
continue continue
} }
var ptx blobTxForPool var ptx BlobTxForPool
if err = rlp.DecodeBytes(data, &ptx); err != nil { if err = rlp.DecodeBytes(data, &ptx); err != nil {
log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err) log.Error("Blobs corrupted for announcable transaction", "from", addr, "nonce", meta.nonce, "id", meta.id, "err", err)
continue continue
@ -1131,7 +1281,7 @@ func (p *BlobPool) vhashesByTx() map[common.Hash][]common.Hash {
// getByVhash reads and decodes the blob transaction which has the given // getByVhash reads and decodes the blob transaction which has the given
// versioned hash. Returns nil if unavailable. // versioned hash. Returns nil if unavailable.
func (p *BlobPool) getByVhash(vhash common.Hash) *blobTxForPool { func (p *BlobPool) getByVhash(vhash common.Hash) *BlobTxForPool {
p.lock.RLock() p.lock.RLock()
txID, exists := p.lookup.storeidOfBlob(vhash) txID, exists := p.lookup.storeidOfBlob(vhash)
p.lock.RUnlock() p.lock.RUnlock()
@ -1143,7 +1293,7 @@ func (p *BlobPool) getByVhash(vhash common.Hash) *blobTxForPool {
log.Error("Tracked blob transaction missing from store", "id", txID, "err", err) log.Error("Tracked blob transaction missing from store", "id", txID, "err", err)
return nil return nil
} }
var ptx blobTxForPool var ptx BlobTxForPool
if err := rlp.DecodeBytes(data, &ptx); err != nil { if err := rlp.DecodeBytes(data, &ptx); err != nil {
log.Error("Blobs corrupted for tracked transaction", "id", txID, "err", err) log.Error("Blobs corrupted for tracked transaction", "id", txID, "err", err)
return nil return nil
@ -1300,7 +1450,7 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error {
// A reorged-out legacy blob transaction can therefore not be re-added, so drop // A reorged-out legacy blob transaction can therefore not be re-added, so drop
// it on the floor instead of putting it back. // it on the floor instead of putting it back.
head := p.head.Load() head := p.head.Load()
if p.chain.Config().IsOsaka(head.Number, head.Time) && ptx.Version == types.BlobSidecarVersion0 { if p.chain.Config().IsOsaka(head.Number, head.Time) && ptx.CellSidecar.Version == types.BlobSidecarVersion0 {
log.Debug("Dropping reorged legacy blob transaction", "hash", txhash) log.Debug("Dropping reorged legacy blob transaction", "hash", txhash)
return errors.New("legacy blob sidecar unsupported post-osaka") return errors.New("legacy blob sidecar unsupported post-osaka")
} }
@ -1314,7 +1464,7 @@ func (p *BlobPool) reinject(addr common.Address, txhash common.Hash) error {
log.Error("Failed to write transaction into storage", "hash", ptx.Tx.Hash(), "err", err) log.Error("Failed to write transaction into storage", "hash", ptx.Tx.Hash(), "err", err)
return err return err
} }
meta := newBlobTxMeta(id, ptx.TxSize(), p.store.Size(id), ptx) meta := newBlobTxMeta(id, p.store.Size(id), ptx)
if _, ok := p.index[addr]; !ok { if _, ok := p.index[addr]; !ok {
if err := p.reserver.Hold(addr); err != nil { if err := p.reserver.Hold(addr); err != nil {
log.Warn("Failed to reserve account for blob pool", "tx", ptx.Tx.Hash(), "from", addr, "err", err) log.Warn("Failed to reserve account for blob pool", "tx", ptx.Tx.Hash(), "from", addr, "err", err)
@ -1543,6 +1693,7 @@ func (p *BlobPool) Has(hash common.Hash) bool {
return poolHas || gapped return poolHas || gapped
} }
// getRLP returns the raw RLP-encoded pooledBlobTx data from the store.
func (p *BlobPool) getRLP(hash common.Hash) []byte { func (p *BlobPool) getRLP(hash common.Hash) []byte {
// Track the amount of time waiting to retrieve a fully resolved blob tx from // Track the amount of time waiting to retrieve a fully resolved blob tx from
// the pool and the amount of time actually spent on pulling the data from disk. // the pool and the amount of time actually spent on pulling the data from disk.
@ -1569,32 +1720,34 @@ func (p *BlobPool) getRLP(hash common.Hash) []byte {
} }
// Get returns a transaction if it is contained in the pool, or nil otherwise. // Get returns a transaction if it is contained in the pool, or nil otherwise.
// Note that this function always try to recover full blobs
func (p *BlobPool) Get(hash common.Hash) *types.Transaction { func (p *BlobPool) Get(hash common.Hash) *types.Transaction {
data := p.getRLP(hash) data := p.getRLP(hash)
if len(data) == 0 { if len(data) == 0 {
return nil return nil
} }
var ptx blobTxForPool var ptx BlobTxForPool
if err := rlp.DecodeBytes(data, &ptx); err != nil { if err := rlp.DecodeBytes(data, &ptx); err != nil {
id, _ := p.lookup.storeidOfTx(hash) id, _ := p.lookup.storeidOfTx(hash)
log.Error("Blobs corrupted for traced transaction", "hash", hash, "id", id, "err", err)
log.Error("Blobs corrupted for traced transaction",
"hash", hash, "id", id, "err", err)
return nil return nil
} }
return ptx.ToTx() tx, err := ptx.toTx()
if err != nil {
log.Error("Failed to recover transaction in blobpool", "hash", hash, "err", err)
return nil
}
return tx
} }
// GetRLP returns a RLP-encoded transaction for network if it is contained in the pool. // GetRLP returns an RLP-encoded transaction if it is contained in the pool.
// It converts the pool's internal type to the RLP format used by the eth protocol: func (p *BlobPool) GetRLP(hash common.Hash, version uint) []byte {
// e.g. type_byte || [..., version, [blobs], [comms], [proofs]]
func (p *BlobPool) GetRLP(hash common.Hash) []byte {
data := p.getRLP(hash) data := p.getRLP(hash)
if len(data) == 0 { if len(data) == 0 {
// Not in this pool, do not log. // Not in this pool, do not log.
return nil return nil
} }
rlp, err := encodeForNetwork(data) rlp, err := encodeForNetwork(data, version)
if err != nil { if err != nil {
log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err) log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err)
return nil return nil
@ -1611,13 +1764,14 @@ func (p *BlobPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
p.lock.RLock() p.lock.RLock()
defer p.lock.RUnlock() defer p.lock.RUnlock()
size, ok := p.lookup.sizeOfTx(hash) meta, ok := p.lookup.txIndex[hash]
if !ok { if !ok {
return nil return nil
} }
return &txpool.TxMetadata{ return &txpool.TxMetadata{
Type: types.BlobTxType, Type: types.BlobTxType,
Size: size, Size: meta.size,
SizeWithoutBlob: meta.sizeWithoutBlob,
} }
} }
@ -1657,12 +1811,16 @@ func (p *BlobPool) getBlobs(vhashes []common.Hash, version byte) (_ []*kzg4844.B
} }
// Decode the blob transaction // Decode the blob transaction
var ptx blobTxForPool var ptx BlobTxForPool
if err := rlp.DecodeBytes(data, &ptx); err != nil { if err := rlp.DecodeBytes(data, &ptx); err != nil {
log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err) log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err)
continue continue
} }
sidecar := ptx.Sidecar() sidecar, err := ptx.sidecar()
if err != nil {
log.Error("Failed to recover sidecar in blobpool", "id", txID, "err", err)
continue
}
// Traverse the blobs in the transaction // Traverse the blobs in the transaction
for i, hash := range ptx.Tx.BlobHashes() { for i, hash := range ptx.Tx.BlobHashes() {
list, ok := indices[hash] list, ok := indices[hash]
@ -1699,6 +1857,90 @@ func (p *BlobPool) getBlobs(vhashes []common.Hash, version byte) (_ []*kzg4844.B
return blobs, commitments, proofs, nil return blobs, commitments, proofs, nil
} }
// GetBlobHashes returns the blob versioned hashes for a given transaction hash.
func (p *BlobPool) GetBlobHashes(txHash common.Hash) []common.Hash {
p.lock.RLock()
defer p.lock.RUnlock()
vhashes, ok := p.lookup.blobHashesOfTx(txHash)
if !ok {
return nil
}
return vhashes
}
// GetBlobCells returns cells for the given versioned blob hashes. Nil entries
// mean that the cell was not available.
func (p *BlobPool) GetBlobCells(vhashes []common.Hash, mask types.CustodyBitmap) ([][]*kzg4844.Cell, [][]*kzg4844.Proof, error) {
var (
cells = make([][]*kzg4844.Cell, len(vhashes))
proofs = make([][]*kzg4844.Proof, len(vhashes))
vindex = make(map[common.Hash][]int) // Indices of versioned hashes in the request
filled = make(map[common.Hash]struct{})
)
for i, h := range vhashes {
vindex[h] = append(vindex[h], i)
}
requestedIndices := mask.Indices()
for _, vhash := range vhashes {
if _, ok := filled[vhash]; ok {
continue
}
p.lock.RLock()
txID, exists := p.lookup.storeidOfBlob(vhash)
p.lock.RUnlock()
if !exists {
continue
}
data, err := p.store.Get(txID)
if err != nil {
continue
}
var ptx BlobTxForPool
if err := rlp.DecodeBytes(data, &ptx); err != nil {
continue
}
tx := ptx.Tx
cellsPerBlob := ptx.CellSidecar.Custody.OneCount()
storedIndices := ptx.CellSidecar.Custody.Indices()
for blobIdx, hash := range tx.BlobHashes() {
indices, ok := vindex[hash]
if !ok {
continue
}
filled[hash] = struct{}{}
blobCells := make([]*kzg4844.Cell, len(requestedIndices))
blobProofs := make([]*kzg4844.Proof, len(requestedIndices))
for i, cellIdx := range requestedIndices {
pos := -1
for k, storedIdx := range storedIndices {
if storedIdx == cellIdx {
pos = k
break
}
}
if pos >= 0 {
cell := ptx.CellSidecar.Cells[blobIdx*cellsPerBlob+pos]
blobCells[i] = &cell
proofIdx := blobIdx*kzg4844.CellProofsPerBlob + int(cellIdx)
if proofIdx < len(ptx.CellSidecar.Proofs) {
proof := ptx.CellSidecar.Proofs[proofIdx]
blobProofs[i] = &proof
}
}
}
for _, idx := range indices {
cells[idx] = blobCells
proofs[idx] = blobProofs
}
}
}
return cells, proofs, nil
}
// availableBlobs returns whether the blobs are available in the subpool. // availableBlobs returns whether the blobs are available in the subpool.
func (p *BlobPool) availableBlobs(vhashes []common.Hash) []bool { func (p *BlobPool) availableBlobs(vhashes []common.Hash) []bool {
available := make([]bool, len(vhashes)) available := make([]bool, len(vhashes))
@ -1718,14 +1960,19 @@ func (p *BlobPool) Add(txs []*types.Transaction, sync bool) []error {
if errs[i] = p.ValidateTxBasics(tx); errs[i] != nil { if errs[i] = p.ValidateTxBasics(tx); errs[i] != nil {
continue continue
} }
errs[i] = p.add(tx) ptx, err := p.cQueue.convert(tx)
if err != nil {
errs[i] = err
continue
}
errs[i] = p.AddPooledTx(ptx)
} }
return errs return errs
} }
// add inserts a new blob transaction into the pool if it passes validation (both // add inserts a new blob transaction into the pool if it passes validation (both
// consensus validity and pool restrictions). // consensus validity and pool restrictions).
func (p *BlobPool) add(tx *types.Transaction) (err error) { func (p *BlobPool) AddPooledTx(ptx *BlobTxForPool) (err error) {
// The blob pool blocks on adding a transaction. This is because blob txs are // The blob pool blocks on adding a transaction. This is because blob txs are
// only even pulled from the network, so this method will act as the overload // only even pulled from the network, so this method will act as the overload
// protection for fetches. // protection for fetches.
@ -1738,13 +1985,15 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) {
addtimeHist.Update(time.Since(start).Nanoseconds()) addtimeHist.Update(time.Since(start).Nanoseconds())
}(time.Now()) }(time.Now())
return p.addLocked(tx, true) return p.addLocked(ptx, true)
} }
// addLocked inserts a new blob transaction into the pool if it passes validation (both // addLocked inserts a new blob transaction into the pool if it passes validation (both
// consensus validity and pool restrictions). It must be called with the pool lock held. // consensus validity and pool restrictions). It must be called with the pool lock held.
// Only for internal use. // Only for internal use.
func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error) { func (p *BlobPool) addLocked(ptx *BlobTxForPool, checkGapped bool) (err error) {
tx := ptx.Tx
// Ensure the transaction is valid from all perspectives // Ensure the transaction is valid from all perspectives
if err := p.validateTx(tx); err != nil { if err := p.validateTx(tx); err != nil {
log.Trace("Transaction validation failed", "hash", tx.Hash(), "err", err) log.Trace("Transaction validation failed", "hash", tx.Hash(), "err", err)
@ -1761,7 +2010,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
from, _ := types.Sender(p.signer, tx) from, _ := types.Sender(p.signer, tx)
allowance := p.gappedAllowance(from) allowance := p.gappedAllowance(from)
if allowance >= 1 && len(p.gappedSource) < maxGapped { if allowance >= 1 && len(p.gappedSource) < maxGapped {
p.gapped[from] = append(p.gapped[from], tx) p.gapped[from] = append(p.gapped[from], ptx)
p.gappedSource[tx.Hash()] = from p.gappedSource[tx.Hash()] = from
gappedGauge.Update(int64(len(p.gappedSource))) gappedGauge.Update(int64(len(p.gappedSource)))
log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) log.Trace("added tx to gapped blob queue", "allowance", allowance, "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from]))
@ -1785,6 +2034,10 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
} }
return err return err
} }
//todo: validation happens twice for eth72
if err := txpool.ValidateCells(ptx.CellSidecar); err != nil {
return err
}
// If the address is not yet known, request exclusivity to track the account // If the address is not yet known, request exclusivity to track the account
// only by this subpool until all transactions are evicted // only by this subpool until all transactions are evicted
from, _ := types.Sender(p.signer, tx) // already validated above from, _ := types.Sender(p.signer, tx) // already validated above
@ -1807,7 +2060,6 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
} }
// Transaction permitted into the pool from a nonce and cost perspective, // Transaction permitted into the pool from a nonce and cost perspective,
// insert it into the database and update the indices // insert it into the database and update the indices
ptx := newBlobTxForPool(tx)
blob, err := rlp.EncodeToBytes(ptx) blob, err := rlp.EncodeToBytes(ptx)
if err != nil { if err != nil {
log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err) log.Error("Failed to encode transaction for storage", "hash", tx.Hash(), "err", err)
@ -1817,7 +2069,7 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
if err != nil { if err != nil {
return err return err
} }
meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), ptx) meta := newBlobTxMeta(id, p.store.Size(id), ptx)
var ( var (
next = p.state.GetNonce(from) next = p.state.GetNonce(from)
@ -1928,13 +2180,13 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
// We have to add in nonce order, but we want to stable sort to cater for situations // We have to add in nonce order, but we want to stable sort to cater for situations
// where transactions are replaced, keeping the original receive order for same nonce // where transactions are replaced, keeping the original receive order for same nonce
sort.SliceStable(gtxs, func(i, j int) bool { sort.SliceStable(gtxs, func(i, j int) bool {
return gtxs[i].Nonce() < gtxs[j].Nonce() return gtxs[i].Tx.Nonce() < gtxs[j].Tx.Nonce()
}) })
for len(gtxs) > 0 { for len(gtxs) > 0 {
stateNonce := p.state.GetNonce(from) stateNonce := p.state.GetNonce(from)
firstgap := stateNonce + uint64(len(p.index[from])) firstgap := stateNonce + uint64(len(p.index[from]))
if gtxs[0].Nonce() > firstgap { if gtxs[0].Tx.Nonce() > firstgap {
// Anything beyond the first gap is not addable yet // Anything beyond the first gap is not addable yet
break break
} }
@ -1942,26 +2194,26 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
// Drop any buffered transactions that became stale in the meantime (included in chain or replaced) // Drop any buffered transactions that became stale in the meantime (included in chain or replaced)
// If we arrive to the transaction in the pending range (between the state Nonce and first gap, we // If we arrive to the transaction in the pending range (between the state Nonce and first gap, we
// try to add them now while removing from here. // try to add them now while removing from here.
tx := gtxs[0] ptx := gtxs[0]
gtxs[0] = nil gtxs[0] = nil
gtxs = gtxs[1:] gtxs = gtxs[1:]
delete(p.gappedSource, tx.Hash()) delete(p.gappedSource, ptx.Tx.Hash())
if tx.Nonce() < stateNonce { if ptx.Tx.Nonce() < stateNonce {
// Stale, drop it. Eventually we could add to limbo here if hash matches. // Stale, drop it. Eventually we could add to limbo here if hash matches.
log.Trace("Gapped blob transaction became stale", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "state", stateNonce, "qlen", len(p.gapped[from])) log.Trace("Gapped blob transaction became stale", "hash", ptx.Tx.Hash(), "from", from, "nonce", ptx.Tx.Nonce(), "state", stateNonce, "qlen", len(p.gapped[from]))
continue continue
} }
if tx.Nonce() <= firstgap { if ptx.Tx.Nonce() <= firstgap {
// If we hit the pending range, including the first gap, add it and continue to try to add more. // If we hit the pending range, including the first gap, add it and continue to try to add more.
// We do not recurse here, but continue to loop instead. // We do not recurse here, but continue to loop instead.
// We are under lock, so we can add the transaction directly. // We are under lock, so we can add the transaction directly.
if err := p.addLocked(tx, false); err == nil { if err := p.addLocked(ptx, false); err == nil {
gappedPromotedMeter.Mark(1) gappedPromotedMeter.Mark(1)
log.Trace("Gapped blob transaction added to pool", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "qlen", len(p.gapped[from])) log.Trace("Gapped blob transaction added to pool", "hash", ptx.Tx.Hash(), "from", from, "nonce", ptx.Tx.Nonce(), "qlen", len(p.gapped[from]))
} else { } else {
log.Trace("Gapped blob transaction not accepted", "hash", tx.Hash(), "from", from, "nonce", tx.Nonce(), "err", err) log.Trace("Gapped blob transaction not accepted", "hash", ptx.Tx.Hash(), "from", from, "nonce", ptx.Tx.Nonce(), "err", err)
} }
} }
} }
@ -2087,6 +2339,10 @@ func (p *BlobPool) Pending(filter txpool.PendingFilter) (map[common.Address][]*t
break // execution gas limit is too high break // execution gas limit is too high
} }
} }
// Skip transactions without enough cells to recover blobs
if !filter.PartialCells && tx.custody != nil && tx.custody.OneCount() < kzg4844.DataPerBlob {
break // not enough cells to build a full payload, discard rest of txs from the account
}
// Transaction was accepted according to the filter, append to the pending list // Transaction was accepted according to the filter, append to the pending list
lazies = append(lazies, &txpool.LazyTransaction{ lazies = append(lazies, &txpool.LazyTransaction{
Pool: p, Pool: p,
@ -2229,10 +2485,10 @@ func (p *BlobPool) evictGapped() {
// and we overwrite the slice for this account after filtering. // and we overwrite the slice for this account after filtering.
keep := txs[:0] keep := txs[:0]
for i, gtx := range txs { for i, gtx := range txs {
if gtx.Time().Before(cutoff) || gtx.Nonce() < nonce { if gtx.Tx.Time().Before(cutoff) || gtx.Tx.Nonce() < nonce {
// Evict old or stale transactions // Evict old or stale transactions
// Should we add stale to limbo here if it would belong? // Should we add stale to limbo here if it would belong?
delete(p.gappedSource, gtx.Hash()) delete(p.gappedSource, gtx.Tx.Hash())
txs[i] = nil // Explicitly nil out evicted element txs[i] = nil // Explicitly nil out evicted element
} else { } else {
keep = append(keep, gtx) keep = append(keep, gtx)
@ -2355,7 +2611,7 @@ func (p *BlobPool) Clear() {
// Reset counters and the gapped buffer // Reset counters and the gapped buffer
p.stored = 0 p.stored = 0
p.gapped = make(map[common.Address][]*types.Transaction) p.gapped = make(map[common.Address][]*BlobTxForPool)
p.gappedSource = make(map[common.Hash]common.Address) p.gappedSource = make(map[common.Hash]common.Address)
var ( var (
@ -2364,3 +2620,13 @@ func (p *BlobPool) Clear() {
) )
p.evict = newPriceHeap(basefee, blobfee, p.index) p.evict = newPriceHeap(basefee, blobfee, p.index)
} }
// GetCustody returns the custody bitmap for a given transaction hash.
func (p *BlobPool) GetCustody(hash common.Hash) *types.CustodyBitmap {
p.lock.RLock()
defer p.lock.RUnlock()
if meta := p.lookup.txIndex[hash]; meta != nil {
return &meta.custody
}
return nil
}

View file

@ -31,6 +31,7 @@ import (
"slices" "slices"
"sync" "sync"
"testing" "testing"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip1559"
@ -233,7 +234,8 @@ func makeTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64,
// 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 { func encodeForPool(tx *types.Transaction) []byte {
blob, _ := rlp.EncodeToBytes(newBlobTxForPool(tx)) ptx, _ := newBlobTxForPool(tx)
blob, _ := rlp.EncodeToBytes(ptx)
return blob return blob
} }
@ -267,6 +269,15 @@ func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCa
return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx) return types.MustSignNewTx(key, types.LatestSigner(params.MainnetChainConfig), blobtx)
} }
// removeBlobs returns a copy of tx with the blob payload removed from the
// sidecar, keeping commitments, proofs and cells intact (simulating what
// ETH/72 peers send).
func removeBlobs(tx *types.Transaction) *types.Transaction {
sidecar := tx.BlobTxSidecar().Copy()
sidecar.Blobs = nil
return tx.WithBlobTxSidecar(sidecar)
}
// makeUnsignedTx is a utility method to construct a random blob transaction // makeUnsignedTx is a utility method to construct a random blob transaction
// without signing it. // without signing it.
func makeUnsignedTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64) *types.BlobTx { func makeUnsignedTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCap uint64) *types.BlobTx {
@ -471,7 +482,7 @@ func verifyBlobRetrievals(t *testing.T, pool *BlobPool) {
// - 8. Fully duplicate transactions (matching hash) must be dropped // - 8. Fully duplicate transactions (matching hash) must be dropped
// - 9. Duplicate nonces from the same account must be dropped // - 9. Duplicate nonces from the same account must be dropped
func TestOpenDrops(t *testing.T) { func TestOpenDrops(t *testing.T) {
//log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true))) // log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelTrace, true)))
// Create a temporary folder for the persistent backend // Create a temporary folder for the persistent backend
storage := t.TempDir() storage := t.TempDir()
@ -498,75 +509,76 @@ func TestOpenDrops(t *testing.T) {
S: new(uint256.Int), S: new(uint256.Int),
}) })
blob, _ := rlp.EncodeToBytes(tx) blob, _ := rlp.EncodeToBytes(tx)
badsig, _ := store.Put(blob) badsig := tx.Hash()
store.Put(blob)
// Insert a sequence of transactions with a nonce gap in between to verify // Insert a sequence of transactions with a nonce gap in between to verify
// that anything gapped will get evicted (case 3). // that anything gapped will get evicted (case 3).
var ( var (
gapper, _ = crypto.GenerateKey() gapper, _ = crypto.GenerateKey()
valids = make(map[uint64]struct{}) valids = make(map[common.Hash]struct{})
gapped = make(map[uint64]struct{}) gapped = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{0, 1, 3, 4, 6, 7} { // first gap at #2, another at #5 for _, nonce := range []uint64{0, 1, 3, 4, 6, 7} { // first gap at #2, another at #5
tx := makeTx(nonce, 1, 1, 1, gapper) tx := makeTx(nonce, 1, 1, 1, gapper)
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
if nonce < 2 { if nonce < 2 {
valids[id] = struct{}{} valids[tx.Hash()] = struct{}{}
} else { } else {
gapped[id] = struct{}{} gapped[tx.Hash()] = struct{}{}
} }
} }
// Insert a sequence of transactions with a gapped starting nonce to verify // Insert a sequence of transactions with a gapped starting nonce to verify
// that the entire set will get dropped (case 3). // that the entire set will get dropped (case 3).
var ( var (
dangler, _ = crypto.GenerateKey() dangler, _ = crypto.GenerateKey()
dangling = make(map[uint64]struct{}) dangling = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{1, 2, 3} { // first gap at #0, all set dangling for _, nonce := range []uint64{1, 2, 3} { // first gap at #0, all set dangling
tx := makeTx(nonce, 1, 1, 1, dangler) tx := makeTx(nonce, 1, 1, 1, dangler)
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
dangling[id] = struct{}{} dangling[tx.Hash()] = struct{}{}
} }
// Insert a sequence of transactions with already passed nonces to verify // Insert a sequence of transactions with already passed nonces to verify
// that the entire set will get dropped (case 4). // that the entire set will get dropped (case 4).
var ( var (
filler, _ = crypto.GenerateKey() filler, _ = crypto.GenerateKey()
filled = make(map[uint64]struct{}) filled = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{0, 1, 2} { // account nonce at 3, all set filled for _, nonce := range []uint64{0, 1, 2} { // account nonce at 3, all set filled
tx := makeTx(nonce, 1, 1, 1, filler) tx := makeTx(nonce, 1, 1, 1, filler)
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
filled[id] = struct{}{} filled[tx.Hash()] = struct{}{}
} }
// Insert a sequence of transactions with partially passed nonces to verify // Insert a sequence of transactions with partially passed nonces to verify
// that the included part of the set will get dropped (case 4). // that the included part of the set will get dropped (case 4).
var ( var (
overlapper, _ = crypto.GenerateKey() overlapper, _ = crypto.GenerateKey()
overlapped = make(map[uint64]struct{}) overlapped = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{0, 1, 2, 3} { // account nonce at 2, half filled for _, nonce := range []uint64{0, 1, 2, 3} { // account nonce at 2, half filled
tx := makeTx(nonce, 1, 1, 1, overlapper) tx := makeTx(nonce, 1, 1, 1, overlapper)
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
if nonce >= 2 { if nonce >= 2 {
valids[id] = struct{}{} valids[tx.Hash()] = struct{}{}
} else { } else {
overlapped[id] = struct{}{} overlapped[tx.Hash()] = struct{}{}
} }
} }
// Insert a sequence of transactions with an underpriced first to verify that // Insert a sequence of transactions with an underpriced first to verify that
// the entire set will get dropped (case 5). // the entire set will get dropped (case 5).
var ( var (
underpayer, _ = crypto.GenerateKey() underpayer, _ = crypto.GenerateKey()
underpaid = make(map[uint64]struct{}) underpaid = make(map[common.Hash]struct{})
) )
for i := 0; i < 5; i++ { // make #0 underpriced for i := 0; i < 5; i++ { // make #0 underpriced
var tx *types.Transaction var tx *types.Transaction
@ -577,15 +589,15 @@ func TestOpenDrops(t *testing.T) {
} }
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
underpaid[id] = struct{}{} underpaid[tx.Hash()] = struct{}{}
} }
// Insert a sequence of transactions with an underpriced in between to verify // Insert a sequence of transactions with an underpriced in between to verify
// that it and anything newly gapped will get evicted (case 5). // that it and anything newly gapped will get evicted (case 5).
var ( var (
outpricer, _ = crypto.GenerateKey() outpricer, _ = crypto.GenerateKey()
outpriced = make(map[uint64]struct{}) outpriced = make(map[common.Hash]struct{})
) )
for i := 0; i < 5; i++ { // make #2 underpriced for i := 0; i < 5; i++ { // make #2 underpriced
var tx *types.Transaction var tx *types.Transaction
@ -596,18 +608,18 @@ func TestOpenDrops(t *testing.T) {
} }
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
if i < 2 { if i < 2 {
valids[id] = struct{}{} valids[tx.Hash()] = struct{}{}
} else { } else {
outpriced[id] = struct{}{} outpriced[tx.Hash()] = struct{}{}
} }
} }
// Insert a sequence of transactions fully overdrafted to verify that the // Insert a sequence of transactions fully overdrafted to verify that the
// entire set will get invalidated (case 6). // entire set will get invalidated (case 6).
var ( var (
exceeder, _ = crypto.GenerateKey() exceeder, _ = crypto.GenerateKey()
exceeded = make(map[uint64]struct{}) exceeded = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{0, 1, 2} { // nonce 0 overdrafts the account for _, nonce := range []uint64{0, 1, 2} { // nonce 0 overdrafts the account
var tx *types.Transaction var tx *types.Transaction
@ -618,14 +630,14 @@ func TestOpenDrops(t *testing.T) {
} }
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
exceeded[id] = struct{}{} exceeded[tx.Hash()] = struct{}{}
} }
// Insert a sequence of transactions partially overdrafted to verify that part // Insert a sequence of transactions partially overdrafted to verify that part
// of the set will get invalidated (case 6). // of the set will get invalidated (case 6).
var ( var (
overdrafter, _ = crypto.GenerateKey() overdrafter, _ = crypto.GenerateKey()
overdrafted = make(map[uint64]struct{}) overdrafted = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{0, 1, 2} { // nonce 1 overdrafts the account for _, nonce := range []uint64{0, 1, 2} { // nonce 1 overdrafts the account
var tx *types.Transaction var tx *types.Transaction
@ -636,44 +648,46 @@ func TestOpenDrops(t *testing.T) {
} }
blob := encodeForPool(tx) blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
if nonce < 1 { if nonce < 1 {
valids[id] = struct{}{} valids[tx.Hash()] = struct{}{}
} else { } else {
overdrafted[id] = struct{}{} overdrafted[tx.Hash()] = struct{}{}
} }
} }
// Insert a sequence of transactions overflowing the account cap to verify // Insert a sequence of transactions overflowing the account cap to verify
// that part of the set will get invalidated (case 7). // that part of the set will get invalidated (case 7).
var ( var (
overcapper, _ = crypto.GenerateKey() overcapper, _ = crypto.GenerateKey()
overcapped = make(map[uint64]struct{}) overcapped = make(map[common.Hash]struct{})
) )
for nonce := uint64(0); nonce < maxTxsPerAccount+3; nonce++ { for nonce := uint64(0); nonce < maxTxsPerAccount+3; nonce++ {
blob := encodeForPool(makeTx(nonce, 1, 1, 1, overcapper)) tx := makeTx(nonce, 1, 1, 1, overcapper)
blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
if nonce < maxTxsPerAccount { if nonce < maxTxsPerAccount {
valids[id] = struct{}{} valids[tx.Hash()] = struct{}{}
} else { } else {
overcapped[id] = struct{}{} overcapped[tx.Hash()] = struct{}{}
} }
} }
// Insert a batch of duplicated transactions to verify that only one of each // Insert a batch of duplicated transactions to verify that only one of each
// version will remain (case 8). // version will remain (case 8).
var ( var (
duplicater, _ = crypto.GenerateKey() duplicater, _ = crypto.GenerateKey()
duplicated = make(map[uint64]struct{}) duplicated = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{0, 1, 2} { for _, nonce := range []uint64{0, 1, 2} {
blob := encodeForPool(makeTx(nonce, 1, 1, 1, duplicater)) tx := makeTx(nonce, 1, 1, 1, duplicater)
blob := encodeForPool(tx)
for i := 0; i < int(nonce)+1; i++ { for i := 0; i < int(nonce)+1; i++ {
id, _ := store.Put(blob) store.Put(blob)
if i == 0 { if i == 0 {
valids[id] = struct{}{} valids[tx.Hash()] = struct{}{}
} else { } else {
duplicated[id] = struct{}{} duplicated[tx.Hash()] = struct{}{}
} }
} }
} }
@ -681,17 +695,18 @@ func TestOpenDrops(t *testing.T) {
// remain (case 9). // remain (case 9).
var ( var (
repeater, _ = crypto.GenerateKey() repeater, _ = crypto.GenerateKey()
repeated = make(map[uint64]struct{}) repeated = make(map[common.Hash]struct{})
) )
for _, nonce := range []uint64{0, 1, 2} { for _, nonce := range []uint64{0, 1, 2} {
for i := 0; i < int(nonce)+1; i++ { for i := 0; i < int(nonce)+1; i++ {
blob := encodeForPool(makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater)) tx := makeTx(nonce, 1, uint64(i)+1 /* unique hashes */, 1, repeater)
blob := encodeForPool(tx)
id, _ := store.Put(blob) store.Put(blob)
if i == 0 { if i == 0 {
valids[id] = struct{}{} valids[tx.Hash()] = struct{}{}
} else { } else {
repeated[id] = struct{}{} repeated[tx.Hash()] = struct{}{}
} }
} }
} }
@ -728,39 +743,41 @@ func TestOpenDrops(t *testing.T) {
// Verify that the malformed (case 1), badly signed (case 2) and gapped (case // Verify that the malformed (case 1), badly signed (case 2) and gapped (case
// 3) txs have been deleted from the pool // 3) txs have been deleted from the pool
alive := make(map[uint64]struct{}) alive := make(map[common.Hash]struct{})
for _, txs := range pool.index { for _, txs := range pool.index {
for _, tx := range txs { for _, tx := range txs {
switch tx.id { switch tx.id {
case malformed: case malformed:
t.Errorf("malformed RLP transaction remained in storage") t.Errorf("malformed RLP transaction remained in storage")
case badsig:
t.Errorf("invalidly signed transaction remained in storage")
default: default:
if _, ok := dangling[tx.id]; ok { if badsig == tx.hash {
t.Errorf("invalidly signed transaction remained in storage")
}
if _, ok := dangling[tx.hash]; ok {
t.Errorf("dangling transaction remained in storage: %d", tx.id) t.Errorf("dangling transaction remained in storage: %d", tx.id)
} else if _, ok := filled[tx.id]; ok { } else if _, ok := filled[tx.hash]; ok {
t.Errorf("filled transaction remained in storage: %d", tx.id) t.Errorf("filled transaction remained in storage: %d", tx.id)
} else if _, ok := overlapped[tx.id]; ok { } else if _, ok := overlapped[tx.hash]; ok {
t.Errorf("overlapped transaction remained in storage: %d", tx.id) t.Errorf("overlapped transaction remained in storage: %d", tx.id)
} else if _, ok := gapped[tx.id]; ok { } else if _, ok := gapped[tx.hash]; ok {
t.Errorf("gapped transaction remained in storage: %d", tx.id) t.Errorf("gapped transaction remained in storage: %d", tx.id)
} else if _, ok := underpaid[tx.id]; ok { } else if _, ok := underpaid[tx.hash]; ok {
t.Errorf("underpaid transaction remained in storage: %d", tx.id) t.Errorf("underpaid transaction remained in storage: %d", tx.id)
} else if _, ok := outpriced[tx.id]; ok { } else if _, ok := outpriced[tx.hash]; ok {
t.Errorf("outpriced transaction remained in storage: %d", tx.id) t.Errorf("outpriced transaction remained in storage: %d", tx.id)
} else if _, ok := exceeded[tx.id]; ok { } else if _, ok := exceeded[tx.hash]; ok {
t.Errorf("fully overdrafted transaction remained in storage: %d", tx.id) t.Errorf("fully overdrafted transaction remained in storage: %d", tx.id)
} else if _, ok := overdrafted[tx.id]; ok { } else if _, ok := overdrafted[tx.hash]; ok {
t.Errorf("partially overdrafted transaction remained in storage: %d", tx.id) t.Errorf("partially overdrafted transaction remained in storage: %d", tx.id)
} else if _, ok := overcapped[tx.id]; ok { } else if _, ok := overcapped[tx.hash]; ok {
t.Errorf("overcapped transaction remained in storage: %d", tx.id) t.Errorf("overcapped transaction remained in storage: %d", tx.id)
} else if _, ok := duplicated[tx.id]; ok { } else if _, ok := repeated[tx.hash]; ok {
t.Errorf("duplicated transaction remained in storage: %d", tx.id)
} else if _, ok := repeated[tx.id]; ok {
t.Errorf("repeated nonce transaction remained in storage: %d", tx.id) t.Errorf("repeated nonce transaction remained in storage: %d", tx.id)
} else { } else {
alive[tx.id] = struct{}{} if _, ok := alive[tx.hash]; ok {
t.Errorf("duplicated transaction remained in storage: %d", tx.id)
}
alive[tx.hash] = struct{}{}
} }
} }
} }
@ -769,14 +786,14 @@ func TestOpenDrops(t *testing.T) {
if len(alive) != len(valids) { if len(alive) != len(valids) {
t.Errorf("valid transaction count mismatch: have %d, want %d", len(alive), len(valids)) t.Errorf("valid transaction count mismatch: have %d, want %d", len(alive), len(valids))
} }
for id := range alive { for hash := range alive {
if _, ok := valids[id]; !ok { if _, ok := valids[hash]; !ok {
t.Errorf("extra transaction %d", id) t.Errorf("extra transaction %s", hash)
} }
} }
for id := range valids { for hash := range valids {
if _, ok := alive[id]; !ok { if _, ok := alive[hash]; !ok {
t.Errorf("missing transaction %d", id) t.Errorf("missing transaction %s", hash)
} }
} }
// Verify all the calculated pool internals. Interestingly, this is **not** // Verify all the calculated pool internals. Interestingly, this is **not**
@ -995,7 +1012,10 @@ func TestOpenCap(t *testing.T) {
keep = []common.Address{addr1, addr3} keep = []common.Address{addr1, addr3}
drop = []common.Address{addr2} drop = []common.Address{addr2}
size = 2 * (txAvgSize + blobSize + uint64(txBlobOverhead)) // After migration to pooledBlobTx, cells (128 x 2048 = 2*blobSize) replace blobs.
// The actual billy slot size for pooledBlobTx is 2*(blobSize+txBlobOverhead)+txAvgSize.
pooledSlotSize uint64 = 2*(blobSize+uint64(txBlobOverhead)) + txAvgSize
size = 2 * pooledSlotSize
) )
store.Put(blob1) store.Put(blob1)
store.Put(blob2) store.Put(blob2)
@ -1004,7 +1024,7 @@ func TestOpenCap(t *testing.T) {
// Verify pool capping twice: first by reducing the data cap, then restarting // Verify pool capping twice: first by reducing the data cap, then restarting
// with a high cap to ensure everything was persisted previously // with a high cap to ensure everything was persisted previously
for _, datacap := range []uint64{2 * (txAvgSize + blobSize + uint64(txBlobOverhead)), 1000 * (txAvgSize + blobSize + uint64(txBlobOverhead))} { for _, datacap := range []uint64{size, 1000 * pooledSlotSize} {
// Create a blob pool out of the pre-seeded data, but cap it to 2 blob transaction // Create a blob pool out of the pre-seeded data, but cap it to 2 blob transaction
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified) statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
@ -1315,6 +1335,14 @@ func TestLegacyTxConversion(t *testing.T) {
} }
defer pool.Close() defer pool.Close()
deadline := time.Now().Add(30 * time.Second)
for pool.Get(tx1.Hash()) == nil || pool.Get(tx2.Hash()) == nil {
if time.Now().After(deadline) {
t.Fatalf("legacy txs were not converted in time")
}
time.Sleep(10 * time.Millisecond)
}
// Both transactions should be retrievable. // Both transactions should be retrievable.
for _, want := range []*types.Transaction{tx1, tx2} { for _, want := range []*types.Transaction{tx1, tx2} {
got := pool.Get(want.Hash()) got := pool.Get(want.Hash())
@ -1331,7 +1359,7 @@ func TestLegacyTxConversion(t *testing.T) {
// Legacy formats should not exist on pool.store // Legacy formats should not exist on pool.store
pool.store.Iterate(func(id uint64, size uint32, blob []byte) { pool.store.Iterate(func(id uint64, size uint32, blob []byte) {
var ptx blobTxForPool var ptx BlobTxForPool
if err := rlp.DecodeBytes(blob, &ptx); err != nil { if err := rlp.DecodeBytes(blob, &ptx); err != nil {
t.Errorf("entry %d not in new blobTxForPool format: %v", id, err) t.Errorf("entry %d not in new blobTxForPool format: %v", id, err)
} }
@ -1340,6 +1368,75 @@ func TestLegacyTxConversion(t *testing.T) {
verifyPoolInternals(t, pool) verifyPoolInternals(t, pool)
} }
func TestLegacyLimboConversion(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
limbodir := filepath.Join(storage, limboedTransactionStore)
os.MkdirAll(limbodir, 0700)
key, _ := crypto.GenerateKey()
tx := makeMultiBlobTx(0, 1, 1000, 100, 2, 0, key)
store, err := billy.Open(billy.Options{Path: limbodir}, newSlotterEIP7594(testMaxBlobsPerBlock), nil)
if err != nil {
t.Fatalf("failed to open limbo billy: %v", err)
}
data, err := rlp.EncodeToBytes(&struct {
TxHash common.Hash
Block uint64
Tx *types.Transaction
}{TxHash: tx.Hash(), Block: 42, Tx: tx})
if err != nil {
t.Fatalf("failed to legacy-encode limbo entry: %v", err)
}
if _, err := store.Put(data); err != nil {
t.Fatalf("failed to put legacy limbo entry: %v", err)
}
store.Close()
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
statedb.Commit(0, true, false)
chain := &testBlockChain{
config: params.MainnetChainConfig,
basefee: uint256.NewInt(params.InitialBaseFee),
blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
statedb: statedb,
}
pool := New(Config{Datadir: storage}, chain, nil)
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
t.Fatalf("failed to create blob pool: %v", err)
}
defer pool.Close()
deadline := time.Now().Add(30 * time.Second)
for {
pool.lock.RLock()
_, ok := pool.limbo.index[tx.Hash()]
pool.lock.RUnlock()
if ok {
break
}
if time.Now().After(deadline) {
t.Fatalf("legacy limbo entry was not converted in time")
}
time.Sleep(10 * time.Millisecond)
}
pool.lock.Lock()
ptx, err := pool.limbo.pull(tx.Hash())
pool.lock.Unlock()
if err != nil {
t.Fatalf("failed to pull converted limbo entry: %v", err)
}
full, err := ptx.toTx()
if err != nil {
t.Fatalf("failed to reconstruct tx from converted limbo entry: %v", err)
}
if full.Hash() != tx.Hash() {
t.Fatalf("converted limbo tx hash mismatch: have %s, want %s", full.Hash(), tx.Hash())
}
}
// TestBlobCountLimit tests the blobpool enforced limits on the max blob count. // TestBlobCountLimit tests the blobpool enforced limits on the max blob count.
func TestBlobCountLimit(t *testing.T) { func TestBlobCountLimit(t *testing.T) {
var ( var (
@ -1389,7 +1486,7 @@ func TestBlobCountLimit(t *testing.T) {
// Check that first succeeds second fails. // Check that first succeeds second fails.
if errs[0] != nil { if errs[0] != nil {
t.Fatalf("expected tx with 7 blobs to succeed, got %v", errs[0]) t.Fatalf("expected tx with 7 blobs to succeed, got: %v", errs[0])
} }
if !errors.Is(errs[1], txpool.ErrTxBlobLimitExceeded) { if !errors.Is(errs[1], txpool.ErrTxBlobLimitExceeded) {
t.Fatalf("expected tx with 8 blobs to fail, got: %v", errs[1]) t.Fatalf("expected tx with 8 blobs to fail, got: %v", errs[1])
@ -2108,23 +2205,40 @@ func TestGetBlobs(t *testing.T) {
pool.Close() pool.Close()
} }
// TestEncodeForNetwork verifies that encodeForNetwork produces output identical // TestEncodeForNetwork verifies that encodeForNetwork produces the correct wire
// to rlp.EncodeToBytes on the original transaction, for both V0 and V1 sidecars. // encoding for each (sidecar version, eth protocol version) combination.
// - eth/69, eth/70: blobs recovered from cells, output matches the original tx
// - eth/72: blob payload omitted, output matches removeBlobs(tx)
func TestEncodeForNetwork(t *testing.T) { func TestEncodeForNetwork(t *testing.T) {
t.Run("v1", func(t *testing.T) { testEncodeForNetwork(t) }) cases := []struct {
name string
ethVer uint
}{
{"eth70", 70},
{"eth72", 72},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
testEncodeForNetwork(t, tc.ethVer)
})
}
} }
func testEncodeForNetwork(t *testing.T) { func testEncodeForNetwork(t *testing.T, ethVer uint) {
key, _ := crypto.GenerateKey() key, _ := crypto.GenerateKey()
tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key) tx := makeMultiBlobTx(0, 1, 1, 1, 1, 0, key)
wantRLP, err := rlp.EncodeToBytes(tx) wantTx := tx
if ethVer >= 72 {
wantTx = removeBlobs(tx)
}
wantRLP, err := rlp.EncodeToBytes(wantTx)
if err != nil { if err != nil {
t.Fatalf("failed to encode tx: %v", err) t.Fatalf("failed to encode tx: %v", err)
} }
storedRLP := encodeForPool(tx) storedRLP := encodeForPool(tx)
gotRLP, err := encodeForNetwork(storedRLP) gotRLP, err := encodeForNetwork(storedRLP, ethVer)
if err != nil { if err != nil {
t.Fatalf("encodeForNetwork failed: %v", err) t.Fatalf("encodeForNetwork failed: %v", err)
} }
@ -2195,7 +2309,8 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) {
b.Fatal(err) b.Fatal(err)
} }
statedb.AddBalance(addr, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified) statedb.AddBalance(addr, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
pool.add(tx) pooledTx, _ := newBlobTxForPool(tx)
pool.AddPooledTx(pooledTx)
} }
statedb.Commit(0, true, false) statedb.Commit(0, true, false)
defer pool.Close() defer pool.Close()
@ -2216,3 +2331,116 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) {
} }
} }
} }
func TestGetCells(t *testing.T) {
storage := t.TempDir()
os.MkdirAll(filepath.Join(storage, pendingTransactionStore), 0700)
store, _ := billy.Open(billy.Options{Path: filepath.Join(storage, pendingTransactionStore)}, newSlotterEIP7594(params.BlobTxMaxBlobs), nil)
var (
key1, _ = crypto.GenerateKey()
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
tx1 = makeMultiBlobTx(0, 1, 1000, 100, 3, 0, key1) // blobs [0, 3)
pooledTx1, _ = newBlobTxForPool(tx1)
blob1, _ = rlp.EncodeToBytes(pooledTx1)
)
store.Put(blob1)
store.Close()
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
statedb.Commit(0, true, false)
chain := &testBlockChain{
config: params.MainnetChainConfig,
basefee: uint256.NewInt(params.InitialBaseFee),
blobfee: uint256.NewInt(params.BlobTxMinBlobGasprice),
statedb: statedb,
}
pool := New(Config{Datadir: storage}, chain, nil)
if err := pool.Init(1, chain.CurrentBlock(), newReserver()); err != nil {
t.Fatalf("failed to create blob pool: %v", err)
}
defer pool.Close()
tests := []struct {
name string
hash common.Hash
mask types.CustodyBitmap
expectedLen int
shouldFail bool
}{
{
name: "Get cells with single index",
hash: tx1.Hash(),
mask: types.NewCustodyBitmap([]uint64{5}),
expectedLen: 3,
shouldFail: false,
},
{
name: "Get cells with all indices",
hash: tx1.Hash(),
mask: types.CustodyBitmapAll,
expectedLen: 384,
shouldFail: false,
},
{
name: "Get cells with no indices",
hash: tx1.Hash(),
mask: types.CustodyBitmap{},
expectedLen: 0,
shouldFail: false,
},
{
name: "Get cells for non-existent transaction",
hash: common.Hash{0x01, 0x02, 0x03},
mask: types.NewCustodyBitmap([]uint64{0, 1}),
expectedLen: 0,
shouldFail: true,
},
{
name: "Get cells with random indices",
hash: tx1.Hash(),
mask: types.NewRandomCustodyBitmap(8),
expectedLen: 24,
shouldFail: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
vhashes := pool.GetBlobHashes(tt.hash)
if tt.shouldFail {
if vhashes != nil {
t.Errorf("expected nil vhashes for non-existent tx")
}
return
}
if vhashes == nil {
t.Fatalf("expected vhashes, got nil")
}
blobCells, _, err := pool.GetBlobCells(vhashes, tt.mask)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Count total non-nil cells across all blobs
totalCells := 0
for _, bc := range blobCells {
for _, c := range bc {
if c != nil {
totalCells++
}
}
}
if totalCells != tt.expectedLen {
t.Errorf("expected %d cells, got %d", tt.expectedLen, totalCells)
}
})
}
}

View file

@ -0,0 +1,317 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package blobpool
import (
"cmp"
"fmt"
"slices"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
var (
blobBufferTxFirstCounter = metrics.NewRegisteredCounter("blobpool/buffer/txfirst", nil)
blobBufferCellsFirstCounter = metrics.NewRegisteredCounter("blobpool/buffer/cellsfirst", nil)
blobBufferTotalTx = metrics.NewRegisteredGauge("blobpool/buffer/txcount", nil)
blobBufferTotalCells = metrics.NewRegisteredGauge("blobpool/buffer/cellcount", nil)
)
const (
bufferLifetime = 2 * time.Minute
)
// PeerDelivery holds cells delivered by a single peer, in blob-major order.
type PeerDelivery struct {
Cells []kzg4844.Cell
Indices []uint64
}
type txEntry struct {
tx *types.Transaction
// Technically it is not required to store peer information to drop properly.
// This is mainly for per peer size limit check.
peer string
added time.Time
}
type cellEntry struct {
deliveries map[string]*PeerDelivery
custody types.CustodyBitmap
added time.Time
}
type BlobBuffer struct {
mu sync.Mutex
txs map[common.Hash]*txEntry
cells map[common.Hash]*cellEntry
completed []*BlobTxForPool
completedCount atomic.Int32
cb BlobBufferFunctions
}
type BlobBufferFunctions struct {
ValidateTx func(*types.Transaction) error
AddToPool func(*BlobTxForPool) error
DropPeer func(string)
}
func NewBlobBuffer(cb BlobBufferFunctions) *BlobBuffer {
return &BlobBuffer{
txs: make(map[common.Hash]*txEntry),
cells: make(map[common.Hash]*cellEntry),
cb: cb,
}
}
// Flush adds all completed entries to the pool and returns the hashes
// and corresponding errors (nil on success) for each attempted insert.
func (b *BlobBuffer) Flush() ([]common.Hash, []error) {
// Read the count first and return early if there is nothing to do.
// Flush is called very frequently from the blob fetcher so this
// optimization is warranted.
if b.completedCount.Load() == 0 {
return nil, nil
}
b.mu.Lock()
defer b.mu.Unlock()
txs := make([]common.Hash, len(b.completed))
errs := make([]error, len(b.completed))
for i, ptx := range b.completed {
txs[i] = ptx.Tx.Hash()
errs[i] = b.cb.AddToPool(ptx)
}
b.completed = nil
b.completedCount.Store(0)
return txs, errs
}
// AddTx buffers a blob transaction (without blobs) from an ETH/72 peer.
// If cells are already buffered, verification and pool insertion are attempted.
func (b *BlobBuffer) AddTx(txs []*types.Transaction, peer string) []error {
b.mu.Lock()
defer b.mu.Unlock()
defer b.updateMetrics()()
// First remove any timed-out entries.
b.evict()
errs := make([]error, len(txs))
for i, tx := range txs {
hash := tx.Hash()
sidecar := tx.BlobTxSidecar()
if sidecar == nil {
errs[i] = fmt.Errorf("blob transaction without sidecar")
continue
}
// tx validation (basic w/o lock)
// error will be handled by tx fetcher
if err := b.cb.ValidateTx(tx); err != nil {
errs[i] = err
continue
}
if entry, ok := b.cells[hash]; ok {
b.storeCompleted(hash, tx, entry)
continue
}
blobBufferTxFirstCounter.Inc(1)
b.txs[hash] = &txEntry{tx: tx, peer: peer, added: time.Now()}
}
return errs
}
// AddCells buffers per-peer cell deliveries from the blob fetcher.
// If the transaction is already buffered, verification and pool insertion are attempted.
func (b *BlobBuffer) AddCells(hash common.Hash, deliveries map[string]*PeerDelivery, custody types.CustodyBitmap) {
b.mu.Lock()
defer b.mu.Unlock()
defer b.updateMetrics()()
// First remove any timed-out entries.
b.evict()
b.cells[hash] = &cellEntry{
deliveries: deliveries,
custody: custody,
added: time.Now(),
}
if txe, ok := b.txs[hash]; ok {
b.storeCompleted(hash, txe.tx, b.cells[hash])
}
blobBufferCellsFirstCounter.Inc(1)
}
// storeCompleted verifies cells per-peer, sorts them, and schedules them for
// addition into the pool. The actual addition happens in Flush().
func (b *BlobBuffer) storeCompleted(hash common.Hash, tx *types.Transaction, cells *cellEntry) {
sidecar := tx.BlobTxSidecar()
// Per-peer cell verification
if badPeers := b.verifyCells(cells, sidecar); len(badPeers) > 0 {
b.dropPeers(badPeers)
delete(b.cells, hash)
delete(b.txs, hash)
}
blobCount := len(tx.BlobHashes())
sorted, custody := sortCells(cells, blobCount)
cellSidecar := types.BlobTxCellSidecar{
Version: sidecar.Version,
Commitments: sidecar.Commitments,
Proofs: sidecar.Proofs,
Cells: sorted,
Custody: custody,
}
pooledTx := &BlobTxForPool{
Tx: tx.WithoutBlobTxSidecar(),
CellSidecar: &cellSidecar,
}
b.completed = append(b.completed, pooledTx)
b.completedCount.Add(1)
delete(b.cells, hash)
delete(b.txs, hash)
}
func (b *BlobBuffer) HasTx(hash common.Hash) bool {
b.mu.Lock()
defer b.mu.Unlock()
_, ok := b.txs[hash]
return ok
}
func (b *BlobBuffer) HasCells(hash common.Hash) bool {
b.mu.Lock()
defer b.mu.Unlock()
_, ok := b.cells[hash]
return ok
}
func (b *BlobBuffer) dropPeers(peers []string) {
if b.cb.DropPeer == nil {
return
}
for _, p := range peers {
b.cb.DropPeer(p)
}
}
func (b *BlobBuffer) evict() {
now := time.Now()
for hash, entry := range b.txs {
if now.Sub(entry.added) > bufferLifetime {
delete(b.txs, hash)
}
}
for hash, entry := range b.cells {
if now.Sub(entry.added) > bufferLifetime {
delete(b.cells, hash)
}
}
}
// updateMetrics updates the metrics gauges.
// This should be called at the start of any operation that changes the buffer
// content. The returned function is to be called at the end of the operation,
// usually with defer.
func (b *BlobBuffer) updateMetrics() func() {
preTxCount := len(b.txs)
preCellsCount := len(b.cells)
return func() {
if len(b.txs) != preTxCount {
blobBufferTotalTx.Update(int64(len(b.txs)))
}
if len(b.cells) != preCellsCount {
blobBufferTotalCells.Update(int64(len(b.cells)))
}
}
}
// verifyCells verifies each peer's cells against the sidecar by treating each
// per-peer delivery as a mini BlobTxCellSidecar and reusing txpool.ValidateCells.
// Returns the list of peers whose cells failed verification.
func (b *BlobBuffer) verifyCells(entry *cellEntry, sidecar *types.BlobTxSidecar) []string {
var badPeers []string
for peer, delivery := range entry.deliveries {
perPeer := &types.BlobTxCellSidecar{
Version: sidecar.Version,
Cells: delivery.Cells,
Commitments: sidecar.Commitments,
Proofs: sidecar.Proofs,
Custody: types.NewCustodyBitmap(delivery.Indices),
}
if err := txpool.ValidateCells(perPeer); err != nil {
log.Debug("Cell verification failed", "peer", peer, "err", err)
badPeers = append(badPeers, peer)
}
}
return badPeers
}
// sortCells merges all per-peer deliveries into a single flat cell array
// sorted by custody index.
//
// e.g.
// peer A: cells = [blob0_cell5, blob0_cell3, blob1_cell5, blob1_cell3]
// peer B: cells = [blob0_cell1, blob0_cell7, blob1_cell1, blob1_cell7]
// -> [blob0_cell1, blob0_cell3, blob0_cell5, blob0_cell7, blob1_cell1, blob1_cell3, blob1_cell5, blob1_cell7]
func sortCells(entry *cellEntry, blobCount int) ([]kzg4844.Cell, types.CustodyBitmap) {
// indices per delivery
var indices []uint64
// 1. compose per blob cells
blob := make([][]kzg4844.Cell, blobCount)
for _, d := range entry.deliveries {
n := len(d.Indices)
indices = append(indices, d.Indices...)
for b := range blobCount {
blob[b] = append(blob[b], d.Cells[b*n:(b+1)*n]...)
}
}
// 2. sort
perm := make([]int, len(indices))
for i := range perm {
perm[i] = i
}
// perm represents the position of cells in sorted array
slices.SortFunc(perm, func(a, b int) int {
return cmp.Compare(indices[a], indices[b])
})
// reorder cells
var res []kzg4844.Cell
for b := range blobCount {
for _, p := range perm {
res = append(res, blob[b][p])
}
}
custody := types.NewCustodyBitmap(indices)
return res, custody
}

View file

@ -0,0 +1,206 @@
package blobpool
import (
"crypto/ecdsa"
"testing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
)
// makeV1Tx creates a V1 blob transaction with cell proofs, then strips blobs
// (simulating what ETH/72 peers send).
func makeV1Tx(t *testing.T, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey) *types.Transaction {
t.Helper()
tx := makeMultiBlobTx(nonce, 1, 1, 1, blobCount, blobOffset, key)
return removeBlobs(tx)
}
// makePeerDelivery creates a PeerDelivery for given cell indices from a set of blobs.
func makePeerDelivery(t *testing.T, blobOffset, blobCount int, indices []uint64) *PeerDelivery {
t.Helper()
var allCells []kzg4844.Cell
for i := 0; i < blobCount; i++ {
cells, err := kzg4844.ComputeCells([]kzg4844.Blob{*testBlobs[blobOffset+i]})
if err != nil {
t.Fatal(err)
}
allCells = append(allCells, cells...)
}
var deliveryCells []kzg4844.Cell
for b := 0; b < blobCount; b++ {
for _, idx := range indices {
deliveryCells = append(deliveryCells, allCells[b*kzg4844.CellsPerBlob+int(idx)])
}
}
return &PeerDelivery{Cells: deliveryCells, Indices: indices}
}
func newTestBuffer(t *testing.T) *BlobBuffer {
t.Helper()
return NewBlobBuffer(BlobBufferFunctions{
ValidateTx: func(tx *types.Transaction) error { return nil },
AddToPool: func(ptx *BlobTxForPool) error { return nil },
DropPeer: func(peer string) {},
})
}
func TestSortCells(t *testing.T) {
blobCount := 2
blobOffset := 0
peerA := makePeerDelivery(t, blobOffset, blobCount, []uint64{5, 3})
peerB := makePeerDelivery(t, blobOffset, blobCount, []uint64{1, 7})
custody := types.NewCustodyBitmap([]uint64{1, 3, 5, 7})
entry := &cellEntry{
deliveries: map[string]*PeerDelivery{
"peerA": peerA,
"peerB": peerB,
},
custody: custody,
}
sorted, resultCustody := sortCells(entry, blobCount)
resultIndices := resultCustody.Indices()
if len(resultIndices) != 4 {
t.Fatalf("expected 4 indices, got %d", len(resultIndices))
}
for i, expected := range []uint64{1, 3, 5, 7} {
if resultIndices[i] != expected {
t.Errorf("index %d: expected %d, got %d", i, expected, resultIndices[i])
}
}
expected := makePeerDelivery(t, blobOffset, blobCount, []uint64{1, 3, 5, 7})
if len(sorted) != len(expected.Cells) {
t.Fatalf("sorted length %d != expected %d", len(sorted), len(expected.Cells))
}
for i := range sorted {
if sorted[i] != expected.Cells[i] {
t.Errorf("cell %d mismatch", i)
}
}
}
func TestAddTxThenCells(t *testing.T) {
key, _ := crypto.GenerateKey()
blobCount := 2
buf := newTestBuffer(t)
tx := makeV1Tx(t, 0, blobCount, 0, key)
hash := tx.Hash()
if err := buf.AddTx([]*types.Transaction{tx}, "peerA")[0]; err != nil {
t.Fatal(err)
}
if !buf.HasTx(hash) {
t.Fatal("tx should be buffered")
}
dataIndices := make([]uint64, kzg4844.DataPerBlob)
for i := range dataIndices {
dataIndices[i] = uint64(i)
}
delivery := makePeerDelivery(t, 0, blobCount, dataIndices)
custody := types.NewCustodyBitmap(dataIndices)
buf.AddCells(hash, map[string]*PeerDelivery{"peerB": delivery}, custody)
if buf.HasTx(hash) || buf.HasCells(hash) {
t.Fatal("buffer should be empty after add")
}
}
func TestAddCellsThenTx(t *testing.T) {
key, _ := crypto.GenerateKey()
blobCount := 2
buf := newTestBuffer(t)
tx := makeV1Tx(t, 0, blobCount, 0, key)
hash := tx.Hash()
dataIndices := make([]uint64, kzg4844.DataPerBlob)
for i := range dataIndices {
dataIndices[i] = uint64(i)
}
delivery := makePeerDelivery(t, 0, blobCount, dataIndices)
custody := types.NewCustodyBitmap(dataIndices)
buf.AddCells(hash, map[string]*PeerDelivery{"peerB": delivery}, custody)
if !buf.HasCells(hash) {
t.Fatal("cells should be buffered")
}
if err := buf.AddTx([]*types.Transaction{tx}, "peerA")[0]; err != nil {
t.Fatal(err)
}
if buf.HasTx(hash) || buf.HasCells(hash) {
t.Fatal("buffer should be empty after add")
}
}
func TestMultiPeerDelivery(t *testing.T) {
key, _ := crypto.GenerateKey()
blobCount := 2
buf := newTestBuffer(t)
tx := makeV1Tx(t, 0, blobCount, 0, key)
hash := tx.Hash()
buf.AddTx([]*types.Transaction{tx}, "peerA")
indicesA := []uint64{0, 2, 4, 6}
indicesB := []uint64{1, 3, 5, 7}
deliveryA := makePeerDelivery(t, 0, blobCount, indicesA)
deliveryB := makePeerDelivery(t, 0, blobCount, indicesB)
allIndices := append(indicesA, indicesB...)
custody := types.NewCustodyBitmap(allIndices)
buf.AddCells(hash, map[string]*PeerDelivery{
"peerB": deliveryA,
"peerC": deliveryB,
}, custody)
if buf.HasTx(hash) || buf.HasCells(hash) {
t.Fatal("buffer should be empty after add")
}
}
func TestBadCell(t *testing.T) {
key, _ := crypto.GenerateKey()
blobCount := 1
var dropped []string
buf := NewBlobBuffer(BlobBufferFunctions{
ValidateTx: func(tx *types.Transaction) error { return nil },
AddToPool: func(ptx *BlobTxForPool) error { return nil },
DropPeer: func(peer string) { dropped = append(dropped, peer) },
})
tx := makeV1Tx(t, 0, blobCount, 0, key)
hash := tx.Hash()
buf.AddTx([]*types.Transaction{tx}, "peerA")
goodDelivery := makePeerDelivery(t, 0, blobCount, []uint64{0, 1, 2, 3})
badDelivery := makePeerDelivery(t, 0, blobCount, []uint64{4, 5, 6, 7})
for i := range badDelivery.Cells {
for j := range badDelivery.Cells[i] {
badDelivery.Cells[i][j] ^= 0xFF
}
}
allIndices := []uint64{0, 1, 2, 3, 4, 5, 6, 7}
custody := types.NewCustodyBitmap(allIndices)
buf.AddCells(hash, map[string]*PeerDelivery{
"peerB": goodDelivery,
"peerC": badDelivery,
}, custody)
if len(dropped) != 1 || dropped[0] != "peerC" {
t.Fatalf("only peerC should have been dropped, got: %v", dropped)
}
if buf.HasTx(hash) || buf.HasCells(hash) {
t.Fatal("buffer should be empty after bad cell drop")
}
}

View file

@ -36,7 +36,7 @@ import (
) )
const ( const (
topKTimeout = 4 * time.Second topKTimeout = 1 * time.Second
hasBlobsTimeout = 1 * time.Second hasBlobsTimeout = 1 * time.Second
) )
@ -46,12 +46,14 @@ var (
// missing on the lower level (in this case, the blobpool). The amount that // missing on the lower level (in this case, the blobpool). The amount that
// we failed to predict) can be calculated with the telemetry span // we failed to predict) can be calculated with the telemetry span
// (blobs.filled - cache.hit). // (blobs.filled - cache.hit).
cacheHitMeter = metrics.NewRegisteredMeter("blobpool/cache/hit", nil) cacheHitMeter = metrics.NewRegisteredMeter("blobpool/cache/hit", nil)
cacheMissMeter = metrics.NewRegisteredMeter("blobpool/cache/miss", nil) cacheMissMeter = metrics.NewRegisteredMeter("blobpool/cache/miss", nil)
cacheBlobsGauge = metrics.NewRegisteredGauge("blobpool/cache/blobs", nil) cacheEntriesGauge = metrics.NewRegisteredGauge("blobpool/cache/entries", nil)
) )
type cachedBlob struct { type cachedBlob struct {
cell []kzg4844.Cell
custody types.CustodyBitmap
blob *kzg4844.Blob blob *kzg4844.Blob
commitment kzg4844.Commitment commitment kzg4844.Commitment
proofs []kzg4844.Proof proofs []kzg4844.Proof
@ -74,11 +76,16 @@ type Cache struct {
mu sync.Mutex mu sync.Mutex
entries map[common.Hash]*cachedBlob entries map[common.Hash]*cachedBlob
// needCell is owned by the loop goroutine; it is only read and written
// there, so it needs no synchronization. It is flipped on via enableCellCh.
needCell bool
// channels into loop // channels into loop
quit chan struct{} quit chan struct{}
topkRequest chan struct{} topkRequest chan struct{}
topkTimer mclock.Timer topkTimer mclock.Timer
hasBlobsCh chan []common.Hash // list of tx hashes that should be pinned hasBlobsCh chan []common.Hash // list of tx hashes that should be pinned
cellModeCh chan bool // signals the loop to switch cell mode on/offo
step func() // test hook fired after each loop iteration step func() // test hook fired after each loop iteration
@ -103,6 +110,7 @@ func newCache(p *BlobPool, clock mclock.Clock, step func()) *Cache {
step: step, step: step,
quit: make(chan struct{}), quit: make(chan struct{}),
topkRequest: make(chan struct{}, 1), topkRequest: make(chan struct{}, 1),
cellModeCh: make(chan bool, 1),
} }
c.wg.Add(1) c.wg.Add(1)
@ -197,13 +205,10 @@ func (c *Cache) GetBlobs(ctx context.Context, vhashes []common.Hash, version byt
c.mu.Lock() c.mu.Lock()
for vhash, idxs := range indices { for vhash, idxs := range indices {
n := len(idxs) n := len(idxs)
cached, ok := c.entries[vhash]
cached := c.entries[vhash] if !ok || cached.version != version || cached.blob == nil {
if cached == nil || cached.version != version {
cacheMiss += n cacheMiss += n
if cached == nil { misses = append(misses, vhash)
misses = append(misses, vhash)
}
continue continue
} }
cacheHits += n cacheHits += n
@ -241,6 +246,82 @@ func (c *Cache) GetBlobs(ctx context.Context, vhashes []common.Hash, version byt
return blobs, commitments, proofs, nil return blobs, commitments, proofs, nil
} }
// GetCells returns cells for the given versioned blob hashes, filtered
// by the requested cell indices (mask). Each entry in the result
// corresponds to one vhash. Nil entries mean the cell was not available.
// If the cell is not available in cache, it falls back to the blobpool.
func (c *Cache) GetCells(vhashes []common.Hash, mask types.CustodyBitmap) ([][]*kzg4844.Cell, [][]*kzg4844.Proof, error) {
var (
cells = make([][]*kzg4844.Cell, len(vhashes))
proofs = make([][]*kzg4844.Proof, len(vhashes))
indices = make(map[common.Hash][]int)
misses []common.Hash
)
for i, h := range vhashes {
indices[h] = append(indices[h], i)
}
requested := mask.Indices()
c.mu.Lock()
for vhash, idxs := range indices {
cached, ok := c.entries[vhash]
if !ok {
misses = append(misses, vhash)
continue
}
stored := cached.custody.Indices()
blobCells := make([]*kzg4844.Cell, len(requested))
blobProofs := make([]*kzg4844.Proof, len(requested))
for i, cellIdx := range requested {
pos := -1
for k, s := range stored {
if s == cellIdx {
pos = k
break
}
}
if pos >= 0 && pos < len(cached.cell) {
cell := cached.cell[pos]
blobCells[i] = &cell
if int(cellIdx) < len(cached.proofs) {
pf := cached.proofs[cellIdx]
blobProofs[i] = &pf
}
}
}
for _, idx := range idxs {
cells[idx] = blobCells
proofs[idx] = blobProofs
}
}
c.mu.Unlock()
if len(misses) > 0 {
mc, mp, err := c.blobpool.GetBlobCells(misses, mask)
if err != nil {
return nil, nil, err
}
for j, vhash := range misses {
for _, idx := range indices[vhash] {
cells[idx] = mc[j]
proofs[idx] = mp[j]
}
}
}
return cells, proofs, nil
}
// EnableCell allows the cache to store only cells without recovering
// blobs. This means we can also cache cells that lack enough blobs to
// recover. It signals the loop to switch to cell mode and re-select
// transactions from this wider pool.
func (c *Cache) SetCellMode(on bool) {
select {
case c.cellModeCh <- on:
case <-c.quit:
}
}
func (c *Cache) loop() { func (c *Cache) loop() {
defer c.wg.Done() defer c.wg.Done()
@ -258,6 +339,14 @@ func (c *Cache) loop() {
c.update(want) c.update(want)
c.triggerTopKAfter(topKTimeout) c.triggerTopKAfter(topKTimeout)
case on := <-c.cellModeCh:
// This runs when the CL signals (non-)support for cell proofs. Enable/disable
// cell mode and re-select immediately to force an update.
if c.needCell != on {
c.needCell = on
c.triggerTopK()
}
case <-c.quit: case <-c.quit:
c.cancelUpdate() c.cancelUpdate()
if c.topkTimer != nil { if c.topkTimer != nil {
@ -285,6 +374,8 @@ func (c *Cache) cancelUpdate() {
// are no longer wanted and loads the missing ones from the blobpool in the // are no longer wanted and loads the missing ones from the blobpool in the
// background. // background.
func (c *Cache) update(want []common.Hash) { func (c *Cache) update(want []common.Hash) {
cellMode := c.needCell
wantSet := make(map[common.Hash]struct{}, len(want)) wantSet := make(map[common.Hash]struct{}, len(want))
for _, vh := range want { for _, vh := range want {
wantSet[vh] = struct{}{} wantSet[vh] = struct{}{}
@ -298,7 +389,13 @@ func (c *Cache) update(want []common.Hash) {
c.mu.Lock() c.mu.Lock()
var missing []common.Hash var missing []common.Hash
for vh := range wantSet { for vh := range wantSet {
if _, ok := c.entries[vh]; !ok { e, ok := c.entries[vh]
if ok && ((cellMode && e.cell == nil) || (!cellMode && e.blob == nil)) {
delete(c.entries, vh)
cacheEntriesGauge.Dec(1)
ok = false
}
if !ok {
missing = append(missing, vh) missing = append(missing, vh)
} }
} }
@ -307,7 +404,7 @@ func (c *Cache) update(want []common.Hash) {
continue continue
} }
delete(c.entries, vh) delete(c.entries, vh)
cacheBlobsGauge.Dec(1) cacheEntriesGauge.Dec(1)
} }
c.mu.Unlock() c.mu.Unlock()
@ -330,37 +427,92 @@ func (c *Cache) update(want []common.Hash) {
if ptx == nil { if ptx == nil {
continue continue
} }
sidecar := ptx.Sidecar() if cellMode {
if sidecar == nil { c.loadCells(ptx, wantSet)
continue } else {
c.loadBlobs(ptx, wantSet)
} }
c.mu.Lock()
for i, v := range sidecar.BlobHashes() {
if _, ok := wantSet[v]; !ok {
continue
}
if _, exists := c.entries[v]; exists {
continue // recompute only new entries
}
cellProofs, err := sidecar.CellProofsAt(i)
if err != nil {
log.Error("Failed to get cell proofs", "txhash", ptx.Tx.Hash(), "err", err)
continue
}
c.entries[v] = &cachedBlob{
blob: &sidecar.Blobs[i],
commitment: sidecar.Commitments[i],
proofs: cellProofs,
version: sidecar.Version,
}
cacheBlobsGauge.Inc(1)
}
c.mu.Unlock()
} }
}() }()
} }
// loadCells loads the cells whose vhash is in wantSet from ptx.
func (c *Cache) loadCells(ptx *BlobTxForPool, wantSet map[common.Hash]struct{}) {
cs := ptx.CellSidecar
cellsPerBlob := cs.Custody.OneCount()
c.mu.Lock()
defer c.mu.Unlock()
for i, v := range ptx.Tx.BlobHashes() {
if _, ok := wantSet[v]; !ok {
continue
}
if _, exists := c.entries[v]; exists {
continue
}
cellStart := i * cellsPerBlob
if cellStart+cellsPerBlob > len(cs.Cells) {
continue
}
blobCells := make([]kzg4844.Cell, cellsPerBlob)
copy(blobCells, cs.Cells[cellStart:cellStart+cellsPerBlob])
var pf []kzg4844.Proof
if ps := i * kzg4844.CellProofsPerBlob; ps+kzg4844.CellProofsPerBlob <= len(cs.Proofs) {
pf = make([]kzg4844.Proof, kzg4844.CellProofsPerBlob)
copy(pf, cs.Proofs[ps:ps+kzg4844.CellProofsPerBlob])
}
c.entries[v] = &cachedBlob{
cell: blobCells,
custody: cs.Custody,
commitment: cs.Commitments[i],
proofs: pf,
version: cs.Version,
}
cacheEntriesGauge.Inc(1)
}
}
// loadBlobs loads the blobs whose vhash is in wantSet from ptx.
func (c *Cache) loadBlobs(ptx *BlobTxForPool, wantSet map[common.Hash]struct{}) {
if ptx.CellSidecar.Custody.OneCount() < kzg4844.DataPerBlob {
return
}
// blobs will be computed inside of sidecar()
sidecar, err := ptx.sidecar()
if err != nil || sidecar == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
for i, v := range sidecar.BlobHashes() {
if _, ok := wantSet[v]; !ok {
continue
}
if _, exists := c.entries[v]; exists {
continue
}
var pf []kzg4844.Proof
switch sidecar.Version {
case types.BlobSidecarVersion0:
pf = []kzg4844.Proof{sidecar.Proofs[i]}
case types.BlobSidecarVersion1:
cellProofs, err := sidecar.CellProofsAt(i)
if err != nil {
log.Error("Failed to get cell proofs", "txhash", ptx.Tx.Hash(), "err", err)
continue
}
pf = cellProofs
}
c.entries[v] = &cachedBlob{
blob: &sidecar.Blobs[i],
commitment: sidecar.Commitments[i],
proofs: pf,
version: sidecar.Version,
}
cacheEntriesGauge.Inc(1)
}
}
// selectTopTxs returns the vhashes of the top K most profitable pending blob // selectTopTxs returns the vhashes of the top K most profitable pending blob
// transactions, up to the active fork's maxBlobsPerBlock. // transactions, up to the active fork's maxBlobsPerBlock.
func (c *Cache) selectTopTxs() []common.Hash { func (c *Cache) selectTopTxs() []common.Hash {
@ -373,8 +525,9 @@ func (c *Cache) selectTopTxs() []common.Hash {
baseFee := eip1559.CalcBaseFee(config, head) baseFee := eip1559.CalcBaseFee(config, head)
filter := txpool.PendingFilter{ filter := txpool.PendingFilter{
BlobTxs: true, BlobTxs: true,
BaseFee: uint256.MustFromBig(baseFee), BaseFee: uint256.MustFromBig(baseFee),
PartialCells: c.needCell,
} }
if head.ExcessBlobGas != nil { if head.ExcessBlobGas != nil {
filter.BlobFee = uint256.MustFromBig(eip4844.CalcBlobFee(config, head)) filter.BlobFee = uint256.MustFromBig(eip4844.CalcBlobFee(config, head))

View file

@ -146,7 +146,10 @@ func (tc *testCache) inject(t *testing.T, spec txSpec) []common.Hash {
tx := makeMultiBlobTx(0, spec.tip, 1_000_000, 1_000_000, spec.blobs, tc.offset, key) tx := makeMultiBlobTx(0, spec.tip, 1_000_000, 1_000_000, spec.blobs, tc.offset, key)
tc.offset += spec.blobs tc.offset += spec.blobs
ptx := newBlobTxForPool(tx) ptx, err := newBlobTxForPool(tx)
if err != nil {
t.Fatalf("new blob tx for pool: %v", err)
}
tc.blobpool.lock.Lock() tc.blobpool.lock.Lock()
defer tc.blobpool.lock.Unlock() defer tc.blobpool.lock.Unlock()
@ -155,7 +158,7 @@ func (tc *testCache) inject(t *testing.T, spec txSpec) []common.Hash {
if err != nil { if err != nil {
t.Fatalf("store put: %v", err) t.Fatalf("store put: %v", err)
} }
meta := newBlobTxMeta(id, ptx.TxSize(), tc.blobpool.store.Size(id), ptx) meta := newBlobTxMeta(id, tc.blobpool.store.Size(id), ptx)
addr := crypto.PubkeyToAddress(key.PublicKey) addr := crypto.PubkeyToAddress(key.PublicKey)
tc.blobpool.index[addr] = append(tc.blobpool.index[addr], meta) tc.blobpool.index[addr] = append(tc.blobpool.index[addr], meta)
tc.blobpool.lookup.track(meta) tc.blobpool.lookup.track(meta)

View file

@ -25,6 +25,8 @@ type Config struct {
Datadir string // Data directory containing the currently executable blobs Datadir string // Data directory containing the currently executable blobs
Datacap uint64 // Soft-cap of database storage (hard cap is larger due to overhead) Datacap uint64 // Soft-cap of database storage (hard cap is larger due to overhead)
PriceBump uint64 // Minimum price bump percentage to replace an already existing nonce PriceBump uint64 // Minimum price bump percentage to replace an already existing nonce
FetchProbability uint64 // EIP-8070: full blob fetch probability for sparse blobpool
} }
// DefaultConfig contains the default configurations for the transaction pool. // DefaultConfig contains the default configurations for the transaction pool.

View file

@ -0,0 +1,207 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package blobpool
import (
"errors"
"slices"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
// maxPendingConversionTasks caps the number of pending conversion tasks. This
// prevents excessive memory usage; the worst-case scenario (2k transactions
// with 6 blobs each) would consume approximately 1.5GB of memory.
const maxPendingConversionTasks = 2048
type convertResult struct {
ptx *BlobTxForPool
err error
}
// txConvert represents a conversion task with an attached legacy blob transaction.
type txConvert struct {
tx *types.Transaction // Legacy blob transaction
done chan convertResult // Channel for signaling back if the conversion succeeds
}
// conversionQueue is a dedicated queue for converting legacy blob transactions
// received from the network after the Osaka fork. Since conversion is expensive,
// it is performed in the background by a single thread, ensuring the main Geth
// process is not overloaded.
type conversionQueue struct {
tasks chan *txConvert
startConversion chan func()
quit chan struct{}
closed chan struct{}
queue []func()
taskDone chan struct{}
}
// newConversionQueue constructs the conversion queue.
func newConversionQueue() *conversionQueue {
q := &conversionQueue{
tasks: make(chan *txConvert),
startConversion: make(chan func()),
quit: make(chan struct{}),
closed: make(chan struct{}),
}
go q.loop()
return q
}
// convert accepts a legacy blob transaction with version-0 blobs and queues it
// for conversion.
//
// This function may block for a long time until the transaction is processed.
func (q *conversionQueue) convert(tx *types.Transaction) (*BlobTxForPool, error) {
done := make(chan convertResult, 1)
select {
case q.tasks <- &txConvert{tx: tx, done: done}:
res := <-done
return res.ptx, res.err
case <-q.closed:
return nil, errors.New("conversion queue closed")
}
}
// launchConversion starts a conversion task in the background.
func (q *conversionQueue) launchConversion(fn func()) error {
select {
case q.startConversion <- fn:
return nil
case <-q.closed:
return errors.New("conversion queue closed")
}
}
// close terminates the conversion queue.
func (q *conversionQueue) close() {
select {
case <-q.closed:
return
default:
close(q.quit)
<-q.closed
}
}
// run converts a batch of legacy blob txs to the new cell proof format.
func (q *conversionQueue) run(tasks []*txConvert, done chan struct{}, interrupt *atomic.Int32) {
defer close(done)
for _, t := range tasks {
if interrupt != nil && interrupt.Load() != 0 {
t.done <- convertResult{err: errors.New("conversion is interrupted")}
continue
}
// Run the conversion, the original sidecar will be mutated in place
start := time.Now()
ptx, err := newBlobTxForPool(t.tx)
t.done <- convertResult{ptx: ptx, err: err}
log.Trace("Converted legacy blob tx", "hash", t.tx.Hash(), "err", err, "elapsed", common.PrettyDuration(time.Since(start)))
}
}
func (q *conversionQueue) loop() {
defer close(q.closed)
var (
done chan struct{} // Non-nil if background routine is active
interrupt *atomic.Int32 // Flag to signal conversion interruption
// The pending tasks for sidecar conversion. We assume the number of legacy
// blob transactions requiring conversion will not be excessive. However,
// a hard cap is applied as a protective measure.
txTasks []*txConvert
)
for {
select {
case t := <-q.tasks:
if len(txTasks) >= maxPendingConversionTasks {
t.done <- convertResult{err: errors.New("conversion queue is overloaded")}
continue
}
txTasks = append(txTasks, t)
// Launch the background conversion thread if it's idle
if done == nil {
done, interrupt = make(chan struct{}), new(atomic.Int32)
tasks := slices.Clone(txTasks)
txTasks = txTasks[:0]
go q.run(tasks, done, interrupt)
}
case <-done:
done, interrupt = nil, nil
if len(txTasks) > 0 {
done, interrupt = make(chan struct{}), new(atomic.Int32)
tasks := slices.Clone(txTasks)
txTasks = txTasks[:0]
go q.run(tasks, done, interrupt)
}
case fn := <-q.startConversion:
q.queue = append(q.queue, fn)
q.runNextTask()
case <-q.taskDone:
q.runNextTask()
case <-q.quit:
if done != nil {
log.Debug("Waiting for blob proof conversion to exit")
interrupt.Store(1)
<-done
}
if q.taskDone != nil {
log.Debug("Waiting for blobpool billy conversion to exit")
<-q.taskDone
}
// Signal any tasks that were queued for the next batch but never started
// so callers blocked in convert() receive an error instead of hanging.
for _, t := range txTasks {
// Best-effort notify; t.done is a buffered channel of size 1
// created by convert(), and we send exactly once per task.
t.done <- convertResult{err: errors.New("conversion queue closed")}
}
// Drop references to allow GC of the backing array.
txTasks = txTasks[:0]
return
}
}
}
func (q *conversionQueue) runNextTask() {
if len(q.queue) == 0 {
q.taskDone = nil
return
}
fn := q.queue[0]
q.queue = append(q.queue[:0], q.queue[1:]...)
done := make(chan struct{})
go func() { defer close(done); fn() }()
q.taskDone = done
}

View file

@ -0,0 +1,114 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package blobpool
import (
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
)
func TestConversionQueueBasic(t *testing.T) {
queue := newConversionQueue()
defer queue.close()
key, _ := crypto.GenerateKey()
tx := makeTx(0, 1, 1, 1, key)
ptx, err := queue.convert(tx)
if err != nil {
t.Fatalf("Expected successful conversion, got error: %v", err)
}
if ptx == nil {
t.Fatal("Expected a converted transaction, got nil")
}
if ptx.Tx.Hash() != tx.Hash() {
t.Errorf("Converted tx hash mismatch: have %s, want %s", ptx.Tx.Hash(), tx.Hash())
}
if len(ptx.CellSidecar.Cells) == 0 {
t.Error("Expected cells to be computed during conversion")
}
}
func TestConversionQueueClosed(t *testing.T) {
queue := newConversionQueue()
queue.close()
key, _ := crypto.GenerateKey()
tx := makeTx(0, 1, 1, 1, key)
if _, err := queue.convert(tx); err == nil {
t.Fatal("Expected error when converting on closed queue, got nil")
}
}
func TestConversionQueueDoubleClose(t *testing.T) {
queue := newConversionQueue()
queue.close()
queue.close() // Should not panic
}
func TestConversionQueueAutoRestartBatch(t *testing.T) {
queue := newConversionQueue()
defer queue.close()
key, _ := crypto.GenerateKey()
// Create a heavy transaction to ensure the first batch runs long enough
// for subsequent tasks to be queued while it is active.
heavy := makeMultiBlobTx(0, 1, 1, 1, int(params.BlobTxMaxBlobs), 0, key)
var wg sync.WaitGroup
wg.Add(1)
heavyDone := make(chan error, 1)
go func() {
defer wg.Done()
_, err := queue.convert(heavy)
heavyDone <- err
}()
// Give the conversion worker a head start so that the following tasks are
// enqueued while the first batch is running.
time.Sleep(200 * time.Millisecond)
tx1 := makeTx(1, 1, 1, 1, key)
tx2 := makeTx(2, 1, 1, 1, key)
wg.Add(2)
done1 := make(chan error, 1)
done2 := make(chan error, 1)
go func() { defer wg.Done(); _, err := queue.convert(tx1); done1 <- err }()
go func() { defer wg.Done(); _, err := queue.convert(tx2); done2 <- err }()
for _, c := range []struct {
name string
done chan error
}{{"tx1", done1}, {"tx2", done2}, {"heavy", heavyDone}} {
select {
case err := <-c.done:
if err != nil {
t.Fatalf("%s conversion error: %v", c.name, err)
}
case <-time.After(30 * time.Second):
t.Fatalf("timeout waiting for %s conversion", c.name)
}
}
wg.Wait()
}

View file

@ -33,7 +33,7 @@ import (
type limboBlob struct { type limboBlob struct {
TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs
Block uint64 // Block in which the blob transaction was included Block uint64 // Block in which the blob transaction was included
Ptx *blobTxForPool Ptx *BlobTxForPool
} }
// limbo is a light, indexed database to temporarily store recently included // limbo is a light, indexed database to temporarily store recently included
@ -49,7 +49,7 @@ type limbo struct {
} }
// newLimbo opens and indexes a set of limboed blob transactions. // newLimbo opens and indexes a set of limboed blob transactions.
func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) { func newLimbo(config *params.ChainConfig, datadir string) (*limbo, []uint64, error) {
l := &limbo{ l := &limbo{
index: make(map[common.Hash]uint64), index: make(map[common.Hash]uint64),
groups: make(map[uint64]map[uint64]common.Hash), groups: make(map[uint64]map[uint64]common.Hash),
@ -61,19 +61,27 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) {
// See if we need to migrate the limbo after fusaka. // See if we need to migrate the limbo after fusaka.
slotter, err := tryMigrate(config, slotter, datadir) slotter, err := tryMigrate(config, slotter, datadir)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
// Index all limboed blobs on disk and delete anything unprocessable // Index all limboed blobs on disk and delete anything unprocessable
var fails []uint64 var (
fails []uint64
convert []uint64
)
index := func(id uint64, size uint32, data []byte) { index := func(id uint64, size uint32, data []byte) {
if l.parseBlob(id, data) != nil { err := l.parseBlob(id, data)
if errors.Is(err, errLegacyTx) {
convert = append(convert, id)
return
}
if err != nil {
fails = append(fails, id) fails = append(fails, id)
} }
} }
store, err := billy.Open(billy.Options{Path: datadir, Repair: true}, slotter, index) store, err := billy.Open(billy.Options{Path: datadir, Repair: true}, slotter, index)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
l.store = store l.store = store
@ -82,11 +90,11 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) {
for _, id := range fails { for _, id := range fails {
if err := l.store.Delete(id); err != nil { if err := l.store.Delete(id); err != nil {
l.Close() l.Close()
return nil, err return nil, nil, err
} }
} }
} }
return l, nil return l, convert, nil
} }
// Close closes down the underlying persistent store. // Close closes down the underlying persistent store.
@ -99,6 +107,9 @@ func (l *limbo) Close() error {
func (l *limbo) parseBlob(id uint64, data []byte) error { func (l *limbo) parseBlob(id uint64, data []byte) error {
item := new(limboBlob) item := new(limboBlob)
if err := rlp.DecodeBytes(data, item); err != nil { if err := rlp.DecodeBytes(data, item); err != nil {
if isLegacyLimboBlob(data) {
return errLegacyTx
}
// This path is impossible unless the disk data representation changes // This path is impossible unless the disk data representation changes
// across restarts. For that ever improbable case, recover gracefully // across restarts. For that ever improbable case, recover gracefully
// by ignoring this data entry. // by ignoring this data entry.
@ -122,6 +133,17 @@ func (l *limbo) parseBlob(id uint64, data []byte) error {
return nil return nil
} }
// isLegacyLimboBlob returns true if the data is encoded in legacy limboBlob type.
// It checks whether the first byte of third element is blobTxType.
func isLegacyLimboBlob(data []byte) bool {
elems, err := rlp.SplitListValues(data)
if err != nil || len(elems) < 3 {
return false
}
kind, content, _, err := rlp.Split(elems[2])
return err == nil && kind == rlp.String && len(content) > 1 && content[0] == types.BlobTxType
}
// finalize evicts all blobs belonging to a recently finalized block or older. // finalize evicts all blobs belonging to a recently finalized block or older.
func (l *limbo) finalize(final *types.Header) { func (l *limbo) finalize(final *types.Header) {
// Just in case there's no final block yet (network not yet merged, weird // Just in case there's no final block yet (network not yet merged, weird
@ -146,7 +168,9 @@ func (l *limbo) finalize(final *types.Header) {
// push stores a new blob transaction into the limbo, waiting until finality for // push stores a new blob transaction into the limbo, waiting until finality for
// it to be automatically evicted. // it to be automatically evicted.
func (l *limbo) push(ptx *blobTxForPool, block uint64) error { func (l *limbo) push(ptx *BlobTxForPool, block uint64) error {
// If the blobs are already tracked by the limbo, consider it a programming
// error. There's not much to do against it, but be loud.
hash := ptx.Tx.Hash() hash := ptx.Tx.Hash()
if _, ok := l.index[hash]; ok { if _, ok := l.index[hash]; ok {
log.Error("Limbo cannot push already tracked blobs", "tx", hash) log.Error("Limbo cannot push already tracked blobs", "tx", hash)
@ -162,7 +186,7 @@ func (l *limbo) push(ptx *blobTxForPool, block uint64) error {
// pull retrieves a previously pushed set of blobs back from the limbo, removing // 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 // it at the same time. This method should be used when a previously included blob
// transaction gets reorged out. // transaction gets reorged out.
func (l *limbo) pull(tx common.Hash) (*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 // 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 // can happen for example if a blob transaction is mined without pushing it
// into the network first. // into the network first.
@ -239,7 +263,7 @@ func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) {
// setAndIndex assembles a limbo blob database entry and stores it, also updating // setAndIndex assembles a limbo blob database entry and stores it, also updating
// the in-memory indices. // the in-memory indices.
func (l *limbo) setAndIndex(ptx *blobTxForPool, block uint64) error { func (l *limbo) setAndIndex(ptx *BlobTxForPool, block uint64) error {
txhash := ptx.Tx.Hash() txhash := ptx.Tx.Hash()
item := &limboBlob{ item := &limboBlob{
TxHash: txhash, TxHash: txhash,

View file

@ -18,11 +18,15 @@ package blobpool
import ( import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
) )
type txMetadata struct { type txMetadata struct {
id uint64 // the billy id of transaction id uint64 // the billy id of transaction
size uint64 // the RLP encoded size of transaction (blobs are included) size uint64 // the RLP encoded size of transaction (blobs are included)
sizeWithoutBlob uint64 // the RLP encoded size without blob data (for ETH/72 announcements)
custody types.CustodyBitmap
vhashes []common.Hash // blob versioned hashes for the transaction
} }
// lookup maps blob versioned hashes to transaction hashes that include them, // lookup maps blob versioned hashes to transaction hashes that include them,
@ -56,6 +60,15 @@ func (l *lookup) storeidOfTx(txhash common.Hash) (uint64, bool) {
return meta.id, true return meta.id, true
} }
// blobHashesOfTx returns the blob versioned hashes for a transaction.
func (l *lookup) blobHashesOfTx(txhash common.Hash) ([]common.Hash, bool) {
meta, ok := l.txIndex[txhash]
if !ok {
return nil, false
}
return meta.vhashes, true
}
// storeidOfBlob returns the datastore storage item id of a blob. // storeidOfBlob returns the datastore storage item id of a blob.
func (l *lookup) storeidOfBlob(vhash common.Hash) (uint64, bool) { func (l *lookup) storeidOfBlob(vhash common.Hash) (uint64, bool) {
// If the blob is unknown, return a miss // If the blob is unknown, return a miss
@ -91,8 +104,11 @@ func (l *lookup) track(tx *blobTxMeta) {
} }
// Map the transaction hash to the datastore id and RLP-encoded transaction size // Map the transaction hash to the datastore id and RLP-encoded transaction size
l.txIndex[tx.hash] = &txMetadata{ l.txIndex[tx.hash] = &txMetadata{
id: tx.id, id: tx.id,
size: tx.size, size: tx.size,
sizeWithoutBlob: tx.sizeWithoutBlob,
custody: *tx.custody,
vhashes: tx.vhashes,
} }
} }

View file

@ -74,4 +74,7 @@ var (
// ErrKZGVerificationError is returned when a KZG proof was not verified correctly. // ErrKZGVerificationError is returned when a KZG proof was not verified correctly.
ErrKZGVerificationError = errors.New("KZG verification error") ErrKZGVerificationError = errors.New("KZG verification error")
// ErrSidecarFormatError is returned when sidecar is malformed
ErrSidecarFormatError = errors.New("Wrong sidecar format")
) )

View file

@ -1010,7 +1010,7 @@ func (pool *LegacyPool) get(hash common.Hash) *types.Transaction {
} }
// GetRLP returns a RLP-encoded transaction if it is contained in the pool. // GetRLP returns a RLP-encoded transaction if it is contained in the pool.
func (pool *LegacyPool) GetRLP(hash common.Hash) []byte { func (pool *LegacyPool) GetRLP(hash common.Hash, _ uint) []byte {
tx := pool.all.Get(hash) tx := pool.all.Get(hash)
if tx == nil { if tx == nil {
return nil return nil

View file

@ -80,14 +80,16 @@ type PendingFilter struct {
// When BlobTxs true, return only blob transactions (block blob-space filling) // When BlobTxs true, return only blob transactions (block blob-space filling)
// when false, return only non-blob txs (peer-join announces, block space filling) // when false, return only non-blob txs (peer-join announces, block space filling)
BlobTxs bool BlobTxs bool
BlobVersion byte // Blob tx version to include. 0 means pre-Osaka, 1 means Osaka and later PartialCells bool
BlobVersion byte // Blob tx version to include. 0 means pre-Osaka, 1 means Osaka and later
} }
// TxMetadata denotes the metadata of a transaction. // TxMetadata denotes the metadata of a transaction.
type TxMetadata struct { type TxMetadata struct {
Type uint8 // The type of the transaction Type uint8 // The type of the transaction
Size uint64 // The length of the 'rlp encoding' of a transaction Size uint64 // The length of the 'rlp encoding' of a transaction (including blobs)
SizeWithoutBlob uint64 // The length without blob data (for ETH/72 announcements)
} }
// SubPool represents a specialized transaction pool that lives on its own (e.g. // SubPool represents a specialized transaction pool that lives on its own (e.g.
@ -132,7 +134,7 @@ type SubPool interface {
Get(hash common.Hash) *types.Transaction Get(hash common.Hash) *types.Transaction
// GetRLP returns a RLP-encoded transaction if it is contained in the pool. // GetRLP returns a RLP-encoded transaction if it is contained in the pool.
GetRLP(hash common.Hash) []byte GetRLP(hash common.Hash, version uint) []byte
// GetMetadata returns the transaction type and transaction size with the // GetMetadata returns the transaction type and transaction size with the
// given transaction hash. // given transaction hash.

View file

@ -288,9 +288,9 @@ func (p *TxPool) Get(hash common.Hash) *types.Transaction {
} }
// GetRLP returns a RLP-encoded transaction if it is contained in the pool. // GetRLP returns a RLP-encoded transaction if it is contained in the pool.
func (p *TxPool) GetRLP(hash common.Hash) []byte { func (p *TxPool) GetRLP(hash common.Hash, version uint) []byte {
for _, subpool := range p.subpools { for _, subpool := range p.subpools {
encoded := subpool.GetRLP(hash) encoded := subpool.GetRLP(hash, version)
if len(encoded) != 0 { if len(encoded) != 0 {
return encoded return encoded
} }

View file

@ -65,9 +65,6 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
if opts.Accept&(1<<tx.Type()) == 0 { if opts.Accept&(1<<tx.Type()) == 0 {
return fmt.Errorf("%w: tx type %v not supported by this pool", core.ErrTxTypeNotSupported, tx.Type()) return fmt.Errorf("%w: tx type %v not supported by this pool", core.ErrTxTypeNotSupported, tx.Type())
} }
if blobCount := len(tx.BlobHashes()); blobCount > opts.MaxBlobCount {
return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, blobCount, opts.MaxBlobCount)
}
// Before performing any expensive validations, sanity check that the tx is // Before performing any expensive validations, sanity check that the tx is
// smaller than the maximum limit the pool can meaningfully handle // smaller than the maximum limit the pool can meaningfully handle
if tx.Size() > opts.MaxSize { if tx.Size() > opts.MaxSize {
@ -161,7 +158,7 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip) return fmt.Errorf("%w: gas tip cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.GasTipCap(), opts.MinTip)
} }
if tx.Type() == types.BlobTxType { if tx.Type() == types.BlobTxType {
return validateBlobTx(tx) return validateBlobSidecar(tx, head, opts)
} }
if tx.Type() == types.SetCodeTxType { if tx.Type() == types.SetCodeTxType {
if len(tx.SetCodeAuthorizations()) == 0 { if len(tx.SetCodeAuthorizations()) == 0 {
@ -171,42 +168,64 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
return nil return nil
} }
// validateBlobTx implements the blob-transaction specific validations. // validateBlobSidecar implements the blob sidecar validation.
func validateBlobTx(tx *types.Transaction) error { // Note that this doesn't verify the consistency between blobs(cells) and
// proofs. For proof verification, use validateCells.
func validateBlobSidecar(tx *types.Transaction, head *types.Header, opts *ValidationOptions) error {
if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 {
return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice)
}
sidecar := tx.BlobTxSidecar() sidecar := tx.BlobTxSidecar()
if sidecar == nil { if sidecar == nil {
return errors.New("missing sidecar in blob transaction") return errors.New("missing sidecar in blob transaction")
} }
// Ensure the sidecar is constructed with the correct version
if sidecar.Version != types.BlobSidecarVersion1 {
return fmt.Errorf("unexpected sidecar version, want: %d, got: %d", types.BlobSidecarVersion1, sidecar.Version)
}
// Ensure the blob fee cap satisfies the minimum blob gas price
if tx.BlobGasFeeCapIntCmp(blobTxMinBlobGasPrice) < 0 {
return fmt.Errorf("%w: blob fee cap %v, minimum needed %v", ErrTxGasPriceTooLow, tx.BlobGasFeeCap(), blobTxMinBlobGasPrice)
}
// Ensure the number of items in the blob transaction and various side
// data match up before doing any expensive validations
hashes := tx.BlobHashes() hashes := tx.BlobHashes()
if len(hashes) == 0 {
return errors.New("blobless blob transaction")
}
if len(hashes) > params.BlobTxMaxBlobs {
return fmt.Errorf("too many blobs in transaction: have %d, permitted %d", len(hashes), params.BlobTxMaxBlobs)
}
if len(sidecar.Blobs) != len(hashes) {
return fmt.Errorf("invalid number of %d blobs compared to %d blob hashes", len(sidecar.Blobs), len(hashes))
}
if err := sidecar.ValidateBlobCommitmentHashes(hashes); err != nil { if err := sidecar.ValidateBlobCommitmentHashes(hashes); err != nil {
return err return err
} }
return validateBlobSidecarOsaka(sidecar, hashes) if len(hashes) > opts.MaxBlobCount {
} return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, len(hashes), opts.MaxBlobCount)
func validateBlobSidecarOsaka(sidecar *types.BlobTxSidecar, hashes []common.Hash) error {
if len(sidecar.Proofs) != len(hashes)*kzg4844.CellProofsPerBlob {
return fmt.Errorf("invalid number of %d blob proofs expected %d", len(sidecar.Proofs), len(hashes)*kzg4844.CellProofsPerBlob)
} }
if err := kzg4844.VerifyCellProofs(sidecar.Blobs, sidecar.Commitments, sidecar.Proofs); err != nil { if sidecar.Version != types.BlobSidecarVersion1 {
return fmt.Errorf("%w: unexpected sidecar version, want: %d, got: %d", ErrSidecarFormatError, types.BlobSidecarVersion1, sidecar.Version)
}
if len(sidecar.Proofs) != len(sidecar.Commitments)*kzg4844.CellProofsPerBlob {
return fmt.Errorf("%w: invalid number of %d blob proofs expected %d", ErrSidecarFormatError, len(sidecar.Proofs), len(sidecar.Commitments)*kzg4844.CellProofsPerBlob)
}
return nil
}
func ValidateCells(sidecar *types.BlobTxCellSidecar) error {
// Two checks here (custody count check and blobCount check) is duplicated in buffer.go
// However it is required to 1) serve eth71 peer and direct submission 2) catch any bug in
// merging cell delivery.
if sidecar.Custody.OneCount() == 0 {
return errors.New("blobless blob transaction")
}
// Verify whether the blob count is consistent with other parts of the sidecar and the transaction
blobCount := len(sidecar.Cells) / sidecar.Custody.OneCount()
if blobCount == 0 {
return errors.New("blobless blob transaction")
}
if blobCount != len(sidecar.Commitments) {
return fmt.Errorf("invalid number of %d blobs compared to %d commitments", blobCount, len(sidecar.Commitments))
}
if sidecar.Version != types.BlobSidecarVersion1 {
return fmt.Errorf("unexpected sidecar version, want: %d, got: %d", types.BlobSidecarVersion1, sidecar.Version)
}
return validateCellsOsaka(sidecar)
}
func validateCellsOsaka(sidecar *types.BlobTxCellSidecar) error {
indices := sidecar.Custody.Indices()
cellProofs := make([]kzg4844.Proof, 0)
for blobIdx := range len(sidecar.Commitments) {
for _, proofIdx := range indices {
idx := blobIdx*kzg4844.CellProofsPerBlob + int(proofIdx)
cellProofs = append(cellProofs, sidecar.Proofs[idx])
}
}
if err := kzg4844.VerifyCells(sidecar.Cells, sidecar.Commitments, cellProofs, sidecar.Custody.Indices()); err != nil {
return fmt.Errorf("%w: %v", ErrKZGVerificationError, err) return fmt.Errorf("%w: %v", ErrKZGVerificationError, err)
} }
return nil return nil

View file

@ -0,0 +1,152 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package types
import (
"fmt"
"math/bits"
"math/rand"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
)
// CustodyBitmap is a bitmap to represent which custody index to store (little endian).
type CustodyBitmap [16]byte
// MarshalText implements encoding.TextMarshaler.
func (b CustodyBitmap) MarshalText() ([]byte, error) {
return []byte(hexutil.Encode(b[:])), nil
}
// UnmarshalText implements encoding.TextUnmarshaler.
func (b *CustodyBitmap) UnmarshalText(input []byte) error {
decoded, err := hexutil.Decode(string(input))
if err != nil {
return fmt.Errorf("custody bitmap: %v", err)
}
if len(decoded) != len(b) {
return fmt.Errorf("custody bitmap: invalid length %d, want %d", len(decoded), len(b))
}
copy(b[:], decoded)
return nil
}
var (
CustodyBitmapAll = func() CustodyBitmap {
var result CustodyBitmap
for i := range result {
result[i] = 0xFF
}
return result
}()
CustodyBitmapData = func() CustodyBitmap {
var result CustodyBitmap
for i := 0; i < kzg4844.DataPerBlob/8; i++ {
result[i] = 0xFF
}
return result
}()
)
func NewCustodyBitmap(indices []uint64) CustodyBitmap {
var result CustodyBitmap
for _, i := range indices {
if i >= uint64(kzg4844.CellsPerBlob) {
panic("CustodyBitmap: bit index out of range")
}
result[i/8] |= 1 << (i % 8)
}
return result
}
// NewRandomCustodyBitmap creates a CustodyBitmap with n randomly selected indices.
// This should be used only for tests.
func NewRandomCustodyBitmap(n int) CustodyBitmap {
if n <= 0 || n > kzg4844.CellsPerBlob {
panic("CustodyBitmap: invalid number of indices")
}
indices := make([]uint64, 0, n)
used := make(map[uint64]bool)
for len(indices) < n {
idx := uint64(rand.Intn(kzg4844.CellsPerBlob))
if !used[idx] {
used[idx] = true
indices = append(indices, idx)
}
}
return NewCustodyBitmap(indices)
}
// IsSet returns whether bit i is set.
func (b CustodyBitmap) IsSet(i uint64) bool {
if i >= uint64(kzg4844.CellsPerBlob) {
return false
}
return (b[i/8]>>(i%8))&1 == 1
}
// OneCount returns the number of bits set to 1.
func (b CustodyBitmap) OneCount() int {
total := 0
for _, v := range b {
total += bits.OnesCount8(v)
}
return total
}
// Indices returns the bit positions set to 1, in ascending order.
func (b CustodyBitmap) Indices() []uint64 {
out := make([]uint64, 0, b.OneCount())
for byteIdx, val := range b {
v := val
for v != 0 {
tz := bits.TrailingZeros8(v)
out = append(out, uint64(byteIdx*8+tz))
v &^= 1 << tz
}
}
return out
}
// Difference returns b AND NOT set (bits in b but not in set).
func (b CustodyBitmap) Difference(set CustodyBitmap) CustodyBitmap {
var out CustodyBitmap
for i := range b {
out[i] = b[i] &^ set[i]
}
return out
}
// Intersection returns b AND set.
func (b CustodyBitmap) Intersection(set CustodyBitmap) CustodyBitmap {
var out CustodyBitmap
for i := range b {
out[i] = b[i] & set[i]
}
return out
}
// Union returns b OR set.
func (b CustodyBitmap) Union(set CustodyBitmap) CustodyBitmap {
var out CustodyBitmap
for i := range b {
out[i] = b[i] | set[i]
}
return out
}

View file

@ -176,6 +176,16 @@ func (sc *BlobTxSidecar) Copy() *BlobTxSidecar {
} }
} }
// BlobTxCellSidecar is a sidecar that carries cells instead of blobs.
// The Custody field represents which cells of each blob this sidecar contains.
type BlobTxCellSidecar struct {
Version byte
Cells []kzg4844.Cell
Commitments []kzg4844.Commitment
Proofs []kzg4844.Proof
Custody CustodyBitmap
}
// blobTxWithBlobs represents blob tx with its corresponding sidecar. // blobTxWithBlobs represents blob tx with its corresponding sidecar.
// This is an interface because sidecars are versioned. // This is an interface because sidecars are versioned.
type blobTxWithBlobs interface { type blobTxWithBlobs interface {

View file

@ -44,6 +44,7 @@ import (
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/fetcher"
"github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/eth/protocols/snap"
@ -350,15 +351,17 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
// Permit the downloader to use the trie cache allowance during fast sync // Permit the downloader to use the trie cache allowance during fast sync
cacheLimit := options.TrieCleanLimit + options.TrieDirtyLimit + options.SnapshotLimit cacheLimit := options.TrieCleanLimit + options.TrieDirtyLimit + options.SnapshotLimit
if eth.handler, err = newHandler(&handlerConfig{ if eth.handler, err = newHandler(&handlerConfig{
NodeID: eth.p2pServer.Self().ID(), NodeID: eth.p2pServer.Self().ID(),
Database: chainDb, Database: chainDb,
Chain: eth.blockchain, Chain: eth.blockchain,
TxPool: eth.txPool, TxPool: eth.txPool,
Network: networkID, BlobPool: eth.blobTxPool,
Sync: config.SyncMode, Network: networkID,
BloomCache: uint64(cacheLimit), Sync: config.SyncMode,
RequiredBlocks: config.RequiredBlocks, BloomCache: uint64(cacheLimit),
SnapV2: config.SnapV2, RequiredBlocks: config.RequiredBlocks,
SnapV2: config.SnapV2,
FetchProbability: config.BlobPool.FetchProbability,
}); err != nil { }); err != nil {
return nil, err return nil, err
} }
@ -442,6 +445,7 @@ func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager
func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain } func (s *Ethereum) BlockChain() *core.BlockChain { return s.blockchain }
func (s *Ethereum) TxPool() *txpool.TxPool { return s.txPool } func (s *Ethereum) TxPool() *txpool.TxPool { return s.txPool }
func (s *Ethereum) BlobTxPool() *blobpool.BlobPool { return s.blobTxPool } func (s *Ethereum) BlobTxPool() *blobpool.BlobPool { return s.blobTxPool }
func (s *Ethereum) BlobFetcher() *fetcher.BlobFetcher { return s.handler.blobFetcher }
func (s *Ethereum) BlobCache() *blobpool.Cache { return s.blobCache } func (s *Ethereum) BlobCache() *blobpool.Cache { return s.blobCache }
func (s *Ethereum) Engine() consensus.Engine { return s.engine } func (s *Ethereum) Engine() consensus.Engine { return s.engine }
func (s *Ethereum) ChainDb() ethdb.Database { return s.chainDb } func (s *Ethereum) ChainDb() ethdb.Database { return s.chainDb }

View file

@ -22,6 +22,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"reflect" "reflect"
"slices"
"strconv" "strconv"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -217,7 +218,7 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV3(ctx context.Context, update engine.
// ForkchoiceUpdatedV4 is equivalent to V3 with the addition of slot number // ForkchoiceUpdatedV4 is equivalent to V3 with the addition of slot number
// in the payload attributes. It supports only PayloadAttributesV4. // in the payload attributes. It supports only PayloadAttributesV4.
func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine.ForkchoiceStateV1, params *engine.PayloadAttributes) (engine.ForkChoiceResponse, error) { func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine.ForkchoiceStateV1, params *engine.PayloadAttributes, custodyColumns *types.CustodyBitmap) (engine.ForkChoiceResponse, error) {
if params != nil { if params != nil {
switch { switch {
case params.Withdrawals == nil: case params.Withdrawals == nil:
@ -232,6 +233,9 @@ func (api *ConsensusAPI) ForkchoiceUpdatedV4(ctx context.Context, update engine.
return engine.STATUS_INVALID, unsupportedForkErr("fcuV4 must only be called for amsterdam payloads") return engine.STATUS_INVALID, unsupportedForkErr("fcuV4 must only be called for amsterdam payloads")
} }
} }
if custodyColumns != nil {
api.eth.BlobFetcher().UpdateCustody(*custodyColumns)
}
// TODO(matt): the spec requires that fcu is applied when called on a valid // TODO(matt): the spec requires that fcu is applied when called on a valid
// hash, even if params are wrong. To do this we need to split up // hash, even if params are wrong. To do this we need to split up
// forkchoiceUpdate into a function that only updates the head and then a // forkchoiceUpdate into a function that only updates the head and then a
@ -711,6 +715,63 @@ func (api *ConsensusAPI) getBlobs(ctx context.Context, hashes []common.Hash, v2
return res, nil return res, nil
} }
// GetBlobsV4 returns cell-level blob data from the transaction pool.
// V4 returns only the requested cells as specified by the indices_bitarray.
func (api *ConsensusAPI) GetBlobsV4(hashes []common.Hash, indicesBitarray types.CustodyBitmap) ([]*engine.BlobCellsAndProofsV1, error) {
head := api.eth.BlockChain().CurrentHeader()
// Sparse blobpool is not necessarily coupled with the Amsterdam fork and
// can technically be supported after the Osaka fork
// (where cell proofs are introduced).
if api.config().LatestFork(head.Time) < forks.Osaka {
return nil, nil
}
if len(hashes) > 128 {
return nil, engine.TooLargeRequest.With(fmt.Errorf("requested blob count too large: %v", len(hashes)))
}
cells, proofs, err := api.eth.BlobCache().GetCells(hashes, indicesBitarray)
if err != nil {
return nil, engine.InvalidParams.With(err)
}
var (
res = make([]*engine.BlobCellsAndProofsV1, len(hashes))
hitCount int
)
getBlobsRequestedCounter.Inc(int64(len(hashes)))
for i := range hashes {
if cells[i] == nil || proofs[i] == nil {
continue
}
hitCount++
blobCells := make([]*hexutil.Bytes, len(cells[i]))
for j, cell := range cells[i] {
if cell != nil {
b := hexutil.Bytes(cell[:])
blobCells[j] = &b
}
}
blobProofs := make([]*hexutil.Bytes, len(proofs[i]))
for j, proof := range proofs[i] {
if proof != nil {
b := hexutil.Bytes(proof[:])
blobProofs[j] = &b
}
}
res[i] = &engine.BlobCellsAndProofsV1{
BlobCells: blobCells,
Proofs: blobProofs,
}
}
getBlobsAvailableCounter.Inc(int64(hitCount))
if hitCount == len(hashes) {
getBlobsRequestCompleteHit.Inc(1)
} else if hitCount > 0 {
getBlobsRequestPartialHit.Inc(1)
} else {
getBlobsRequestMiss.Inc(1)
}
return res, nil
}
// HasBlobs reports availability for the requested blob-versioned-hashes. // HasBlobs reports availability for the requested blob-versioned-hashes.
func (api *ConsensusAPI) HasBlobs(hashes []common.Hash) []bool { func (api *ConsensusAPI) HasBlobs(hashes []common.Hash) []bool {
return api.eth.BlobCache().HasBlobs(context.Background(), hashes) return api.eth.BlobCache().HasBlobs(context.Background(), hashes)
@ -1140,17 +1201,26 @@ func (api *ConsensusAPI) checkFork(timestamp uint64, forks ...forks.Fork) bool {
} }
// ExchangeCapabilities returns the current methods provided by this node. // ExchangeCapabilities returns the current methods provided by this node.
func (api *ConsensusAPI) ExchangeCapabilities([]string) []string { func (api *ConsensusAPI) ExchangeCapabilities(caps []string) []string {
valueT := reflect.TypeOf(api) valueT := reflect.TypeOf(api)
caps := make([]string, 0, valueT.NumMethod())
// If the CL supports getBlobsV4, we call EnableCell() on the
// blob cache to skip the blob recovery process. This is a
// one-directional toggle, which assumes that once the CL
// supports getBlobsV4, it will not fall back to getBlobsV3
// again.
cellmode := slices.Contains(caps, "engine_getBlobsV4")
api.eth.BlobCache().SetCellMode(cellmode)
ourCaps := make([]string, 0, valueT.NumMethod())
for i := 0; i < valueT.NumMethod(); i++ { for i := 0; i < valueT.NumMethod(); i++ {
name := []rune(valueT.Method(i).Name) name := []rune(valueT.Method(i).Name)
if string(name) == "ExchangeCapabilities" { if string(name) == "ExchangeCapabilities" {
continue continue
} }
caps = append(caps, "engine_"+string(unicode.ToLower(name[0]))+string(name[1:])) ourCaps = append(ourCaps, "engine_"+string(unicode.ToLower(name[0]))+string(name[1:]))
} }
return caps return ourCaps
} }
// GetClientVersionV1 exchanges client version data of this node. // GetClientVersionV1 exchanges client version data of this node.

View file

@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/consensus/beacon"
"github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/crypto/kzg4844"
@ -1847,23 +1848,27 @@ func init() {
// makeMultiBlobTx is a utility method to construct a random blob tx with // makeMultiBlobTx is a utility method to construct a random blob tx with
// certain number of blobs in its sidecar. // certain number of blobs in its sidecar.
func makeMultiBlobTx(chainConfig *params.ChainConfig, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte) *types.Transaction { func makeMultiBlobTx(chainConfig *params.ChainConfig, nonce uint64, blobCount int, blobOffset int, key *ecdsa.PrivateKey, version byte, custody types.CustodyBitmap) *blobpool.BlobTxForPool {
indices := custody.Indices()
var ( var (
blobs []kzg4844.Blob
blobHashes []common.Hash blobHashes []common.Hash
commitments []kzg4844.Commitment commitments []kzg4844.Commitment
proofs []kzg4844.Proof proofs []kzg4844.Proof
cells []kzg4844.Cell
) )
for i := 0; i < blobCount; i++ { for i := 0; i < blobCount; i++ {
blobs = append(blobs, *testBlobs[blobOffset+i]) j := blobOffset + i
commitments = append(commitments, testBlobCommits[blobOffset+i]) blobHashes = append(blobHashes, testBlobVHashes[j])
commitments = append(commitments, testBlobCommits[j])
if version == types.BlobSidecarVersion0 { if version == types.BlobSidecarVersion0 {
proofs = append(proofs, testBlobProofs[blobOffset+i]) proofs = append(proofs, testBlobProofs[j])
} else { } else {
cellProofs, _ := kzg4844.ComputeCellProofs(testBlobs[blobOffset+i]) proofs = append(proofs, testBlobCellProofs[j]...)
proofs = append(proofs, cellProofs...) }
full, _ := kzg4844.ComputeCells([]kzg4844.Blob{*testBlobs[j]})
for _, idx := range indices {
cells = append(cells, full[idx])
} }
blobHashes = append(blobHashes, testBlobVHashes[blobOffset+i])
} }
blobtx := &types.BlobTx{ blobtx := &types.BlobTx{
ChainID: uint256.MustFromBig(chainConfig.ChainID), ChainID: uint256.MustFromBig(chainConfig.ChainID),
@ -1874,12 +1879,20 @@ func makeMultiBlobTx(chainConfig *params.ChainConfig, nonce uint64, blobCount in
BlobFeeCap: uint256.NewInt(1000), BlobFeeCap: uint256.NewInt(1000),
BlobHashes: blobHashes, BlobHashes: blobHashes,
Value: uint256.NewInt(100), Value: uint256.NewInt(100),
Sidecar: types.NewBlobTxSidecar(version, blobs, commitments, proofs),
} }
return types.MustSignNewTx(key, types.LatestSigner(chainConfig), blobtx) return &blobpool.BlobTxForPool{
Tx: types.MustSignNewTx(key, types.LatestSigner(chainConfig), blobtx),
CellSidecar: &types.BlobTxCellSidecar{
Version: version,
Cells: cells,
Commitments: commitments,
Proofs: proofs,
Custody: custody,
},
}
} }
func newGetBlobEnv(t testing.TB, version byte) (*node.Node, *ConsensusAPI) { func newGetBlobEnv(t testing.TB, version byte, custody types.CustodyBitmap) (*node.Node, *ConsensusAPI) {
var ( var (
// Create a database pre-initialize with a genesis block // Create a database pre-initialize with a genesis block
config = *params.MergedTestChainConfig config = *params.MergedTestChainConfig
@ -1908,17 +1921,23 @@ func newGetBlobEnv(t testing.TB, version byte) (*node.Node, *ConsensusAPI) {
} }
n, ethServ := startEthService(t, gspec, nil) n, ethServ := startEthService(t, gspec, nil)
// fill blob txs into the pool // fill blob txs into the pool, each holding only the given custody cells
tx1 := makeMultiBlobTx(&config, 0, 2, 0, key1, version) // blob[0, 2) txs := []*blobpool.BlobTxForPool{
tx2 := makeMultiBlobTx(&config, 0, 2, 2, key2, version) // blob[2, 4) makeMultiBlobTx(&config, 0, 2, 0, key1, version, custody), // blob[0, 2)
tx3 := makeMultiBlobTx(&config, 0, 2, 4, key3, version) // blob[4, 6) makeMultiBlobTx(&config, 0, 2, 2, key2, version, custody), // blob[2, 4)
ethServ.TxPool().Add([]*types.Transaction{tx1, tx2, tx3}, true) makeMultiBlobTx(&config, 0, 2, 4, key3, version, custody), // blob[4, 6)
}
for _, ptx := range txs {
if err := ethServ.BlobTxPool().AddPooledTx(ptx); err != nil {
t.Fatalf("failed to add blob tx: %v", err)
}
}
api := newConsensusAPIWithoutHeartbeat(ethServ) api := newConsensusAPIWithoutHeartbeat(ethServ)
return n, api return n, api
} }
func TestGetBlobsV2And3(t *testing.T) { func TestGetBlobsV2And3(t *testing.T) {
n, api := newGetBlobEnv(t, 1) n, api := newGetBlobEnv(t, 1, types.CustodyBitmapAll)
defer n.Close() defer n.Close()
suites := []struct { suites := []struct {
@ -1954,7 +1973,7 @@ func TestGetBlobsV2And3(t *testing.T) {
// Benchmark GetBlobsV2 internals // Benchmark GetBlobsV2 internals
// Note that this is not an RPC-level benchmark, so JSON-RPC overhead is not included. // Note that this is not an RPC-level benchmark, so JSON-RPC overhead is not included.
func BenchmarkGetBlobsV2(b *testing.B) { func BenchmarkGetBlobsV2(b *testing.B) {
n, api := newGetBlobEnv(b, 1) n, api := newGetBlobEnv(b, 1, types.CustodyBitmapAll)
defer n.Close() defer n.Close()
// for blobs in [1, 2, 4, 6], print string and run benchmark // for blobs in [1, 2, 4, 6], print string and run benchmark
@ -2014,3 +2033,112 @@ func runGetBlobs(t testing.TB, getBlobs getBlobsFn, start, limit int, fillRandom
t.Fatalf("Unexpected result for case %s", name) t.Fatalf("Unexpected result for case %s", name)
} }
} }
func TestGetBlobsV4(t *testing.T) {
// The pool holds only this set of custody cells.
custody := types.NewCustodyBitmap([]uint64{0, 1, 2, 3, 4})
n, api := newGetBlobEnv(t, 1, custody)
defer n.Close()
masks := []struct {
name string
mask types.CustodyBitmap
}{
{"missing", types.NewCustodyBitmap([]uint64{5})},
{"overlap", types.NewCustodyBitmap([]uint64{0, 2, 5, 127})},
{"aligned", custody},
}
suites := []struct {
start int
limit int
missingBlob bool
}{
{start: 0, limit: 1},
{start: 0, limit: 2},
{start: 1, limit: 3},
{start: 0, limit: 6},
{start: 1, limit: 5},
{start: 0, limit: 6, missingBlob: true},
}
for _, m := range masks {
for i, suite := range suites {
runGetBlobsV4(t, api, custody, m.mask, suite.start, suite.limit, suite.missingBlob, fmt.Sprintf("GetBlobsV4 mask=%s suite=%d", m.name, i))
}
}
}
func runGetBlobsV4(t testing.TB, api *ConsensusAPI, custody, mask types.CustodyBitmap, start, limit int, missingBlob bool, name string) {
// Fill the request for retrieving cells and build the expected response.
var (
vhashes []common.Hash
expect []*engine.BlobCellsAndProofsV1
indices = mask.Indices()
)
for j := start; j < limit; j++ {
vhashes = append(vhashes, testBlobVHashes[j])
cells, err := kzg4844.ComputeCells([]kzg4844.Blob{*testBlobs[j]})
if err != nil {
t.Fatalf("Failed to compute cells for case %s: %v", name, err)
}
blobCells := make([]*hexutil.Bytes, len(indices))
blobProofs := make([]*hexutil.Bytes, len(indices))
for i, idx := range indices {
if !custody.IsSet(idx) {
continue
}
cell := hexutil.Bytes(cells[idx][:])
blobCells[i] = &cell
proof := hexutil.Bytes(testBlobCellProofs[j][idx][:])
blobProofs[i] = &proof
}
expect = append(expect, &engine.BlobCellsAndProofsV1{
BlobCells: blobCells,
Proofs: blobProofs,
})
}
// put random missing blob
if missingBlob {
vhashes = append(vhashes, testrand.Hash())
expect = append(expect, nil)
}
result, err := api.GetBlobsV4(vhashes, mask)
if err != nil {
t.Errorf("Unexpected error for case %s, %v", name, err)
}
if !reflect.DeepEqual(result, expect) {
t.Fatalf("Unexpected result for case %s", name)
}
}
// TestForkchoiceUpdatedV4 tests the custody bitmap argument added in V4.
func TestForkchoiceUpdatedV4(t *testing.T) {
n, api := newGetBlobEnv(t, 1, types.CustodyBitmapAll)
defer n.Close()
head := api.eth.BlockChain().CurrentHeader().Hash()
fcState := engine.ForkchoiceStateV1{
HeadBlockHash: head,
SafeBlockHash: head,
FinalizedBlockHash: head,
}
// Non nil custody bitmap case.
custody := types.NewCustodyBitmap([]uint64{0, 1, 2, 127})
resp, err := api.ForkchoiceUpdatedV4(context.Background(), fcState, nil, &custody)
if err != nil {
t.Fatalf("Unexpected error with custody bitmap: %v", err)
}
if resp.PayloadStatus.Status != engine.VALID {
t.Fatalf("Unexpected status with custody bitmap: got %s, want %s", resp.PayloadStatus.Status, engine.VALID)
}
// Nil custody bitmap case.
resp, err = api.ForkchoiceUpdatedV4(context.Background(), fcState, nil, nil)
if err != nil {
t.Fatalf("Unexpected error with nil custody bitmap: %v", err)
}
if resp.PayloadStatus.Status != engine.VALID {
t.Fatalf("Unexpected status with nil custody bitmap: got %s, want %s", resp.PayloadStatus.Status, engine.VALID)
}
}

904
eth/fetcher/blob_fetcher.go Normal file
View file

@ -0,0 +1,904 @@
// Copyright 2026 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package fetcher
import (
"iter"
"math/rand"
"slices"
"sort"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
)
type random interface {
Intn(n int) int
}
// BlobFetcher fetches blobs of new type-3 transactions with probability p,
// and for the remaining (1-p) transactions, it performs availability checks.
// For availability checks, it fetches cells from each blob in the transaction
// according to the custody cell indices provided by the consensus client
// connected to this execution client.
var blobFetchTimeout = 5 * time.Second
var blobAvailabilityTimeout = 2 * time.Second
// DefaultFetchProbability is the default probability of fetching the full blob
// payload for the sparse blobpool.
const DefaultFetchProbability = 15
const (
availabilityThreshold = 2
maxPayloadRetrievals = 128
maxPayloadAnnounces = 4096
// maxCellRequests caps the burst of cell requests we can issue at once
// to a single peer. Worst case 256 * 6 = 1536 cells (~3 MB)
maxCellRequests = 256
// refillInterval is the gap between token refill. Combined with
// maxCellRequests and 2-minute buffer timeout, a peer's worst case
// buffered cells is about 16 MB
refillInterval = time.Second / 9
)
type blobTxAnnounce struct {
origin string // Identifier of the peer that sent the announcement
txs []common.Hash // Hashes of transactions announced
cells types.CustodyBitmap // Custody information of transactions being announced
}
type cellRequest struct {
txs []common.Hash // Transactions that have been requested for their cells
cells types.CustodyBitmap // Requested cell indices
time mclock.AbsTime // Timestamp when the request was made
}
type payloadDelivery struct {
origin string // Peer from which the payloads were delivered
txs []common.Hash // Hashes of transactions that were delivered
cells [][]kzg4844.Cell
cellBitmap types.CustodyBitmap
}
type cellWithSeq struct {
seq uint64
cells types.CustodyBitmap
}
// PeerCellDelivery holds cells delivered by a single peer.
type PeerCellDelivery struct {
Cells []kzg4844.Cell // blob-major order as received
Indices []uint64 // custody indices provided by this peer
}
type fetchStatus struct {
fetching types.CustodyBitmap // To avoid fetching cells which had already been fetched / currently being fetched
fetched []uint64 // Custody indices that have been fetched (per-blob, same for all blobs)
deliveries map[string]*PeerCellDelivery // Per-peer cell deliveries
blobCount int // Number of blobs in this tx (set on first delivery)
}
type BlobFetcherFunctions struct {
HasPayload func(common.Hash) bool
AddCells func(common.Hash, map[string]*PeerCellDelivery, types.CustodyBitmap)
FetchPayloads func(string, []common.Hash, types.CustodyBitmap) error
DropPeer func(string)
}
// BlobFetcher is responsible for managing type 3 transactions based on peer announcements.
//
// BlobFetcher manages three buffers:
// - Transactions not to be fetched are moved to "waitlist"
// if a payload(blob) seems to be possessed by D(threshold) other peers, request custody cells for that.
// Accept it when the cells are received. Otherwise, it is dropped.
// - Transactions queued to be fetched are moved to "announces"
// if a payload is received, it is added to the blob pool. Otherwise, the transaction is dropped.
// - Transactions to be fetched are moved to "fetching"
// if a payload/cell announcement is received during fetch, the peer is recorded as an alternate source.
type BlobFetcher struct {
notify chan *blobTxAnnounce
cleanup chan *payloadDelivery
drop chan *txDrop
custodyCh chan types.CustodyBitmap
quit chan struct{}
custody types.CustodyBitmap
txSeq uint64 // To make transactions fetched in arrival order
full map[common.Hash]struct{}
partial map[common.Hash]struct{}
// Buffer 1: Set of blob txs whose blob data is waiting for availability confirmation (partial fetch)
waitlist map[common.Hash]map[string]struct{} // Peer set that announced blob availability
waittime map[common.Hash]mclock.AbsTime // Timestamp when added to waitlist
waitslots map[string]map[common.Hash]struct{} // Waiting announcements grouped by peer (DoS protection)
// waitSlots should also include announcements with partial cells
// Buffer 2: Transactions queued for fetching (full fetch + partial fetch)
// "announces" is shared with stage 3, for DoS protection
announces map[string]map[common.Hash]*cellWithSeq // Set of announced transactions, grouped by origin peer
// Buffer 2
// Stage 3: Transactions whose payloads/cells are currently being fetched (full fetch + partial fetch)
fetches map[common.Hash]*fetchStatus // Hash -> Bitmap, in-flight transaction cells
requests map[string][]*cellRequest // In-flight transaction retrievals
alternates map[common.Hash]map[string]types.CustodyBitmap // In-flight transaction alternate origins (in case the peer is dropped)
fn BlobFetcherFunctions // callbacks
fetchProbability uint64
// peerTokens tracks each peer's remaining cell request token.
peerTokens map[string]*token
step chan struct{} // Notification channel when the fetcher loop iterates
clock mclock.Clock // Monotonic clock or simulated clock for tests
realTime func() time.Time // Real system time or simulated time for tests
rand random // Randomizer
}
// token is a per peer token bucket for outgoing cell requests.
type token struct {
amount int64
last mclock.AbsTime
}
func NewBlobFetcher(fn BlobFetcherFunctions, custody types.CustodyBitmap, rand random, fetchProbability uint64) *BlobFetcher {
if fetchProbability < DefaultFetchProbability {
fetchProbability = DefaultFetchProbability
}
return &BlobFetcher{
notify: make(chan *blobTxAnnounce),
cleanup: make(chan *payloadDelivery),
drop: make(chan *txDrop),
custodyCh: make(chan types.CustodyBitmap),
quit: make(chan struct{}),
full: make(map[common.Hash]struct{}),
partial: make(map[common.Hash]struct{}),
waitlist: make(map[common.Hash]map[string]struct{}),
waittime: make(map[common.Hash]mclock.AbsTime),
waitslots: make(map[string]map[common.Hash]struct{}),
announces: make(map[string]map[common.Hash]*cellWithSeq),
fetches: make(map[common.Hash]*fetchStatus),
requests: make(map[string][]*cellRequest),
alternates: make(map[common.Hash]map[string]types.CustodyBitmap),
peerTokens: make(map[string]*token),
fn: fn,
fetchProbability: fetchProbability,
custody: custody,
clock: mclock.System{},
realTime: time.Now,
rand: rand,
}
}
// Notify is called when a Type 3 transaction is observed on the network. (TransactionPacket / NewPooledTransactionHashesPacket)
func (f *BlobFetcher) Notify(peer string, txs []common.Hash, cells types.CustodyBitmap) error {
blobAnnounceInMeter.Mark(int64(len(txs)))
anns := make([]common.Hash, 0)
for _, tx := range txs {
if f.fn.HasPayload(tx) {
continue
}
anns = append(anns, tx)
}
blobAnnounce := &blobTxAnnounce{origin: peer, txs: anns, cells: cells}
select {
case f.notify <- blobAnnounce:
return nil
case <-f.quit:
return errTerminated
}
}
// Enqueue inserts a batch of received blob payloads into the blob pool.
// This is triggered by ethHandler upon receiving direct request responses.
func (f *BlobFetcher) Enqueue(peer string, hashes []common.Hash, cells [][]kzg4844.Cell, cellBitmap types.CustodyBitmap) error {
blobReplyInMeter.Mark(int64(len(hashes)))
select {
case f.cleanup <- &payloadDelivery{origin: peer, txs: hashes, cells: cells, cellBitmap: cellBitmap}:
case <-f.quit:
return errTerminated
}
return nil
}
func (f *BlobFetcher) Drop(peer string) error {
select {
case f.drop <- &txDrop{peer: peer}:
return nil
case <-f.quit:
return errTerminated
}
}
// UpdateCustody hands a new custody bitmap to the fetcher loop. The actual
// swap happens inside the loop so f.custody is never read and written
// concurrently.
func (f *BlobFetcher) UpdateCustody(cells types.CustodyBitmap) {
select {
case f.custodyCh <- cells:
case <-f.quit:
}
}
func (f *BlobFetcher) Start() {
go f.loop()
}
func (f *BlobFetcher) Stop() {
close(f.quit)
}
func (f *BlobFetcher) loop() {
var (
waitTimer = new(mclock.Timer) // Timer for waitlist (availability)
waitTrigger = make(chan struct{}, 1)
timeoutTimer = new(mclock.Timer) // Timer for payload fetch request
timeoutTrigger = make(chan struct{}, 1)
)
for {
select {
case ann := <-f.notify:
// Drop part of the announcements if too many have accumulated from that peer
// This prevents a peer from dominating the queue with txs without responding to the request
used := len(f.waitslots[ann.origin]) + len(f.announces[ann.origin])
if used >= maxPayloadAnnounces {
blobAnnounceDOSMeter.Mark(int64(len(ann.txs)))
break
}
want := used + len(ann.txs)
if want >= maxPayloadAnnounces {
blobAnnounceDOSMeter.Mark(int64(want - maxPayloadAnnounces))
ann.txs = ann.txs[:maxPayloadAnnounces-used]
}
var (
idleWait = len(f.waittime) == 0
_, oldPeer = f.announces[ann.origin]
nextSeq = func() uint64 {
seq := f.txSeq
f.txSeq++
return seq
}
reschedule = make(map[string]struct{})
)
for _, hash := range ann.txs {
if oldPeer && f.announces[ann.origin][hash] != nil {
// Ignore already announced information
// We also have to prevent reannouncement by changing cells field.
// Considering cell custody transition is notified in advance of its finalization by consensus client,
// there is no reason to reannounce cells, and it has to be prevented.
continue
}
// Decide full or partial request
if _, ok := f.full[hash]; !ok {
if _, ok := f.partial[hash]; !ok {
// Not decided yet
var randomValue int
if f.rand == nil {
randomValue = rand.Intn(100)
} else {
randomValue = f.rand.Intn(100)
}
// For eager mode, always fetch immediately
if uint64(randomValue) < f.fetchProbability || f.custody.OneCount() >= kzg4844.DataPerBlob {
f.full[hash] = struct{}{}
} else {
f.partial[hash] = struct{}{}
// Register for availability check
f.waitlist[hash] = make(map[string]struct{})
f.waittime[hash] = f.clock.Now()
}
}
}
if _, ok := f.full[hash]; ok {
// 1) Decided to send full request of the tx
if ann.cells != types.CustodyBitmapAll {
continue
}
if f.announces[ann.origin] == nil {
f.announces[ann.origin] = make(map[common.Hash]*cellWithSeq)
}
f.announces[ann.origin][hash] = &cellWithSeq{
cells: types.CustodyBitmapData,
seq: nextSeq(),
}
reschedule[ann.origin] = struct{}{}
continue
}
if _, ok := f.partial[hash]; ok {
// 2) Decided to send partial request of the tx
if f.waitlist[hash] != nil {
if ann.cells != types.CustodyBitmapAll {
// Availability check is only meaningful with full availability announcements
continue
}
// Transaction is at the stage of availability check
// Add the peer to the peer list with full availability (waitlist)
f.waitlist[hash][ann.origin] = struct{}{}
if waitslots := f.waitslots[ann.origin]; waitslots != nil {
waitslots[hash] = struct{}{}
} else {
f.waitslots[ann.origin] = map[common.Hash]struct{}{
hash: {},
}
}
if len(f.waitlist[hash]) >= availabilityThreshold {
// Passed availability check, move to fetching stage
blobFetcherWaitTime.Update(int64(time.Duration(f.clock.Now() - f.waittime[hash])))
for peer := range f.waitlist[hash] {
if f.announces[peer] == nil {
f.announces[peer] = make(map[common.Hash]*cellWithSeq)
}
f.announces[peer][hash] = &cellWithSeq{
cells: f.custody,
seq: nextSeq(),
}
delete(f.waitslots[peer], hash)
if len(f.waitslots[peer]) == 0 {
delete(f.waitslots, peer)
}
reschedule[peer] = struct{}{}
}
delete(f.waitlist, hash)
delete(f.waittime, hash)
}
continue
}
if ann.cells.Intersection(f.custody).OneCount() == 0 {
// If there's no custody overlapping in ann, it can be ignored
continue
}
// Add this peer as a possible fetch source
// todo: Did we remove fetch from partial
if f.announces[ann.origin] == nil {
f.announces[ann.origin] = make(map[common.Hash]*cellWithSeq)
}
f.announces[ann.origin][hash] = &cellWithSeq{
cells: ann.cells.Intersection(f.custody),
seq: nextSeq(),
}
reschedule[ann.origin] = struct{}{}
}
}
// If a new item was added to the waitlist, schedule its timeout
if idleWait && len(f.waittime) > 0 {
f.rescheduleWait(waitTimer, waitTrigger)
}
// If this is a new peer and that peer sent transaction with payload flag,
// schedule transaction fetches from it
if !oldPeer && len(f.announces[ann.origin]) > 0 {
f.scheduleFetches(timeoutTimer, timeoutTrigger, reschedule)
}
case <-waitTrigger:
// At least one transaction's waiting time ran out. Instead of dropping,
// convert timed-out partial fetches to full fetches so we don't lose
// the transaction. All peers in the waitlist announced full custody
// (that was the entry condition), so they can serve as full fetch sources.
reschedule := make(map[string]struct{})
for hash, instance := range f.waittime {
if time.Duration(f.clock.Now()-instance)+txGatherSlack > blobAvailabilityTimeout {
// partial -> full conversion
delete(f.partial, hash)
f.full[hash] = struct{}{}
blobAnnounceTimeoutMeter.Mark(1)
for peer := range f.waitlist[hash] {
if f.announces[peer] == nil {
f.announces[peer] = make(map[common.Hash]*cellWithSeq)
}
f.announces[peer][hash] = &cellWithSeq{
cells: types.CustodyBitmapData,
seq: f.txSeq,
}
f.txSeq++
delete(f.waitslots[peer], hash)
if len(f.waitslots[peer]) == 0 {
delete(f.waitslots, peer)
}
reschedule[peer] = struct{}{}
}
delete(f.waittime, hash)
delete(f.waitlist, hash)
}
}
if len(reschedule) > 0 {
f.scheduleFetches(timeoutTimer, timeoutTrigger, reschedule)
}
// If transactions are still waiting for availability, reschedule the wait timer
if len(f.waittime) > 0 {
f.rescheduleWait(waitTimer, waitTrigger)
}
case <-timeoutTrigger:
// Clean up any expired retrievals and avoid re-requesting them from the
// same peer (either overloaded or malicious, useless in both cases).
// Update blobpool according to availability result.
for peer, requests := range f.requests {
newRequests := make([]*cellRequest, 0)
for _, req := range requests {
if time.Duration(f.clock.Now()-req.time)+txGatherSlack > blobFetchTimeout {
blobRequestTimeoutMeter.Mark(int64(len(req.txs)))
for _, hash := range req.txs {
// Do not request the same tx from this peer
delete(f.announces[peer], hash)
delete(f.alternates[hash], peer)
// Allow other candidates to be requested these cells
f.fetches[hash].fetching = f.fetches[hash].fetching.Difference(req.cells)
// Drop cells if there is no alternate source to fetch cells from
if len(f.alternates[hash]) == 0 {
delete(f.alternates, hash)
delete(f.fetches, hash)
}
}
if len(f.announces[peer]) == 0 {
delete(f.announces, peer)
}
} else {
newRequests = append(newRequests, req)
}
}
f.requests[peer] = newRequests
if len(f.requests[peer]) == 0 {
delete(f.requests, peer)
}
}
// Schedule a new transaction retrieval
f.scheduleFetches(timeoutTimer, timeoutTrigger, nil)
// Trigger timeout for new schedule
f.rescheduleTimeout(timeoutTimer, timeoutTrigger)
case delivery := <-f.cleanup:
// Remove from announce
var requestId int
var request *cellRequest
for _, hash := range delivery.txs {
// Find the request
for i, req := range f.requests[delivery.origin] {
if slices.Contains(req.txs, hash) && req.cells == delivery.cellBitmap {
request = req
requestId = i
break
}
}
if request != nil {
break
}
}
if request == nil {
// peer sent cells not requested. ignore
break
}
for i, hash := range delivery.txs {
if !slices.Contains(request.txs, hash) {
// Unexpected hash, ignore
continue
}
indices := delivery.cellBitmap.Indices()
cellsPerBlob := len(indices)
if cellsPerBlob > 0 {
status := f.fetches[hash]
blobCount := len(delivery.cells[i]) / cellsPerBlob
if status.blobCount == 0 {
status.blobCount = blobCount
status.deliveries = make(map[string]*PeerCellDelivery)
}
status.deliveries[delivery.origin] = &PeerCellDelivery{
Cells: delivery.cells[i],
Indices: indices,
}
status.fetched = append(status.fetched, indices...)
}
// Update announces of this peer
delete(f.announces[delivery.origin], hash)
if len(f.announces[delivery.origin]) == 0 {
delete(f.announces, delivery.origin)
}
delete(f.alternates[hash], delivery.origin)
if len(f.alternates[hash]) == 0 {
delete(f.alternates, hash)
}
// Check whether the all required cells are fetched
completed := false
if _, ok := f.full[hash]; ok && len(f.fetches[hash].fetched) >= kzg4844.DataPerBlob {
completed = true
} else if _, ok := f.partial[hash]; ok {
fetched := make([]uint64, len(f.fetches[hash].fetched))
copy(fetched, f.fetches[hash].fetched)
slices.Sort(fetched)
custodyIndices := f.custody.Indices()
completed = slices.Equal(fetched, custodyIndices)
}
if completed {
blobFetcherFetchTime.Update(int64(time.Duration(f.clock.Now() - request.time)))
status := f.fetches[hash]
collectedCustody := types.NewCustodyBitmap(status.fetched)
f.fn.AddCells(hash, status.deliveries, collectedCustody)
for peer, txset := range f.announces {
delete(txset, hash)
if len(txset) == 0 {
delete(f.announces, peer)
}
}
delete(f.alternates, hash)
delete(f.fetches, hash)
}
}
blobRequestDoneMeter.Mark(int64(len(delivery.txs)))
// Remove the request
f.requests[delivery.origin][requestId] = f.requests[delivery.origin][len(f.requests[delivery.origin])-1]
f.requests[delivery.origin] = f.requests[delivery.origin][:len(f.requests[delivery.origin])-1]
if len(f.requests[delivery.origin]) == 0 {
delete(f.requests, delivery.origin)
}
// Reschedule missing transactions in the request
// Anything not delivered should be re-scheduled (with or without
// this peer, depending on the response cutoff)
delivered := make(map[common.Hash]struct{})
for _, hash := range delivery.txs {
delivered[hash] = struct{}{}
}
cutoff := len(request.txs)
for i, hash := range request.txs {
if _, ok := delivered[hash]; ok {
cutoff = i
continue
}
}
// Reschedule missing hashes from alternates, not-fulfilled from alt+self
for i, hash := range request.txs {
if _, ok := delivered[hash]; !ok {
// Not delivered
if i < cutoff {
// Remove origin from candidate sources for partial responses
delete(f.alternates[hash], delivery.origin)
delete(f.announces[delivery.origin], hash)
if len(f.announces[delivery.origin]) == 0 {
delete(f.announces, delivery.origin)
}
}
// Mark cells deliverable by other peers
if f.fetches[hash] != nil {
f.fetches[hash].fetching = f.fetches[hash].fetching.Difference(delivery.cellBitmap)
}
}
}
// Something was delivered, try to reschedule requests
f.scheduleFetches(timeoutTimer, timeoutTrigger, nil) // Partial delivery may enable others to deliver too
case drop := <-f.drop:
// A peer was dropped, remove all traces of it
delete(f.peerTokens, drop.peer)
if _, ok := f.waitslots[drop.peer]; ok {
for hash := range f.waitslots[drop.peer] {
delete(f.waitlist[hash], drop.peer)
if len(f.waitlist[hash]) == 0 {
delete(f.waitlist, hash)
delete(f.waittime, hash)
}
}
delete(f.waitslots, drop.peer)
if len(f.waitlist) > 0 {
f.rescheduleWait(waitTimer, waitTrigger)
}
}
// Clean up general announcement tracking
if _, ok := f.announces[drop.peer]; ok {
for hash := range f.announces[drop.peer] {
delete(f.alternates[hash], drop.peer)
if len(f.alternates[hash]) == 0 {
delete(f.alternates, hash)
}
}
delete(f.announces, drop.peer)
}
delete(f.announces, drop.peer)
// Clean up any active requests
if request, ok := f.requests[drop.peer]; ok && len(request) != 0 {
for _, req := range request {
for _, hash := range req.txs {
// Undelivered hash, reschedule if there's an alternative origin available
f.fetches[hash].fetching = f.fetches[hash].fetching.Difference(req.cells)
delete(f.alternates[hash], drop.peer)
if len(f.alternates[hash]) == 0 {
delete(f.alternates, hash)
delete(f.fetches, hash)
}
}
}
delete(f.requests, drop.peer)
// If a request was cancelled, check if anything needs to be rescheduled
f.scheduleFetches(timeoutTimer, timeoutTrigger, nil)
f.rescheduleTimeout(timeoutTimer, timeoutTrigger)
}
case cells := <-f.custodyCh:
f.custody = cells
case <-f.quit:
return
}
// Update metrics gauges
blobFetcherWaitingPeers.Update(int64(len(f.waitslots)))
blobFetcherWaitingHashes.Update(int64(len(f.waitlist)))
blobFetcherQueueingPeers.Update(int64(len(f.announces) - len(f.requests)))
blobFetcherQueueingHashes.Update(int64(len(f.announces)))
blobFetcherFetchingPeers.Update(int64(len(f.requests)))
blobFetcherFetchingHashes.Update(int64(len(f.fetches)))
// Loop did something, ping the step notifier if needed (tests)
if f.step != nil {
f.step <- struct{}{}
}
}
}
func (f *BlobFetcher) rescheduleWait(timer *mclock.Timer, trigger chan struct{}) {
if *timer != nil {
(*timer).Stop()
}
now := f.clock.Now()
earliest := now
for _, instance := range f.waittime {
if earliest > instance {
earliest = instance
if txArriveTimeout-time.Duration(now-earliest) < txGatherSlack {
break
}
}
}
*timer = f.clock.AfterFunc(txArriveTimeout-time.Duration(now-earliest), func() {
trigger <- struct{}{}
})
}
// Exactly same as the one in TxFetcher
func (f *BlobFetcher) rescheduleTimeout(timer *mclock.Timer, trigger chan struct{}) {
if *timer != nil {
(*timer).Stop()
}
now := f.clock.Now()
earliest := now
for _, requests := range f.requests {
for _, req := range requests {
// If this request already timed out, skip it altogether
if req.txs == nil {
continue
}
if earliest > req.time {
earliest = req.time
if blobFetchTimeout-time.Duration(now-earliest) < txGatherSlack {
break
}
}
}
}
*timer = f.clock.AfterFunc(blobFetchTimeout-time.Duration(now-earliest), func() {
trigger <- struct{}{}
})
}
// consumeToken consumes n tokens from peer's cell-request budget.
// It returns false if the remaining tokens cannot cover n.
func (f *BlobFetcher) consumeToken(peer string, n int) bool {
now := f.clock.Now()
b, ok := f.peerTokens[peer]
if !ok {
b = &token{amount: maxCellRequests, last: now}
f.peerTokens[peer] = b
} else {
// Here, fractional remaining elapsed time is left in b.last
// so that it can be carried over to the next call
elapsed := time.Duration(now - b.last)
if add := int64(elapsed / refillInterval); add > 0 {
b.amount += add
if b.amount > maxCellRequests {
b.amount = maxCellRequests
}
b.last += mclock.AbsTime(time.Duration(add) * refillInterval)
}
}
if b.amount < int64(n) {
return false
}
b.amount -= int64(n)
return true
}
func (f *BlobFetcher) scheduleFetches(timer *mclock.Timer, timeout chan struct{}, whitelist map[string]struct{}) {
// Gather the set of peers we want to retrieve from (default to all)
actives := whitelist
if actives == nil {
actives = make(map[string]struct{})
for peer := range f.announces {
actives[peer] = struct{}{}
}
}
if len(actives) == 0 {
return
}
wasIdle := len(f.requests) == 0
// For each active peer, try to schedule some payload fetches.
for peer := range f.peers(actives) {
if len(f.announces[peer]) == 0 || len(f.requests[peer]) != 0 {
continue
}
var (
hashes []common.Hash
custodies []types.CustodyBitmap
)
for hash, cells := range f.announcesByArrival(f.announces[peer]) {
var unfetched types.CustodyBitmap
if f.fetches[hash] == nil {
// tx is not being fetched
unfetched = cells
} else {
unfetched = cells.Difference(f.fetches[hash].fetching)
}
// Mark fetching for unfetched cells if the peer has enough token.
// Otherwise, the next peer who announced the hash and has token will be selected
// in the next loop
if unfetched.OneCount() > 0 && f.consumeToken(peer, unfetched.OneCount()) {
if f.fetches[hash] == nil {
f.fetches[hash] = &fetchStatus{
fetching: unfetched,
fetched: make([]uint64, 0),
}
} else {
f.fetches[hash].fetching = f.fetches[hash].fetching.Union(unfetched)
}
// Accumulate the hash and stop if the limit was reached
hashes = append(hashes, hash)
custodies = append(custodies, unfetched)
}
// Mark alternatives
if f.alternates[hash] == nil {
f.alternates[hash] = map[string]types.CustodyBitmap{
peer: cells,
}
} else {
f.alternates[hash][peer] = cells
}
// Stop once we've accumulated enough hashes for this peer
if len(hashes) >= maxPayloadRetrievals {
break
}
}
// If any hashes were allocated, request them from the peer
if len(hashes) > 0 {
// Group hashes by custody bitmap
requestByCustody := make(map[types.CustodyBitmap]*cellRequest)
for i, hash := range hashes {
key := custodies[i]
if _, ok := requestByCustody[key]; !ok {
requestByCustody[key] = &cellRequest{
txs: []common.Hash{},
cells: custodies[i],
time: f.clock.Now(),
}
}
requestByCustody[key].txs = append(requestByCustody[key].txs, hash)
}
// construct request
var request []*cellRequest
for _, cr := range requestByCustody {
request = append(request, cr)
}
f.requests[peer] = request
go func() {
for _, req := range request {
blobRequestOutMeter.Mark(int64(len(req.txs)))
if err := f.fn.FetchPayloads(peer, req.txs, req.cells); err != nil {
blobRequestFailMeter.Mark(int64(len(req.txs)))
f.Drop(peer)
break
}
}
}()
}
}
// If a new request was fired, schedule a timeout timer
if wasIdle && len(f.requests) > 0 {
f.rescheduleTimeout(timer, timeout)
}
}
// announcesByArrival returns an iterator over the given announcements
// in arrival order. We enforce an arrival ordering to minimize
// the chances of transaction nonce-gaps, which result in
// transactions being rejected by the txpool.
func (f *BlobFetcher) announcesByArrival(announces map[common.Hash]*cellWithSeq) iter.Seq2[common.Hash, types.CustodyBitmap] {
return func(yield func(hash common.Hash, cells types.CustodyBitmap) bool) {
type announcement struct {
hash common.Hash
cells types.CustodyBitmap
seq uint64
}
// Process announcements by their arrival order
list := make([]announcement, 0, len(announces))
for hash, entry := range announces {
list = append(list, announcement{hash: hash, cells: entry.cells, seq: entry.seq})
}
sort.Slice(list, func(i, j int) bool {
return list[i].seq < list[j].seq
})
for i := range list {
if !yield(list[i].hash, list[i].cells) {
return
}
}
}
}
// peers returns an iterator over a map of peers in production, but during
// testing it does a deterministic sorted random to allow reproducing issues.
func (f *BlobFetcher) peers(peers map[string]struct{}) iter.Seq[string] {
return func(yield func(peer string) bool) {
// If we're running production(step == nil), use whatever Go's map gives us
if f.step == nil {
for peer := range peers {
if !yield(peer) {
return
}
}
return
}
// We're running the test suite, make iteration deterministic (sorted by peer id)
list := make([]string, 0, len(peers))
for peer := range peers {
list = append(list, peer)
}
sort.Strings(list)
for _, peer := range list {
if !yield(peer) {
return
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -57,4 +57,26 @@ var (
// to become "unfrozen", either by eventually replying to the request // to become "unfrozen", either by eventually replying to the request
// or by being dropped, measuring from the moment the request was sent. // or by being dropped, measuring from the moment the request was sent.
txFetcherSlowWait = metrics.NewRegisteredHistogram("eth/fetcher/transaction/slow/wait", nil, metrics.NewExpDecaySample(1028, 0.015)) txFetcherSlowWait = metrics.NewRegisteredHistogram("eth/fetcher/transaction/slow/wait", nil, metrics.NewExpDecaySample(1028, 0.015))
blobAnnounceInMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/announces/in", nil)
blobAnnounceDOSMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/announces/dos", nil)
// This metric tracks partial→full conversions due to availability timeout
blobAnnounceTimeoutMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/announces/timeout", nil)
blobRequestOutMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/request/out", nil)
blobRequestFailMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/request/fail", nil)
blobRequestDoneMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/request/done", nil)
blobRequestTimeoutMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/request/timeout", nil)
blobReplyInMeter = metrics.NewRegisteredMeter("eth/fetcher/blob/replies/in", nil)
blobFetcherWaitingPeers = metrics.NewRegisteredGauge("eth/fetcher/blob/waiting/peers", nil)
blobFetcherWaitingHashes = metrics.NewRegisteredGauge("eth/fetcher/blob/waiting/hashes", nil)
blobFetcherQueueingPeers = metrics.NewRegisteredGauge("eth/fetcher/blob/queueing/peers", nil)
blobFetcherQueueingHashes = metrics.NewRegisteredGauge("eth/fetcher/blob/queueing/hashes", nil)
blobFetcherFetchingPeers = metrics.NewRegisteredGauge("eth/fetcher/blob/fetching/peers", nil)
blobFetcherFetchingHashes = metrics.NewRegisteredGauge("eth/fetcher/blob/fetching/hashes", nil)
blobFetcherWaitTime = metrics.NewRegisteredHistogram("eth/fetcher/blob/wait/time", nil, metrics.NewExpDecaySample(1028, 0.015))
blobFetcherFetchTime = metrics.NewRegisteredHistogram("eth/fetcher/blob/fetch/time", nil, metrics.NewExpDecaySample(1028, 0.015))
) )

View file

@ -29,8 +29,11 @@ import (
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
) )
const ( const (
@ -185,6 +188,8 @@ type TxFetcher struct {
fetchTxs func(string, []common.Hash) error // Retrieves a set of txs from a remote peer fetchTxs func(string, []common.Hash) error // Retrieves a set of txs from a remote peer
dropPeer func(string) // Drops a peer in case of announcement violation dropPeer func(string) // Drops a peer in case of announcement violation
buffer *blobpool.BlobBuffer
step chan struct{} // Notification channel when the fetcher loop iterates step chan struct{} // Notification channel when the fetcher loop iterates
clock mclock.Clock // Monotonic clock or simulated clock for tests clock mclock.Clock // Monotonic clock or simulated clock for tests
realTime func() time.Time // Real system time or simulated time for tests realTime func() time.Time // Real system time or simulated time for tests
@ -194,8 +199,9 @@ type TxFetcher struct {
// NewTxFetcher creates a transaction fetcher to retrieve transaction // NewTxFetcher creates a transaction fetcher to retrieve transaction
// based on hash announcements. // based on hash announcements.
// Chain can be nil to disable on-chain checks. // Chain can be nil to disable on-chain checks.
func NewTxFetcher(chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string)) *TxFetcher { func NewTxFetcher(chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error,
return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, mclock.System{}, time.Now, nil) dropPeer func(string), buffer *blobpool.BlobBuffer) *TxFetcher {
return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, buffer, mclock.System{}, time.Now, nil)
} }
// NewTxFetcherForTests is a testing method to mock out the realtime clock with // NewTxFetcherForTests is a testing method to mock out the realtime clock with
@ -203,7 +209,7 @@ func NewTxFetcher(chain *core.BlockChain, validateMeta func(common.Hash, byte) e
// Chain can be nil to disable on-chain checks. // Chain can be nil to disable on-chain checks.
func NewTxFetcherForTests( func NewTxFetcherForTests(
chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string), chain *core.BlockChain, validateMeta func(common.Hash, byte) error, addTxs func([]*types.Transaction) []error, fetchTxs func(string, []common.Hash) error, dropPeer func(string),
clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher { buffer *blobpool.BlobBuffer, clock mclock.Clock, realTime func() time.Time, rand *mrand.Rand) *TxFetcher {
return &TxFetcher{ return &TxFetcher{
notify: make(chan *txAnnounce), notify: make(chan *txAnnounce),
cleanup: make(chan *txDelivery), cleanup: make(chan *txDelivery),
@ -224,6 +230,7 @@ func NewTxFetcherForTests(
addTxs: addTxs, addTxs: addTxs,
fetchTxs: fetchTxs, fetchTxs: fetchTxs,
dropPeer: dropPeer, dropPeer: dropPeer,
buffer: buffer,
clock: clock, clock: clock,
realTime: realTime, realTime: realTime,
rand: rand, rand: rand,
@ -231,8 +238,8 @@ func NewTxFetcherForTests(
} }
// Notify announces the fetcher of the potential availability of a new batch of // Notify announces the fetcher of the potential availability of a new batch of
// transactions in the network. // transactions in the network. It returns array of hashes decided to be fetched.
func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []common.Hash) error { func (f *TxFetcher) Notify(peer string, kinds []byte, sizes []uint32, hashes []common.Hash) ([]common.Hash, error) {
// Keep track of all the announced transactions // Keep track of all the announced transactions
txAnnounceInMeter.Mark(int64(len(hashes))) txAnnounceInMeter.Mark(int64(len(hashes)))
@ -245,13 +252,18 @@ func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []c
unknownHashes = make([]common.Hash, 0, len(hashes)) unknownHashes = make([]common.Hash, 0, len(hashes))
unknownMetas = make([]txMetadata, 0, len(hashes)) unknownMetas = make([]txMetadata, 0, len(hashes))
blobFetchHashes = make([]common.Hash, 0, len(hashes))
duplicate int64 duplicate int64
onchain int64 onchain int64
underpriced int64 underpriced int64
) )
for i, hash := range hashes { for i, hash := range hashes {
err := f.validateMeta(hash, types[i]) err := f.validateMeta(hash, kinds[i])
if errors.Is(err, txpool.ErrAlreadyKnown) { if errors.Is(err, txpool.ErrAlreadyKnown) {
if kinds[i] == types.BlobTxType {
blobFetchHashes = append(blobFetchHashes, hash)
}
duplicate++ duplicate++
continue continue
} }
@ -271,11 +283,14 @@ func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []c
} }
unknownHashes = append(unknownHashes, hash) unknownHashes = append(unknownHashes, hash)
if kinds[i] == types.BlobTxType {
blobFetchHashes = append(blobFetchHashes, hash)
}
// Transaction metadata has been available since eth68, and all // Transaction metadata has been available since eth68, and all
// legacy eth protocols (prior to eth68) have been deprecated. // legacy eth protocols (prior to eth68) have been deprecated.
// Therefore, metadata is always expected in the announcement. // Therefore, metadata is always expected in the announcement.
unknownMetas = append(unknownMetas, txMetadata{kind: types[i], size: sizes[i]}) unknownMetas = append(unknownMetas, txMetadata{kind: kinds[i], size: sizes[i]})
} }
txAnnounceKnownMeter.Mark(duplicate) txAnnounceKnownMeter.Mark(duplicate)
txAnnounceUnderpricedMeter.Mark(underpriced) txAnnounceUnderpricedMeter.Mark(underpriced)
@ -283,14 +298,14 @@ func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []c
// If anything's left to announce, push it into the internal loop // If anything's left to announce, push it into the internal loop
if len(unknownHashes) == 0 { if len(unknownHashes) == 0 {
return nil return blobFetchHashes, nil
} }
announce := &txAnnounce{origin: peer, hashes: unknownHashes, metas: unknownMetas} announce := &txAnnounce{origin: peer, hashes: unknownHashes, metas: unknownMetas}
select { select {
case f.notify <- announce: case f.notify <- announce:
return nil return blobFetchHashes, nil
case <-f.quit: case <-f.quit:
return errTerminated return nil, errTerminated
} }
} }
@ -304,26 +319,36 @@ func (f *TxFetcher) isKnownUnderpriced(hash common.Hash) bool {
return ok return ok
} }
type deliveryMetrics struct {
inMeter *metrics.Meter
knownMeter *metrics.Meter
underpricedMeter *metrics.Meter
otherRejectMeter *metrics.Meter
}
// Enqueue imports a batch of received transaction into the transaction pool // Enqueue imports a batch of received transaction into the transaction pool
// and the fetcher. This method may be called by both transaction broadcasts and // and the fetcher. This method may be called by both transaction broadcasts and
// direct request replies. The differentiation is important so the fetcher can // direct request replies. The differentiation is important so the fetcher can
// re-schedule missing transactions as soon as possible. // re-schedule missing transactions as soon as possible.
func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) error { func (f *TxFetcher) Enqueue(peer string, version uint, txs []*types.Transaction, direct bool) error {
var ( var violation error
inMeter = txReplyInMeter
knownMeter = txReplyKnownMeter metrics := deliveryMetrics{
underpricedMeter = txReplyUnderpricedMeter inMeter: txReplyInMeter,
otherRejectMeter = txReplyOtherRejectMeter knownMeter: txReplyKnownMeter,
violation error underpricedMeter: txReplyUnderpricedMeter,
) otherRejectMeter: txReplyOtherRejectMeter,
}
if !direct { if !direct {
inMeter = txBroadcastInMeter metrics = deliveryMetrics{
knownMeter = txBroadcastKnownMeter inMeter: txBroadcastInMeter,
underpricedMeter = txBroadcastUnderpricedMeter knownMeter: txBroadcastKnownMeter,
otherRejectMeter = txBroadcastOtherRejectMeter underpricedMeter: txBroadcastUnderpricedMeter,
otherRejectMeter: txBroadcastOtherRejectMeter,
}
} }
// Keep track of all the propagated transactions // Keep track of all the propagated transactions
inMeter.Mark(int64(len(txs))) metrics.inMeter.Mark(int64(len(txs)))
// Push all the transactions into the pool, tracking underpriced ones to avoid // Push all the transactions into the pool, tracking underpriced ones to avoid
// re-requesting them and dropping the peer in case of malicious transfers. // re-requesting them and dropping the peer in case of malicious transfers.
@ -337,38 +362,37 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
if end > len(txs) { if end > len(txs) {
end = len(txs) end = len(txs)
} }
var (
duplicate int64
underpriced int64
otherreject int64
)
batch := txs[i:end] batch := txs[i:end]
var (
for j, err := range f.addTxs(batch) { poolTxs []*types.Transaction
// Track the transaction hash if the price is too low for us. blobTxs []*types.Transaction
// Avoid re-request this transaction when we receive another )
// announcement. if version >= eth.ETH72 {
if errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow) { for _, tx := range batch {
f.underpriced.Add(batch[j].Hash(), batch[j].Time()) if tx.Type() == types.BlobTxType {
blobTxs = append(blobTxs, tx)
} else {
poolTxs = append(poolTxs, tx)
}
} }
// Track a few interesting failure types } else {
switch { poolTxs = batch
case err == nil: // Noop, but need to handle to not count these }
batch = append(poolTxs, blobTxs...)
case errors.Is(err, txpool.ErrAlreadyKnown): // Add regular tx to pool, blob tx to buffer.
duplicate++ errs := append(f.addTxs(poolTxs), f.buffer.AddTx(blobTxs, peer)...)
case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow): hashes := make([]common.Hash, len(batch))
underpriced++ for j := range batch {
hashes[j] = batch[j].Hash()
case errors.Is(err, txpool.ErrKZGVerificationError): }
for j, err := range errs {
if errors.Is(err, txpool.ErrKZGVerificationError) || errors.Is(err, txpool.ErrSidecarFormatError) {
// KZG verification failed, terminate transaction processing immediately. // KZG verification failed, terminate transaction processing immediately.
// Since KZG verification is computationally expensive, this acts as a // Since KZG verification is computationally expensive, this acts as a
// defensive measure against potential DoS attacks. // defensive measure against potential DoS attacks.
violation = err violation = err
default:
otherreject++
} }
added = append(added, batch[j].Hash()) added = append(added, batch[j].Hash())
metas = append(metas, txMetadata{ metas = append(metas, txMetadata{
@ -381,13 +405,11 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
break break
} }
} }
knownMeter.Mark(duplicate) otherreject := f.handleAddErrors(hashes, errs, metrics)
underpricedMeter.Mark(underpriced) // If 'other reject' is >25% of the deliveries in any batch, sleep a bit
otherRejectMeter.Mark(otherreject) // to throttle the misbehaving peer.
if otherreject > int64((len(hashes)+3)/4) {
// If 'other reject' is >25% of the deliveries in any batch, sleep a bit. log.Debug("Peer delivering stale or invalid transactions", "rejected", otherreject)
if otherreject > int64((len(batch)+3)/4) {
log.Debug("Peer delivering stale or invalid transactions", "peer", peer, "rejected", otherreject)
time.Sleep(200 * time.Millisecond) time.Sleep(200 * time.Millisecond)
} }
// If we encountered a protocol violation, disconnect this peer. // If we encountered a protocol violation, disconnect this peer.
@ -403,6 +425,36 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
} }
} }
func (f *TxFetcher) handleAddErrors(txs []common.Hash, errs []error, metrics deliveryMetrics) (otherreject int64) {
var (
duplicate int64
underpriced int64
)
for i, err := range errs {
// Track a few interesting failure types
switch {
case err == nil: // Noop, but need to handle to not count these
case errors.Is(err, txpool.ErrAlreadyKnown):
duplicate++
// Track the transaction hash if the price is too low for us.
// Avoid re-request this transaction when we receive another
// announcement.
case errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow):
f.underpriced.Add(txs[i], f.realTime())
underpriced++
default:
otherreject++
}
}
metrics.knownMeter.Mark(duplicate)
metrics.underpricedMeter.Mark(underpriced)
metrics.otherRejectMeter.Mark(otherreject)
return otherreject
}
// Drop should be called when a peer disconnects. It cleans up all the internal // Drop should be called when a peer disconnects. It cleans up all the internal
// data structures of the given node. // data structures of the given node.
func (f *TxFetcher) Drop(peer string) error { func (f *TxFetcher) Drop(peer string) error {
@ -448,6 +500,14 @@ func (f *TxFetcher) loop() {
} }
for { for {
txs, errs := f.buffer.Flush()
f.handleAddErrors(txs, errs, deliveryMetrics{
inMeter: txReplyInMeter,
knownMeter: txReplyKnownMeter,
underpricedMeter: txReplyUnderpricedMeter,
otherRejectMeter: txReplyOtherRejectMeter,
})
select { select {
case ann := <-f.notify: case ann := <-f.notify:
// Drop part of the new announcements if there are too many accumulated. // Drop part of the new announcements if there are too many accumulated.

View file

@ -28,9 +28,11 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
@ -60,9 +62,10 @@ type doTxNotify struct {
sizes []uint32 sizes []uint32
} }
type doTxEnqueue struct { type doTxEnqueue struct {
peer string peer string
txs []*types.Transaction version uint
direct bool txs []*types.Transaction
direct bool
} }
type doWait struct { type doWait struct {
time time.Duration time time.Duration
@ -87,6 +90,16 @@ type txFetcherTest struct {
steps []interface{} steps []interface{}
} }
// newTestBlobBuffer returns a BlobBuffer with no-op callbacks for tests that
// don't exercise blob handling but still need a non-nil buffer.
func newTestBlobBuffer() *blobpool.BlobBuffer {
return blobpool.NewBlobBuffer(blobpool.BlobBufferFunctions{
ValidateTx: func(*types.Transaction) error { return nil },
AddToPool: func(*blobpool.BlobTxForPool) error { return nil },
DropPeer: func(string) {},
})
}
// newTestTxFetcher creates a tx fetcher with noop callbacks, simulated clock, // newTestTxFetcher creates a tx fetcher with noop callbacks, simulated clock,
// and deterministic randomness. // and deterministic randomness.
func newTestTxFetcher() *TxFetcher { func newTestTxFetcher() *TxFetcher {
@ -98,6 +111,7 @@ func newTestTxFetcher() *TxFetcher {
}, },
func(string, []common.Hash) error { return nil }, func(string, []common.Hash) error { return nil },
nil, nil,
newTestBlobBuffer(),
) )
} }
@ -1888,7 +1902,7 @@ func testTransactionFetcher(t *testing.T, tt txFetcherTest) {
// Process the original or expanded steps // Process the original or expanded steps
switch step := step.(type) { switch step := step.(type) {
case doTxNotify: case doTxNotify:
if err := fetcher.Notify(step.peer, step.types, step.sizes, step.hashes); err != nil { if _, err := fetcher.Notify(step.peer, step.types, step.sizes, step.hashes); err != nil {
t.Errorf("step %d: %v", i, err) t.Errorf("step %d: %v", i, err)
} }
<-wait // Fetcher needs to process this, wait until it's done <-wait // Fetcher needs to process this, wait until it's done
@ -1899,7 +1913,7 @@ func testTransactionFetcher(t *testing.T, tt txFetcherTest) {
} }
case doTxEnqueue: case doTxEnqueue:
if err := fetcher.Enqueue(step.peer, step.txs, step.direct); err != nil { if err := fetcher.Enqueue(step.peer, step.version, step.txs, step.direct); err != nil {
t.Errorf("step %d: %v", i, err) t.Errorf("step %d: %v", i, err)
} }
<-wait // Fetcher needs to process this, wait until it's done <-wait // Fetcher needs to process this, wait until it's done
@ -2203,6 +2217,7 @@ func TestTransactionForgotten(t *testing.T) {
}, },
func(string, []common.Hash) error { return nil }, func(string, []common.Hash) error { return nil },
func(string) {}, func(string) {},
newTestBlobBuffer(),
mockClock, mockClock,
mockTime, mockTime,
rand.New(rand.NewSource(0)), // Use fixed seed for deterministic behavior rand.New(rand.NewSource(0)), // Use fixed seed for deterministic behavior
@ -2219,7 +2234,7 @@ func TestTransactionForgotten(t *testing.T) {
tx2.SetTime(now) tx2.SetTime(now)
// Initial state: both transactions should be marked as underpriced // Initial state: both transactions should be marked as underpriced
if err := fetcher.Enqueue("peer", []*types.Transaction{tx1, tx2}, false); err != nil { if err := fetcher.Enqueue("peer", eth.ETH70, []*types.Transaction{tx1, tx2}, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !fetcher.isKnownUnderpriced(tx1.Hash()) { if !fetcher.isKnownUnderpriced(tx1.Hash()) {
@ -2268,7 +2283,7 @@ func TestTransactionForgotten(t *testing.T) {
// Re-enqueue tx1 with updated timestamp // Re-enqueue tx1 with updated timestamp
tx1.SetTime(mockTime()) tx1.SetTime(mockTime())
if err := fetcher.Enqueue("peer", []*types.Transaction{tx1}, false); err != nil { if err := fetcher.Enqueue("peer", eth.ETH70, []*types.Transaction{tx1}, false); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !fetcher.isKnownUnderpriced(tx1.Hash()) { if !fetcher.isKnownUnderpriced(tx1.Hash()) {

View file

@ -32,7 +32,9 @@ import (
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/downloader"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/fetcher" "github.com/ethereum/go-ethereum/eth/fetcher"
@ -75,7 +77,7 @@ type txPool interface {
// GetRLP retrieves the RLP-encoded transaction from local txpool // GetRLP retrieves the RLP-encoded transaction from local txpool
// with given tx hash. // with given tx hash.
GetRLP(hash common.Hash) []byte GetRLP(hash common.Hash, version uint) []byte
// GetMetadata returns the transaction type and transaction size with the // GetMetadata returns the transaction type and transaction size with the
// given transaction hash. // given transaction hash.
@ -97,18 +99,31 @@ type txPool interface {
FilterType(kind byte) bool FilterType(kind byte) bool
} }
// blobPool defines the methods needed from a blob pool implementation to
// support cell-based blob data availability.
type blobPool interface {
Has(hash common.Hash) bool
GetBlobHashes(hash common.Hash) []common.Hash
GetBlobCells(vhashes []common.Hash, mask types.CustodyBitmap) ([][]*kzg4844.Cell, [][]*kzg4844.Proof, error)
GetCustody(hash common.Hash) *types.CustodyBitmap
AddPooledTx(pooledTx *blobpool.BlobTxForPool) error
ValidateTxBasics(pooledTx *types.Transaction) error
}
// handlerConfig is the collection of initialization parameters to create a full // handlerConfig is the collection of initialization parameters to create a full
// node network handler. // node network handler.
type handlerConfig struct { type handlerConfig struct {
NodeID enode.ID // P2P node ID used for tx propagation topology NodeID enode.ID // P2P node ID used for tx propagation topology
Database ethdb.Database // Database for direct sync insertions Database ethdb.Database // Database for direct sync insertions
Chain *core.BlockChain // Blockchain to serve data from Chain *core.BlockChain // Blockchain to serve data from
TxPool txPool // Transaction pool to propagate from TxPool txPool // Transaction pool to propagate from
Network uint64 // Network identifier to advertise BlobPool blobPool // Blob pool for cell-based blob data availability
Sync ethconfig.SyncMode // Whether to snap or full sync Network uint64 // Network identifier to advertise
BloomCache uint64 // Megabytes to alloc for snap sync bloom Sync ethconfig.SyncMode // Whether to snap or full sync
RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges BloomCache uint64 // Megabytes to alloc for snap sync bloom
SnapV2 bool // Whether to advertise and sync via the snap/2 protocol RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges
SnapV2 bool // Whether to advertise and sync via the snap/2 protocol
FetchProbability uint64 // Full blob fetch probability for sparse blobpool (blobFetcher)
} }
type handler struct { type handler struct {
@ -118,11 +133,13 @@ type handler struct {
database ethdb.Database database ethdb.Database
txpool txPool txpool txPool
blobpool blobPool
chain *core.BlockChain chain *core.BlockChain
maxPeers int maxPeers int
downloader *downloader.Downloader downloader *downloader.Downloader
txFetcher *fetcher.TxFetcher txFetcher *fetcher.TxFetcher
blobFetcher *fetcher.BlobFetcher
peers *peerSet peers *peerSet
txBroadcastKey [16]byte txBroadcastKey [16]byte
@ -148,6 +165,7 @@ func newHandler(config *handlerConfig) (*handler, error) {
networkID: config.Network, networkID: config.Network,
database: config.Database, database: config.Database,
txpool: config.TxPool, txpool: config.TxPool,
blobpool: config.BlobPool,
chain: config.Chain, chain: config.Chain,
peers: newPeerSet(), peers: newPeerSet(),
txBroadcastKey: newBroadcastChoiceKey(), txBroadcastKey: newBroadcastChoiceKey(),
@ -170,11 +188,19 @@ func newHandler(config *handlerConfig) (*handler, error) {
} }
return p.RequestTxs(hashes) return p.RequestTxs(hashes)
} }
// Construct the blob buffer for assembling blob txs from separate tx and cell deliveries.
blobBuffer := blobpool.NewBlobBuffer(blobpool.BlobBufferFunctions{
ValidateTx: h.blobpool.ValidateTxBasics,
AddToPool: h.blobpool.AddPooledTx,
DropPeer: h.removePeer,
})
addTxs := func(txs []*types.Transaction) []error { addTxs := func(txs []*types.Transaction) []error {
return h.txpool.Add(txs, false) return h.txpool.Add(txs, false)
} }
validateMeta := func(tx common.Hash, kind byte) error { validateMeta := func(tx common.Hash, kind byte) error {
if h.txpool.Has(tx) { if h.txpool.Has(tx) || blobBuffer.HasTx(tx) {
return txpool.ErrAlreadyKnown return txpool.ErrAlreadyKnown
} }
if !h.txpool.FilterType(kind) { if !h.txpool.FilterType(kind) {
@ -182,7 +208,30 @@ func newHandler(config *handlerConfig) (*handler, error) {
} }
return nil return nil
} }
h.txFetcher = fetcher.NewTxFetcher(h.chain, validateMeta, addTxs, fetchTx, h.removePeer) h.txFetcher = fetcher.NewTxFetcher(h.chain, validateMeta, addTxs, fetchTx, h.removePeer, blobBuffer)
// Construct the blob fetcher for cell-based blob data availability
blobCallbacks := fetcher.BlobFetcherFunctions{
FetchPayloads: func(peer string, hashes []common.Hash, cells types.CustodyBitmap) error {
p := h.peers.peer(peer)
if p == nil {
return errors.New("unknown peer")
}
return p.RequestPayload(hashes, cells)
},
HasPayload: func(hash common.Hash) bool {
return h.blobpool.Has(hash) || blobBuffer.HasCells(hash)
},
AddCells: func(hash common.Hash, deliveries map[string]*fetcher.PeerCellDelivery, custody types.CustodyBitmap) {
converted := make(map[string]*blobpool.PeerDelivery, len(deliveries))
for peer, d := range deliveries {
converted[peer] = &blobpool.PeerDelivery{Cells: d.Cells, Indices: d.Indices}
}
blobBuffer.AddCells(hash, converted, custody)
},
DropPeer: h.removePeer,
}
h.blobFetcher = fetcher.NewBlobFetcher(blobCallbacks, types.CustodyBitmapAll, nil, config.FetchProbability)
return h, nil return h, nil
} }
@ -397,6 +446,7 @@ func (h *handler) unregisterPeer(id string) {
} }
h.downloader.UnregisterPeer(id) h.downloader.UnregisterPeer(id)
h.txFetcher.Drop(id) h.txFetcher.Drop(id)
h.blobFetcher.Drop(id)
if err := h.peers.unregisterPeer(id); err != nil { if err := h.peers.unregisterPeer(id); err != nil {
logger.Error("Ethereum peer removal failed", "err", err) logger.Error("Ethereum peer removal failed", "err", err)
@ -419,6 +469,7 @@ func (h *handler) Start(maxPeers int) {
// start sync handlers // start sync handlers
h.txFetcher.Start() h.txFetcher.Start()
h.blobFetcher.Start()
// start peer handler tracker // start peer handler tracker
h.wg.Add(1) h.wg.Add(1)
@ -429,6 +480,7 @@ func (h *handler) Stop() {
h.txsSub.Unsubscribe() // quits txBroadcastLoop h.txsSub.Unsubscribe() // quits txBroadcastLoop
h.blockRange.stop() h.blockRange.stop()
h.txFetcher.Stop() h.txFetcher.Stop()
h.blobFetcher.Stop()
h.downloader.Terminate() h.downloader.Terminate()
// Quit chainSync and txsync64. // Quit chainSync and txsync64.

View file

@ -33,6 +33,7 @@ type ethHandler handler
func (h *ethHandler) Chain() *core.BlockChain { return h.chain } func (h *ethHandler) Chain() *core.BlockChain { return h.chain }
func (h *ethHandler) TxPool() eth.TxPool { return h.txpool } func (h *ethHandler) TxPool() eth.TxPool { return h.txpool }
func (h *ethHandler) BlobPool() eth.BlobPool { return h.blobpool }
// RunPeer is invoked when a peer joins on the `eth` protocol. // RunPeer is invoked when a peer joins on the `eth` protocol.
func (h *ethHandler) RunPeer(peer *eth.Peer, hand eth.Handler) error { func (h *ethHandler) RunPeer(peer *eth.Peer, hand eth.Handler) error {
@ -58,8 +59,19 @@ func (h *ethHandler) AcceptTxs() bool {
func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error { func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error {
// Consume any broadcasts and announces, forwarding the rest to the downloader // Consume any broadcasts and announces, forwarding the rest to the downloader
switch packet := packet.(type) { switch packet := packet.(type) {
case *eth.NewPooledTransactionHashesPacket: case *eth.NewPooledTransactionHashesPacket72:
return h.txFetcher.Notify(peer.ID(), packet.Types, packet.Sizes, packet.Hashes) hashes, err := h.txFetcher.Notify(peer.ID(), packet.Types, packet.Sizes, packet.Hashes)
if err != nil {
return err
}
if len(hashes) != 0 {
return h.blobFetcher.Notify(peer.ID(), hashes, packet.Mask)
}
return nil
case *eth.NewPooledTransactionHashesPacket71:
_, err := h.txFetcher.Notify(peer.ID(), packet.Types, packet.Sizes, packet.Hashes)
return err
case *eth.TransactionsPacket: case *eth.TransactionsPacket:
txs, err := packet.Items() txs, err := packet.Items()
@ -69,7 +81,7 @@ func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error {
if err := handleTransactions(peer, txs, true); err != nil { if err := handleTransactions(peer, txs, true); err != nil {
return fmt.Errorf("Transactions: %v", err) return fmt.Errorf("Transactions: %v", err)
} }
return h.txFetcher.Enqueue(peer.ID(), txs, false) return h.txFetcher.Enqueue(peer.ID(), peer.Version(), txs, false)
case *eth.PooledTransactionsPacket: case *eth.PooledTransactionsPacket:
txs, err := packet.List.Items() txs, err := packet.List.Items()
@ -79,7 +91,10 @@ func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error {
if err := handleTransactions(peer, txs, false); err != nil { if err := handleTransactions(peer, txs, false); err != nil {
return fmt.Errorf("PooledTransactions: %v", err) return fmt.Errorf("PooledTransactions: %v", err)
} }
return h.txFetcher.Enqueue(peer.ID(), txs, true) return h.txFetcher.Enqueue(peer.ID(), peer.Version(), txs, true)
case *eth.CellsResponse:
return h.blobFetcher.Enqueue(peer.ID(), packet.Hashes, packet.Cells, packet.Mask)
default: default:
return fmt.Errorf("unexpected eth packet type: %T", packet) return fmt.Errorf("unexpected eth packet type: %T", packet)

View file

@ -44,13 +44,14 @@ type testEthHandler struct {
func (h *testEthHandler) Chain() *core.BlockChain { panic("no backing chain") } func (h *testEthHandler) Chain() *core.BlockChain { panic("no backing chain") }
func (h *testEthHandler) TxPool() eth.TxPool { panic("no backing tx pool") } func (h *testEthHandler) TxPool() eth.TxPool { panic("no backing tx pool") }
func (h *testEthHandler) BlobPool() eth.BlobPool { return nil }
func (h *testEthHandler) AcceptTxs() bool { return true } func (h *testEthHandler) AcceptTxs() bool { return true }
func (h *testEthHandler) RunPeer(*eth.Peer, eth.Handler) error { panic("not used in tests") } func (h *testEthHandler) RunPeer(*eth.Peer, eth.Handler) error { panic("not used in tests") }
func (h *testEthHandler) PeerInfo(enode.ID) interface{} { panic("not used in tests") } func (h *testEthHandler) PeerInfo(enode.ID) interface{} { panic("not used in tests") }
func (h *testEthHandler) Handle(peer *eth.Peer, packet eth.Packet) error { func (h *testEthHandler) Handle(peer *eth.Peer, packet eth.Packet) error {
switch packet := packet.(type) { switch packet := packet.(type) {
case *eth.NewPooledTransactionHashesPacket: case *eth.NewPooledTransactionHashesPacket71:
h.txAnnounces.Send(packet.Hashes) h.txAnnounces.Send(packet.Hashes)
return nil return nil
@ -105,10 +106,12 @@ func testForkIDSplit(t *testing.T, protocol uint) {
_, blocksNoFork, _ = core.GenerateChainWithGenesis(gspecNoFork, engine, 2, nil) _, blocksNoFork, _ = core.GenerateChainWithGenesis(gspecNoFork, engine, 2, nil)
_, blocksProFork, _ = core.GenerateChainWithGenesis(gspecProFork, engine, 2, nil) _, blocksProFork, _ = core.GenerateChainWithGenesis(gspecProFork, engine, 2, nil)
txPool = newTestTxPool()
ethNoFork, _ = newHandler(&handlerConfig{ ethNoFork, _ = newHandler(&handlerConfig{
Database: dbNoFork, Database: dbNoFork,
Chain: chainNoFork, Chain: chainNoFork,
TxPool: newTestTxPool(), TxPool: txPool,
BlobPool: txPool,
Network: 1, Network: 1,
Sync: ethconfig.FullSync, Sync: ethconfig.FullSync,
BloomCache: 1, BloomCache: 1,
@ -116,7 +119,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
ethProFork, _ = newHandler(&handlerConfig{ ethProFork, _ = newHandler(&handlerConfig{
Database: dbProFork, Database: dbProFork,
Chain: chainProFork, Chain: chainProFork,
TxPool: newTestTxPool(), TxPool: txPool,
BlobPool: txPool,
Network: 1, Network: 1,
Sync: ethconfig.FullSync, Sync: ethconfig.FullSync,
BloomCache: 1, BloomCache: 1,
@ -137,8 +141,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
defer p2pNoFork.Close() defer p2pNoFork.Close()
defer p2pProFork.Close() defer p2pProFork.Close()
peerNoFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil, nil) peerNoFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil, nil, nil)
peerProFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, nil, nil) peerProFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, nil, nil, nil)
defer peerNoFork.Close() defer peerNoFork.Close()
defer peerProFork.Close() defer peerProFork.Close()
@ -168,8 +172,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
defer p2pNoFork.Close() defer p2pNoFork.Close()
defer p2pProFork.Close() defer p2pProFork.Close()
peerNoFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{1}, "", nil), p2pNoFork, nil, nil) peerNoFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{1}, "", nil), p2pNoFork, nil, nil, nil)
peerProFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{2}, "", nil), p2pProFork, nil, nil) peerProFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{2}, "", nil), p2pProFork, nil, nil, nil)
defer peerNoFork.Close() defer peerNoFork.Close()
defer peerProFork.Close() defer peerProFork.Close()
@ -199,8 +203,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
defer p2pNoFork.Close() defer p2pNoFork.Close()
defer p2pProFork.Close() defer p2pProFork.Close()
peerNoFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil, nil) peerNoFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil, nil, nil)
peerProFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, nil, nil) peerProFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, nil, nil, nil)
defer peerNoFork.Close() defer peerNoFork.Close()
defer peerProFork.Close() defer peerProFork.Close()
@ -249,8 +253,8 @@ func testRecvTransactions(t *testing.T, protocol uint) {
defer p2pSrc.Close() defer p2pSrc.Close()
defer p2pSink.Close() defer p2pSink.Close()
src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool, nil) src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool, handler.txpool, nil)
sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, handler.txpool, nil) sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, handler.txpool, handler.txpool, nil)
defer src.Close() defer src.Close()
defer sink.Close() defer sink.Close()
@ -305,8 +309,8 @@ func testSendTransactions(t *testing.T, protocol uint) {
defer p2pSrc.Close() defer p2pSrc.Close()
defer p2pSink.Close() defer p2pSink.Close()
src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool, nil) src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool, handler.blobpool, nil)
sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, handler.txpool, nil) sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, handler.txpool, handler.blobpool, nil)
defer src.Close() defer src.Close()
defer sink.Close() defer sink.Close()
@ -380,8 +384,8 @@ func testTransactionPropagation(t *testing.T, protocol uint) {
defer sourcePipe.Close() defer sourcePipe.Close()
defer sinkPipe.Close() defer sinkPipe.Close()
sourcePeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{byte(i + 1)}, "", nil, sourcePipe), sourcePipe, source.txpool, nil) sourcePeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{byte(i + 1)}, "", nil, sourcePipe), sourcePipe, source.txpool, source.txpool, nil)
sinkPeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{0}, "", nil, sinkPipe), sinkPipe, sink.txpool, nil) sinkPeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{0}, "", nil, sinkPipe), sinkPipe, sink.txpool, sink.txpool, nil)
defer sourcePeer.Close() defer sourcePeer.Close()
defer sinkPeer.Close() defer sinkPeer.Close()

View file

@ -29,8 +29,10 @@ import (
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/ethconfig"
"github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/eth"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
@ -54,7 +56,10 @@ var (
// Its goal is to get around setting up a valid statedb for the balance and nonce // Its goal is to get around setting up a valid statedb for the balance and nonce
// checks. // checks.
type testTxPool struct { type testTxPool struct {
pool map[common.Hash]*types.Transaction // Hash map of collected transactions txPool map[common.Hash]*types.Transaction // Hash map of collected transactions
cellPool map[common.Hash][]kzg4844.Cell
custody map[common.Hash]types.CustodyBitmap
txFeed event.Feed // Notification feed to allow waiting for inclusion txFeed event.Feed // Notification feed to allow waiting for inclusion
lock sync.RWMutex // Protects the transaction pool lock sync.RWMutex // Protects the transaction pool
@ -63,7 +68,9 @@ type testTxPool struct {
// newTestTxPool creates a mock transaction pool. // newTestTxPool creates a mock transaction pool.
func newTestTxPool() *testTxPool { func newTestTxPool() *testTxPool {
return &testTxPool{ return &testTxPool{
pool: make(map[common.Hash]*types.Transaction), txPool: make(map[common.Hash]*types.Transaction),
cellPool: make(map[common.Hash][]kzg4844.Cell),
custody: make(map[common.Hash]types.CustodyBitmap),
} }
} }
@ -73,7 +80,16 @@ func (p *testTxPool) Has(hash common.Hash) bool {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
return p.pool[hash] != nil return p.txPool[hash] != nil
}
// Has returns an indicator whether txpool has a transaction
// cached with the given hash.
func (p *testTxPool) HasPayload(hash common.Hash) bool {
p.lock.Lock()
defer p.lock.Unlock()
return p.cellPool[hash] != nil
} }
// Get retrieves the transaction from local txpool with given // Get retrieves the transaction from local txpool with given
@ -81,16 +97,16 @@ func (p *testTxPool) Has(hash common.Hash) bool {
func (p *testTxPool) Get(hash common.Hash) *types.Transaction { func (p *testTxPool) Get(hash common.Hash) *types.Transaction {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
return p.pool[hash] return p.txPool[hash]
} }
// Get retrieves the transaction from local txpool with given // Get retrieves the transaction from local txpool with given
// tx hash. // tx hash.
func (p *testTxPool) GetRLP(hash common.Hash) []byte { func (p *testTxPool) GetRLP(hash common.Hash, _ uint) []byte {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
tx := p.pool[hash] tx := p.txPool[hash]
if tx != nil { if tx != nil {
blob, _ := rlp.EncodeToBytes(tx) blob, _ := rlp.EncodeToBytes(tx)
return blob return blob
@ -104,7 +120,7 @@ func (p *testTxPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
p.lock.Lock() p.lock.Lock()
defer p.lock.Unlock() defer p.lock.Unlock()
tx := p.pool[hash] tx := p.txPool[hash]
if tx != nil { if tx != nil {
return &txpool.TxMetadata{ return &txpool.TxMetadata{
Type: tx.Type(), Type: tx.Type(),
@ -121,7 +137,7 @@ func (p *testTxPool) Add(txs []*types.Transaction, sync bool) []error {
defer p.lock.Unlock() defer p.lock.Unlock()
for _, tx := range txs { for _, tx := range txs {
p.pool[tx.Hash()] = tx p.txPool[tx.Hash()] = tx
} }
p.txFeed.Send(core.NewTxsEvent{Txs: txs}) p.txFeed.Send(core.NewTxsEvent{Txs: txs})
return make([]error, len(txs)) return make([]error, len(txs))
@ -134,7 +150,7 @@ func (p *testTxPool) Pending(filter txpool.PendingFilter) (map[common.Address][]
var count int var count int
batches := make(map[common.Address][]*types.Transaction) batches := make(map[common.Address][]*types.Transaction)
for _, tx := range p.pool { for _, tx := range p.txPool {
from, _ := types.Sender(types.HomesteadSigner{}, tx) from, _ := types.Sender(types.HomesteadSigner{}, tx)
batches[from] = append(batches[from], tx) batches[from] = append(batches[from], tx)
} }
@ -164,6 +180,87 @@ func (p *testTxPool) Pending(filter txpool.PendingFilter) (map[common.Address][]
func (p *testTxPool) SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool) event.Subscription { func (p *testTxPool) SubscribeTransactions(ch chan<- core.NewTxsEvent, reorgs bool) event.Subscription {
return p.txFeed.Subscribe(ch) return p.txFeed.Subscribe(ch)
} }
func (p *testTxPool) GetBlobHashes(hash common.Hash) []common.Hash {
p.lock.RLock()
defer p.lock.RUnlock()
tx, exists := p.txPool[hash]
if !exists {
return nil
}
return tx.BlobHashes()
}
func (p *testTxPool) GetBlobCells(vhashes []common.Hash, mask types.CustodyBitmap) ([][]*kzg4844.Cell, [][]*kzg4844.Proof, error) {
p.lock.RLock()
defer p.lock.RUnlock()
requestedIndices := mask.Indices()
cells := make([][]*kzg4844.Cell, len(vhashes))
proofs := make([][]*kzg4844.Proof, len(vhashes))
for i, vhash := range vhashes {
// Find the tx containing this versioned hash
var foundTx *types.Transaction
var blobIdx int
for _, tx := range p.txPool {
for j, bh := range tx.BlobHashes() {
if bh == vhash {
foundTx = tx
blobIdx = j
break
}
}
if foundTx != nil {
break
}
}
if foundTx == nil {
continue
}
txCells, ok := p.cellPool[foundTx.Hash()]
if !ok {
continue
}
_ = blobIdx // cells in the mock are stored flat by cell index
blobCells := make([]*kzg4844.Cell, len(requestedIndices))
for j, idx := range requestedIndices {
if int(idx) < len(txCells) {
cell := txCells[idx]
blobCells[j] = &cell
}
}
cells[i] = blobCells
}
return cells, proofs, nil
}
func (p *testTxPool) GetCustody(hash common.Hash) *types.CustodyBitmap {
p.lock.RLock()
defer p.lock.RUnlock()
mask, ok := p.custody[hash]
if !ok {
return nil
}
return &mask
}
// AddCells adds cells for a specific transaction hash (for testing)
func (p *testTxPool) AddCells(hash common.Hash, cells []kzg4844.Cell, mask types.CustodyBitmap) {
p.lock.Lock()
defer p.lock.Unlock()
p.cellPool[hash] = cells
p.custody[hash] = mask
}
func (p *testTxPool) AddPooledTx(pooledTx *blobpool.BlobTxForPool) error {
p.lock.Lock()
defer p.lock.Unlock()
hash := pooledTx.Tx.Hash()
p.cellPool[hash] = pooledTx.CellSidecar.Cells
p.txPool[hash] = pooledTx.Tx
return nil
}
// FilterType should check whether the pool supports the given type of transactions. // FilterType should check whether the pool supports the given type of transactions.
func (p *testTxPool) FilterType(kind byte) bool { func (p *testTxPool) FilterType(kind byte) bool {
@ -174,14 +271,19 @@ func (p *testTxPool) FilterType(kind byte) bool {
return false return false
} }
func (p *testTxPool) ValidateTxBasics(_ *types.Transaction) error {
return nil
}
// testHandler is a live implementation of the Ethereum protocol handler, just // testHandler is a live implementation of the Ethereum protocol handler, just
// preinitialized with some sane testing defaults and the transaction pool mocked // preinitialized with some sane testing defaults and the transaction pool mocked
// out. // out.
type testHandler struct { type testHandler struct {
db ethdb.Database db ethdb.Database
chain *core.BlockChain chain *core.BlockChain
txpool *testTxPool txpool *testTxPool
handler *handler blobpool *testTxPool
handler *handler
} }
// newTestHandler creates a new handler for testing purposes with no blocks. // newTestHandler creates a new handler for testing purposes with no blocks.
@ -210,6 +312,7 @@ func newTestHandlerWithBlocks(blocks int, mode ethconfig.SyncMode) *testHandler
Database: db, Database: db,
Chain: chain, Chain: chain,
TxPool: txpool, TxPool: txpool,
BlobPool: txpool,
Network: 1, Network: 1,
Sync: mode, Sync: mode,
BloomCache: 1, BloomCache: 1,
@ -217,10 +320,11 @@ func newTestHandlerWithBlocks(blocks int, mode ethconfig.SyncMode) *testHandler
handler.Start(1000) handler.Start(1000)
return &testHandler{ return &testHandler{
db: db, db: db,
chain: chain, chain: chain,
txpool: txpool, txpool: txpool,
handler: handler, blobpool: txpool,
handler: handler,
} }
} }
@ -317,7 +421,7 @@ func createTestPeers(rand *rand.Rand, n int) []*ethPeer {
var id enode.ID var id enode.ID
rand.Read(id[:]) rand.Read(id[:])
p2pPeer := p2p.NewPeer(id, "test", nil) p2pPeer := p2p.NewPeer(id, "test", nil)
ep := eth.NewPeer(eth.ETH69, p2pPeer, nil, nil, nil) ep := eth.NewPeer(eth.ETH69, p2pPeer, nil, nil, nil, nil)
peers[i] = &ethPeer{Peer: ep} peers[i] = &ethPeer{Peer: ep}
} }
return peers return peers

View file

@ -113,29 +113,59 @@ func (p *Peer) announceTransactions() {
pending []common.Hash pending []common.Hash
pendingTypes []byte pendingTypes []byte
pendingSizes []uint32 pendingSizes []uint32
mask types.CustodyBitmap
size common.StorageSize size common.StorageSize
processed = make(map[int]bool)
) )
for count = 0; count < len(queue) && size < maxTxPacketSize; count++ { for count = 0; count < len(queue) && size < maxTxPacketSize; count++ {
if meta := p.txpool.GetMetadata(queue[count]); meta != nil { if meta := p.txpool.GetMetadata(queue[count]); meta != nil {
custody := p.blobpool.GetCustody(queue[count])
if custody != nil {
// Blob txs should be batched into the same announcement
// if they share the same custody.
if mask.OneCount() == 0 {
// This is the first blob tx in this batch, so use its
// custody as a mask in this batch.
mask = *custody
} else {
if mask != *custody {
// Leave this in the queue so that it can be included
// in a later announcement.
continue
}
}
}
pending = append(pending, queue[count]) pending = append(pending, queue[count])
pendingTypes = append(pendingTypes, meta.Type) pendingTypes = append(pendingTypes, meta.Type)
pendingSizes = append(pendingSizes, uint32(meta.Size)) if p.version >= ETH72 && meta.SizeWithoutBlob > 0 {
pendingSizes = append(pendingSizes, uint32(meta.SizeWithoutBlob))
} else {
pendingSizes = append(pendingSizes, uint32(meta.Size))
}
size += common.HashLength size += common.HashLength
processed[count] = true
} }
} }
// Shift and trim queue // Shift and trim queue using processed map
queue = queue[:copy(queue, queue[count:])] var remaining []common.Hash
for i, h := range queue {
if !processed[i] {
remaining = append(remaining, h)
}
}
queue = remaining
// If there's anything available to transfer, fire up an async writer // If there's anything available to transfer, fire up an async writer
if len(pending) > 0 { if len(pending) > 0 {
done = make(chan struct{}) done = make(chan struct{})
go func() { go func() {
if err := p.sendPooledTransactionHashes(pending, pendingTypes, pendingSizes); err != nil { if err := p.sendPooledTransactionHashes(pending, pendingTypes, pendingSizes, mask); err != nil {
fail <- err fail <- err
return return
} }
close(done) close(done)
p.Log().Trace("Sent transaction announcements", "count", len(pending)) p.Log().Trace("Sent transaction announcements", "count", len(pending), "mask", mask, "tx", pending)
}() }()
} }
} }

View file

@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/txpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/p2p/enode"
@ -71,6 +72,9 @@ type Backend interface {
// TxPool retrieves the transaction pool object to serve data. // TxPool retrieves the transaction pool object to serve data.
TxPool() TxPool TxPool() TxPool
// BlobPool retrieves the blob pool object to serve cell requests.
BlobPool() BlobPool
// AcceptTxs retrieves whether transaction processing is enabled on the node // AcceptTxs retrieves whether transaction processing is enabled on the node
// or if inbound transactions should simply be dropped. // or if inbound transactions should simply be dropped.
AcceptTxs() bool AcceptTxs() bool
@ -90,6 +94,18 @@ type Backend interface {
Handle(peer *Peer, packet Packet) error Handle(peer *Peer, packet Packet) error
} }
// BlobPool defines the methods needed by the protocol handler to serve cell requests.
type BlobPool interface {
// GetBlobHashes returns the blob versioned hashes for a given transaction hash.
GetBlobHashes(hash common.Hash) []common.Hash
// GetBlobCells retrieves cells and proofs for given versioned blob hashes filtered by the custody bitmap.
GetBlobCells(vhashes []common.Hash, mask types.CustodyBitmap) ([][]*kzg4844.Cell, [][]*kzg4844.Proof, error)
// GetCustody returns the custody bitmap for a given transaction hash.
GetCustody(hash common.Hash) *types.CustodyBitmap
// Has returns whether the blob pool contains a transaction with the given hash.
Has(hash common.Hash) bool
}
// TxPool defines the methods needed by the protocol handler to serve transactions. // TxPool defines the methods needed by the protocol handler to serve transactions.
type TxPool interface { type TxPool interface {
// Get retrieves the transaction from the local txpool with the given hash. // Get retrieves the transaction from the local txpool with the given hash.
@ -97,7 +113,7 @@ type TxPool interface {
// GetRLP retrieves the RLP-encoded transaction from the local txpool with // GetRLP retrieves the RLP-encoded transaction from the local txpool with
// the given hash. // the given hash.
GetRLP(hash common.Hash) []byte GetRLP(hash common.Hash, version uint) []byte
// GetMetadata returns the transaction type and transaction size with the // GetMetadata returns the transaction type and transaction size with the
// given transaction hash. // given transaction hash.
@ -113,7 +129,7 @@ func MakeProtocols(backend Backend, network uint64, disc enode.Iterator) []p2p.P
Version: version, Version: version,
Length: protocolLengths[version], Length: protocolLengths[version],
Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error { Run: func(p *p2p.Peer, rw p2p.MsgReadWriter) error {
peer := NewPeer(version, p, rw, backend.TxPool(), backend.Chain().Config()) peer := NewPeer(version, p, rw, backend.TxPool(), backend.BlobPool(), backend.Chain().Config())
defer peer.Close() defer peer.Close()
return backend.RunPeer(peer, func(peer *Peer) error { return backend.RunPeer(peer, func(peer *Peer) error {
@ -216,6 +232,24 @@ var eth71 = map[uint64]msgHandler{
BlockAccessListsMsg: handleBlockAccessLists, BlockAccessListsMsg: handleBlockAccessLists,
} }
var eth72 = map[uint64]msgHandler{
TransactionsMsg: handleTransactions,
NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes71,
GetBlockHeadersMsg: handleGetBlockHeaders,
BlockHeadersMsg: handleBlockHeaders,
GetBlockBodiesMsg: handleGetBlockBodies,
BlockBodiesMsg: handleBlockBodies,
GetReceiptsMsg: handleGetReceipts70,
ReceiptsMsg: handleReceipts70,
GetPooledTransactionsMsg: handleGetPooledTransactions,
PooledTransactionsMsg: handlePooledTransactions,
BlockRangeUpdateMsg: handleBlockRangeUpdate,
GetBlockAccessListsMsg: handleGetBlockAccessLists,
BlockAccessListsMsg: handleBlockAccessLists,
GetCellsMsg: handleGetCells,
CellsMsg: handleCells,
}
// handleMessage is invoked whenever an inbound message is received from a remote // handleMessage is invoked whenever an inbound message is received from a remote
// peer. The remote connection is torn down upon returning any error. // peer. The remote connection is torn down upon returning any error.
func handleMessage(backend Backend, peer *Peer) error { func handleMessage(backend Backend, peer *Peer) error {
@ -237,6 +271,8 @@ func handleMessage(backend Backend, peer *Peer) error {
handlers = eth70 handlers = eth70
case ETH71: case ETH71:
handlers = eth71 handlers = eth71
case ETH72:
handlers = eth72
default: default:
return fmt.Errorf("unknown eth protocol version: %v", peer.version) return fmt.Errorf("unknown eth protocol version: %v", peer.version)
} }

View file

@ -63,9 +63,10 @@ func u64(val uint64) *uint64 { return &val }
// purpose is to allow testing the request/reply workflows and wire serialization // purpose is to allow testing the request/reply workflows and wire serialization
// in the `eth` protocol without actually doing any data processing. // in the `eth` protocol without actually doing any data processing.
type testBackend struct { type testBackend struct {
db ethdb.Database db ethdb.Database
chain *core.BlockChain chain *core.BlockChain
txpool *txpool.TxPool txpool *txpool.TxPool
blobpool *blobpool.BlobPool
} }
// newTestBackend creates an empty chain and wraps it into a mock backend. // newTestBackend creates an empty chain and wraps it into a mock backend.
@ -143,9 +144,10 @@ func newTestBackendWithGenerator(blocks int, shanghai bool, cancun bool, generat
txpool, _ := txpool.New(txconfig.PriceLimit, chain, []txpool.SubPool{legacyPool, blobPool}) txpool, _ := txpool.New(txconfig.PriceLimit, chain, []txpool.SubPool{legacyPool, blobPool})
return &testBackend{ return &testBackend{
db: db, db: db,
chain: chain, chain: chain,
txpool: txpool, txpool: txpool,
blobpool: blobPool,
} }
} }
@ -157,6 +159,7 @@ func (b *testBackend) close() {
func (b *testBackend) Chain() *core.BlockChain { return b.chain } func (b *testBackend) Chain() *core.BlockChain { return b.chain }
func (b *testBackend) TxPool() TxPool { return b.txpool } func (b *testBackend) TxPool() TxPool { return b.txpool }
func (b *testBackend) BlobPool() BlobPool { return b.blobpool }
func (b *testBackend) RunPeer(peer *Peer, handler Handler) error { func (b *testBackend) RunPeer(peer *Peer, handler Handler) error {
// Normally the backend would do peer maintenance and handshakes. All that // Normally the backend would do peer maintenance and handshakes. All that

View file

@ -26,6 +26,7 @@ import (
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/tracker" "github.com/ethereum/go-ethereum/p2p/tracker"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
@ -568,7 +569,27 @@ func handleNewPooledTransactionHashes(backend Backend, msg Decoder, peer *Peer)
if !backend.AcceptTxs() { if !backend.AcceptTxs() {
return nil return nil
} }
ann := new(NewPooledTransactionHashesPacket) ann := new(NewPooledTransactionHashesPacket71)
if err := msg.Decode(ann); err != nil {
return err
}
if len(ann.Hashes) != len(ann.Types) || len(ann.Hashes) != len(ann.Sizes) {
return fmt.Errorf("NewPooledTransactionHashes: invalid len of fields in %v %v %v", len(ann.Hashes), len(ann.Types), len(ann.Sizes))
}
// Schedule all the unknown hashes for retrieval
for _, hash := range ann.Hashes {
peer.MarkTransaction(hash)
}
return backend.Handle(peer, ann)
}
func handleNewPooledTransactionHashes71(backend Backend, msg Decoder, peer *Peer) error {
// New transaction announcement arrived, make sure we have
// a valid and fresh chain to handle them
if !backend.AcceptTxs() {
return nil
}
ann := new(NewPooledTransactionHashesPacket72)
if err := msg.Decode(ann); err != nil { if err := msg.Decode(ann); err != nil {
return err return err
} }
@ -588,11 +609,11 @@ func handleGetPooledTransactions(backend Backend, msg Decoder, peer *Peer) error
if err := msg.Decode(&query); err != nil { if err := msg.Decode(&query); err != nil {
return err return err
} }
hashes, txs := answerGetPooledTransactions(backend, query.GetPooledTransactionsRequest) hashes, txs := answerGetPooledTransactions(backend, query.GetPooledTransactionsRequest, peer.version)
return peer.ReplyPooledTransactionsRLP(query.RequestId, hashes, txs) return peer.ReplyPooledTransactionsRLP(query.RequestId, hashes, txs)
} }
func answerGetPooledTransactions(backend Backend, query GetPooledTransactionsRequest) ([]common.Hash, []rlp.RawValue) { func answerGetPooledTransactions(backend Backend, query GetPooledTransactionsRequest, version uint) ([]common.Hash, []rlp.RawValue) {
// Gather transactions until the fetch or network limits is reached // Gather transactions until the fetch or network limits is reached
var ( var (
bytes int bytes int
@ -604,7 +625,7 @@ func answerGetPooledTransactions(backend Backend, query GetPooledTransactionsReq
break break
} }
// Retrieve the requested transaction, skipping if unknown to us // Retrieve the requested transaction, skipping if unknown to us
encoded := backend.TxPool().GetRLP(hash) encoded := backend.TxPool().GetRLP(hash, version)
if len(encoded) == 0 { if len(encoded) == 0 {
continue continue
} }
@ -667,6 +688,80 @@ func handleBlockRangeUpdate(backend Backend, msg Decoder, peer *Peer) error {
return nil return nil
} }
func handleGetCells(backend Backend, msg Decoder, peer *Peer) error {
// Decode the cell retrieval message
var query GetCellsRequestPacket
if err := msg.Decode(&query); err != nil {
return err
}
hashes, cells, custody := answerGetCells(backend, query.GetCellsRequest)
return peer.ReplyCells(query.RequestId, hashes, cells, custody)
}
func answerGetCells(backend Backend, query GetCellsRequest) ([]common.Hash, [][]kzg4844.Cell, types.CustodyBitmap) {
var (
cellCounts int
hashes []common.Hash
cells [][]kzg4844.Cell
)
maxCells := softResponseLimit / 2048
for _, hash := range query.Hashes {
if cellCounts >= maxCells {
break
}
// Look up the blob versioned hashes for this transaction
vhashes := backend.BlobPool().GetBlobHashes(hash)
if len(vhashes) == 0 {
continue
}
blobCells, _, _ := backend.BlobPool().GetBlobCells(vhashes, query.Mask)
// Flatten per-blob cells into a single slice. If any blob has a nil
// entry (unavailable cell), skip the entire transaction.
var flat []kzg4844.Cell
skip := false
for _, bc := range blobCells {
if bc == nil {
skip = true
break
}
for _, c := range bc {
if c == nil {
skip = true
break
}
flat = append(flat, *c)
}
if skip {
break
}
}
if skip || len(flat) == 0 {
continue
}
hashes = append(hashes, hash)
cells = append(cells, flat)
cellCounts += len(flat)
}
return hashes, cells, query.Mask
}
func handleCells(backend Backend, msg Decoder, peer *Peer) error {
var cellsResponse CellsPacket
if err := msg.Decode(&cellsResponse); err != nil {
return err
}
tresp := tracker.Response{
ID: cellsResponse.RequestId,
MsgCode: CellsMsg,
Size: len(cellsResponse.CellsResponse.Hashes),
}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("Cells: %w", err)
}
return backend.Handle(peer, &cellsResponse.CellsResponse)
}
// handleGetBlockAccessLists serves a GetBlockAccessLists request. // handleGetBlockAccessLists serves a GetBlockAccessLists request.
func handleGetBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error { func handleGetBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error {
var query GetBlockAccessListsPacket var query GetBlockAccessListsPacket

View file

@ -77,7 +77,7 @@ func testHandshake(t *testing.T, protocol uint) {
defer app.Close() defer app.Close()
defer net.Close() defer net.Close()
peer := NewPeer(protocol, p2p.NewPeer(enode.ID{}, "peer", nil), net, nil, nil) peer := NewPeer(protocol, p2p.NewPeer(enode.ID{}, "peer", nil), net, nil, nil, nil)
defer peer.Close() defer peer.Close()
// Send the junk test with one peer, check the handshake failure // Send the junk test with one peer, check the handshake failure

View file

@ -27,6 +27,7 @@ import (
mapset "github.com/deckarep/golang-set/v2" mapset "github.com/deckarep/golang-set/v2"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/p2p/tracker" "github.com/ethereum/go-ethereum/p2p/tracker"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
@ -66,7 +67,8 @@ type Peer struct {
version uint // Protocol version negotiated version uint // Protocol version negotiated
lastRange atomic.Pointer[BlockRangeUpdatePacket] lastRange atomic.Pointer[BlockRangeUpdatePacket]
txpool TxPool // Transaction pool used by the broadcasters for liveness checks txpool TxPool // Transaction pool used by the broadcasters for liveness checks
blobpool BlobPool
knownTxs *knownCache // Set of transaction hashes known to be known by this peer knownTxs *knownCache // Set of transaction hashes known to be known by this peer
txBroadcast chan []common.Hash // Channel used to queue transaction propagation requests txBroadcast chan []common.Hash // Channel used to queue transaction propagation requests
txAnnounce chan []common.Hash // Channel used to queue transaction announcement requests txAnnounce chan []common.Hash // Channel used to queue transaction announcement requests
@ -86,11 +88,11 @@ type Peer struct {
// NewPeer creates a wrapper for a network connection and negotiated protocol // NewPeer creates a wrapper for a network connection and negotiated protocol
// version. // version.
func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool, chainConfig *params.ChainConfig) *Peer { func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool, blobpool BlobPool, chainConfig *params.ChainConfig) *Peer {
cap := p2p.Cap{Name: ProtocolName, Version: version} cap := p2p.Cap{Name: ProtocolName, Version: version}
id := p.ID().String() id := p.ID().String()
peer := &Peer{ peer := &Peer{
id: p.ID().String(), id: id,
Peer: p, Peer: p,
rw: rw, rw: rw,
version: version, version: version,
@ -102,6 +104,7 @@ func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool, cha
reqCancel: make(chan *cancel), reqCancel: make(chan *cancel),
resDispatch: make(chan *response), resDispatch: make(chan *response),
txpool: txpool, txpool: txpool,
blobpool: blobpool,
chainConfig: chainConfig, chainConfig: chainConfig,
receiptBuffer: make(map[uint64]*receiptRequest), receiptBuffer: make(map[uint64]*receiptRequest),
term: make(chan struct{}), term: make(chan struct{}),
@ -188,13 +191,13 @@ func (p *Peer) AsyncSendTransactions(hashes []common.Hash) {
// This method is a helper used by the async transaction announcer. Don't call it // This method is a helper used by the async transaction announcer. Don't call it
// directly as the queueing (memory) and transmission (bandwidth) costs should // directly as the queueing (memory) and transmission (bandwidth) costs should
// not be managed directly. // not be managed directly.
func (p *Peer) sendPooledTransactionHashes(hashes []common.Hash, types []byte, sizes []uint32) error { func (p *Peer) sendPooledTransactionHashes(hashes []common.Hash, types []byte, sizes []uint32, cells types.CustodyBitmap) error {
if err := p2p.Send(p.rw, NewPooledTransactionHashesMsg, NewPooledTransactionHashesPacket{Types: types, Sizes: sizes, Hashes: hashes}); err != nil {
return err
}
// Mark all the transactions as known, but ensure we don't overflow our limits // Mark all the transactions as known, but ensure we don't overflow our limits
p.knownTxs.Add(hashes...) p.knownTxs.Add(hashes...)
return nil if p.version >= ETH72 {
return p2p.Send(p.rw, NewPooledTransactionHashesMsg, NewPooledTransactionHashesPacket72{Types: types, Sizes: sizes, Hashes: hashes, Mask: cells})
}
return p2p.Send(p.rw, NewPooledTransactionHashesMsg, NewPooledTransactionHashesPacket71{Types: types, Sizes: sizes, Hashes: hashes})
} }
// AsyncSendPooledTransactionHashes queues a list of transactions hashes to eventually // AsyncSendPooledTransactionHashes queues a list of transactions hashes to eventually
@ -248,6 +251,41 @@ func (p *Peer) ReplyReceiptsRLP69(id uint64, receipts rlp.RawList[*ReceiptList])
}) })
} }
// ReplyCells is the response to GetCells.
func (p *Peer) ReplyCells(id uint64, hashes []common.Hash, cells [][]kzg4844.Cell, mask types.CustodyBitmap) error {
return p2p.Send(p.rw, CellsMsg, &CellsPacket{
RequestId: id,
CellsResponse: CellsResponse{
Hashes: hashes,
Cells: cells,
Mask: mask,
},
})
}
// RequestPayload fetches a batch of cells from a remote node.
func (p *Peer) RequestPayload(hashes []common.Hash, cell types.CustodyBitmap) error {
p.Log().Debug("Fetching batch of cells", "txcount", len(hashes), "cellcount", cell.OneCount())
id := rand.Uint64()
err := p.tracker.Track(tracker.Request{
ID: id,
ReqCode: GetCellsMsg,
RespCode: CellsMsg,
Size: len(hashes),
})
if err != nil {
return err
}
return p2p.Send(p.rw, GetCellsMsg, &GetCellsRequestPacket{
RequestId: id,
GetCellsRequest: GetCellsRequest{
Hashes: hashes,
Mask: cell,
},
})
}
// ReplyReceiptsRLP70 is the response to GetReceipts. // ReplyReceiptsRLP70 is the response to GetReceipts.
func (p *Peer) ReplyReceiptsRLP70(id uint64, receipts rlp.RawList[*ReceiptList], lastBlockIncomplete bool) error { func (p *Peer) ReplyReceiptsRLP70(id uint64, receipts rlp.RawList[*ReceiptList], lastBlockIncomplete bool) error {
return p2p.Send(p.rw, ReceiptsMsg, &ReceiptsPacket70{ return p2p.Send(p.rw, ReceiptsMsg, &ReceiptsPacket70{

View file

@ -45,7 +45,7 @@ func newTestPeer(name string, version uint, backend Backend) (*testPeer, <-chan
var id enode.ID var id enode.ID
rand.Read(id[:]) rand.Read(id[:])
peer := NewPeer(version, p2p.NewPeer(id, name, nil), net, backend.TxPool(), nil) peer := NewPeer(version, p2p.NewPeer(id, name, nil), net, backend.TxPool(), backend.BlobPool(), nil)
errc := make(chan error, 1) errc := make(chan error, 1)
go func() { go func() {
defer app.Close() defer app.Close()

View file

@ -25,6 +25,7 @@ import (
"github.com/ethereum/go-ethereum/core/forkid" "github.com/ethereum/go-ethereum/core/forkid"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal" "github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
@ -33,6 +34,7 @@ const (
ETH69 = 69 ETH69 = 69
ETH70 = 70 ETH70 = 70
ETH71 = 71 ETH71 = 71
ETH72 = 72
) )
// ProtocolName is the official short name of the `eth` protocol used during // ProtocolName is the official short name of the `eth` protocol used during
@ -41,11 +43,11 @@ const ProtocolName = "eth"
// ProtocolVersions are the supported versions of the `eth` protocol (first // ProtocolVersions are the supported versions of the `eth` protocol (first
// is primary). // is primary).
var ProtocolVersions = []uint{ETH71, ETH70, ETH69} var ProtocolVersions = []uint{ETH72, ETH71, ETH70, ETH69}
// protocolLengths are the number of implemented message corresponding to // protocolLengths are the number of implemented message corresponding to
// different protocol versions. // different protocol versions.
var protocolLengths = map[uint]uint64{ETH71: 20, ETH69: 18, ETH70: 18} var protocolLengths = map[uint]uint64{ETH69: 18, ETH70: 18, ETH71: 20, ETH72: 22}
// maxMessageSize is the maximum cap on the size of a protocol message. // maxMessageSize is the maximum cap on the size of a protocol message.
const maxMessageSize = 10 * 1024 * 1024 const maxMessageSize = 10 * 1024 * 1024
@ -70,6 +72,8 @@ const (
BlockRangeUpdateMsg = 0x11 BlockRangeUpdateMsg = 0x11
GetBlockAccessListsMsg = 0x12 GetBlockAccessListsMsg = 0x12
BlockAccessListsMsg = 0x13 BlockAccessListsMsg = 0x13
GetCellsMsg = 0x14
CellsMsg = 0x15
) )
var ( var (
@ -249,13 +253,22 @@ type ReceiptsPacket70 struct {
// ReceiptsRLPResponse is used for receipts, when we already have it encoded // ReceiptsRLPResponse is used for receipts, when we already have it encoded
type ReceiptsRLPResponse []rlp.RawValue type ReceiptsRLPResponse []rlp.RawValue
// NewPooledTransactionHashesPacket represents a transaction announcement packet on eth/68 and newer. // NewPooledTransactionHashesPacket71 represents a transaction announcement packet on eth/69.
type NewPooledTransactionHashesPacket struct { type NewPooledTransactionHashesPacket71 struct {
Types []byte Types []byte
Sizes []uint32 Sizes []uint32
Hashes []common.Hash Hashes []common.Hash
} }
// NewPooledTransactionHashesPacket72 represents a transaction announcement packet on ETH/72
// with an additional custody bitmap field for cell-based blob data availability.
type NewPooledTransactionHashesPacket72 struct {
Types []byte
Sizes []uint32
Hashes []common.Hash
Mask types.CustodyBitmap
}
// GetPooledTransactionsRequest represents a transaction query. // GetPooledTransactionsRequest represents a transaction query.
type GetPooledTransactionsRequest []common.Hash type GetPooledTransactionsRequest []common.Hash
@ -292,6 +305,31 @@ type BlockRangeUpdatePacket struct {
LatestBlockHash common.Hash LatestBlockHash common.Hash
} }
// GetCellsRequest represents a request for cells of blob transactions.
type GetCellsRequest struct {
Hashes []common.Hash
Mask types.CustodyBitmap
}
// GetCellsRequestPacket represents a cell request with request ID wrapping.
type GetCellsRequestPacket struct {
RequestId uint64
GetCellsRequest
}
// CellsResponse represents a response containing cells for blob transactions.
type CellsResponse struct {
Hashes []common.Hash
Cells [][]kzg4844.Cell
Mask types.CustodyBitmap
}
// CellsPacket represents a cells response with request ID wrapping.
type CellsPacket struct {
RequestId uint64
CellsResponse
}
type GetBlockAccessListsRequest []common.Hash type GetBlockAccessListsRequest []common.Hash
type GetBlockAccessListsPacket struct { type GetBlockAccessListsPacket struct {
@ -328,8 +366,11 @@ func (*GetBlockBodiesRequest) Kind() byte { return GetBlockBodiesMsg }
func (*BlockBodiesResponse) Name() string { return "BlockBodies" } func (*BlockBodiesResponse) Name() string { return "BlockBodies" }
func (*BlockBodiesResponse) Kind() byte { return BlockBodiesMsg } func (*BlockBodiesResponse) Kind() byte { return BlockBodiesMsg }
func (*NewPooledTransactionHashesPacket) Name() string { return "NewPooledTransactionHashes" } func (*NewPooledTransactionHashesPacket71) Name() string { return "NewPooledTransactionHashes" }
func (*NewPooledTransactionHashesPacket) Kind() byte { return NewPooledTransactionHashesMsg } func (*NewPooledTransactionHashesPacket71) Kind() byte { return NewPooledTransactionHashesMsg }
func (*NewPooledTransactionHashesPacket72) Name() string { return "NewPooledTransactionHashes" }
func (*NewPooledTransactionHashesPacket72) Kind() byte { return NewPooledTransactionHashesMsg }
func (*GetPooledTransactionsRequest) Name() string { return "GetPooledTransactions" } func (*GetPooledTransactionsRequest) Name() string { return "GetPooledTransactions" }
func (*GetPooledTransactionsRequest) Kind() byte { return GetPooledTransactionsMsg } func (*GetPooledTransactionsRequest) Kind() byte { return GetPooledTransactionsMsg }
@ -354,3 +395,9 @@ func (*GetBlockAccessListsRequest) Kind() byte { return GetBlockAccessListsMsg
func (*BlockAccessListResponse) Name() string { return "BlockAccessLists" } func (*BlockAccessListResponse) Name() string { return "BlockAccessLists" }
func (*BlockAccessListResponse) Kind() byte { return BlockAccessListsMsg } func (*BlockAccessListResponse) Kind() byte { return BlockAccessListsMsg }
func (*GetCellsRequest) Name() string { return "GetCells" }
func (*GetCellsRequest) Kind() byte { return GetCellsMsg }
func (*CellsResponse) Name() string { return "Cells" }
func (*CellsResponse) Kind() byte { return CellsMsg }

View file

@ -50,8 +50,8 @@ func testSnapSyncDisabling(t *testing.T, ethVer uint, snapVer uint) {
defer emptyPipeEth.Close() defer emptyPipeEth.Close()
defer fullPipeEth.Close() defer fullPipeEth.Close()
emptyPeerEth := eth.NewPeer(ethVer, p2p.NewPeer(enode.ID{1}, "", caps), emptyPipeEth, empty.txpool, nil) emptyPeerEth := eth.NewPeer(ethVer, p2p.NewPeer(enode.ID{1}, "", caps), emptyPipeEth, empty.txpool, empty.blobpool, nil)
fullPeerEth := eth.NewPeer(ethVer, p2p.NewPeer(enode.ID{2}, "", caps), fullPipeEth, full.txpool, nil) fullPeerEth := eth.NewPeer(ethVer, p2p.NewPeer(enode.ID{2}, "", caps), fullPipeEth, full.txpool, full.blobpool, nil)
defer emptyPeerEth.Close() defer emptyPeerEth.Close()
defer fullPeerEth.Close() defer fullPeerEth.Close()

View file

@ -25,22 +25,28 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/fetcher" "github.com/ethereum/go-ethereum/eth/fetcher"
"github.com/ethereum/go-ethereum/eth/protocols/eth"
) )
var ( var (
peers []string peers []string
txs []*types.Transaction peerVersions map[string]uint
txs []*types.Transaction
) )
func init() { func init() {
// Random is nice, but we need it deterministic // Random is nice, but we need it deterministic
rand := rand.New(rand.NewSource(0x3a29)) rand := rand.New(rand.NewSource(0x3a29))
supportedVersions := []uint{eth.ETH69, eth.ETH70, eth.ETH72}
peers = make([]string, 10) peers = make([]string, 10)
peerVersions = make(map[string]uint, len(peers))
for i := 0; i < len(peers); i++ { for i := 0; i < len(peers); i++ {
peers[i] = fmt.Sprintf("Peer #%d", i) peers[i] = fmt.Sprintf("Peer #%d", i)
peerVersions[peers[i]] = supportedVersions[i%len(supportedVersions)]
} }
txs = make([]*types.Transaction, 65536) // We need to bump enough to hit all the limits txs = make([]*types.Transaction, 65536) // We need to bump enough to hit all the limits
for i := 0; i < len(txs); i++ { for i := 0; i < len(txs); i++ {
@ -85,6 +91,12 @@ func fuzz(input []byte) int {
}, },
func(string, []common.Hash) error { return nil }, func(string, []common.Hash) error { return nil },
nil, nil,
blobpool.NewBlobBuffer(blobpool.BlobBufferFunctions{
ValidateTx: func(*types.Transaction) error { return nil },
AddToPool: func(*blobpool.BlobTxForPool) error { return nil },
DropPeer: func(string) {},
}),
clock, clock,
func() time.Time { func() time.Time {
nanoTime := int64(clock.Now()) nanoTime := int64(clock.Now())
@ -139,7 +151,7 @@ func fuzz(input []byte) int {
if verbose { if verbose {
fmt.Println("Notify", peer, announceIdxs) fmt.Println("Notify", peer, announceIdxs)
} }
if err := f.Notify(peer, types, sizes, announces); err != nil { if _, err := f.Notify(peer, types, sizes, announces); err != nil {
panic(err) panic(err)
} }
@ -180,7 +192,7 @@ func fuzz(input []byte) int {
if verbose { if verbose {
fmt.Println("Enqueue", peer, deliverIdxs, direct) fmt.Println("Enqueue", peer, deliverIdxs, direct)
} }
if err := f.Enqueue(peer, deliveries, direct); err != nil { if err := f.Enqueue(peer, peerVersions[peer], deliveries, direct); err != nil {
panic(err) panic(err)
} }