mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-27 00:46:18 +00:00
Implement EIP 8070
This commit is contained in:
parent
cda717581d
commit
49e199bbfb
57 changed files with 10023 additions and 4602 deletions
|
|
@ -174,6 +174,11 @@ type BlobAndProofV2 struct {
|
|||
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
|
||||
// that avoids the overhead of encoding/json for large blob payloads.
|
||||
type BlobAndProofListV2 []*BlobAndProofV2
|
||||
|
|
|
|||
|
|
@ -66,10 +66,11 @@ func (s *Suite) dialAs(key *ecdsa.PrivateKey) (*Conn, error) {
|
|||
return nil, err
|
||||
}
|
||||
conn.caps = []p2p.Cap{
|
||||
{Name: "eth", Version: 72},
|
||||
{Name: "eth", Version: 70},
|
||||
{Name: "eth", Version: 69},
|
||||
}
|
||||
conn.ourHighestProtoVersion = 70
|
||||
conn.ourHighestProtoVersion = 72
|
||||
return &conn, nil
|
||||
}
|
||||
|
||||
|
|
@ -106,6 +107,10 @@ type Conn struct {
|
|||
ourHighestProtoVersion uint
|
||||
ourHighestSnapProtoVersion uint
|
||||
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.
|
||||
|
|
@ -181,11 +186,15 @@ func (c *Conn) ReadEth() (any, error) {
|
|||
case eth.TransactionsMsg:
|
||||
msg = new(eth.TransactionsPacket)
|
||||
case eth.NewPooledTransactionHashesMsg:
|
||||
msg = new(eth.NewPooledTransactionHashesPacket)
|
||||
msg = new(eth.NewPooledTransactionHashesPacket72)
|
||||
case eth.GetPooledTransactionsMsg:
|
||||
msg = new(eth.GetPooledTransactionsPacket)
|
||||
case eth.PooledTransactionsMsg:
|
||||
msg = new(eth.PooledTransactionsPacket)
|
||||
case eth.GetCellsMsg:
|
||||
msg = new(eth.GetCellsRequestPacket)
|
||||
case eth.CellsMsg:
|
||||
msg = new(eth.CellsPacket)
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled eth msg code %d", code))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,31 +18,32 @@ package ethtest
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/beacon/engine"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/golang-jwt/jwt/v4"
|
||||
)
|
||||
|
||||
// EngineClient is a wrapper around engine-related data.
|
||||
type EngineClient struct {
|
||||
url string
|
||||
jwt [32]byte
|
||||
headfcu []byte
|
||||
url string
|
||||
jwt [32]byte
|
||||
chain *Chain
|
||||
http *http.Client
|
||||
}
|
||||
|
||||
// NewEngineClient creates a new engine client.
|
||||
func NewEngineClient(dir, url, jwt string) (*EngineClient, error) {
|
||||
headfcu, err := os.ReadFile(filepath.Join(dir, "headfcu.json"))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read headfcu: %w", err)
|
||||
func NewEngineClient(url, jwtSecret string, chain *Chain) *EngineClient {
|
||||
return &EngineClient{
|
||||
url: url,
|
||||
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.
|
||||
|
|
@ -52,18 +53,41 @@ func (ec *EngineClient) token() string {
|
|||
return token
|
||||
}
|
||||
|
||||
// sendForkchoiceUpdated sends an fcu for the head of the generated chain.
|
||||
func (ec *EngineClient) sendForkchoiceUpdated() error {
|
||||
var (
|
||||
req, _ = http.NewRequest(http.MethodPost, ec.url, io.NopCloser(bytes.NewReader(ec.headfcu)))
|
||||
header = make(http.Header)
|
||||
)
|
||||
// Set header
|
||||
header.Set("accept", "application/json")
|
||||
header.Set("content-type", "application/json")
|
||||
header.Set("Authorization", fmt.Sprintf("Bearer %v", ec.token()))
|
||||
req.Header = header
|
||||
// rpcRequest marshals a JSON-RPC 2.0 request body for the given method and params.
|
||||
func rpcRequest(method string, params ...any) ([]byte, error) {
|
||||
p, err := json.Marshal(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fmt.Appendf(nil, `{"jsonrpc":"2.0","id":1,"method":%q,"params":%s}`, method, p), nil
|
||||
}
|
||||
|
||||
_, 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
|
||||
}
|
||||
|
||||
// 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
2
cmd/devp2p/internal/ethtest/mkchain.sh
Normal file → Executable file
|
|
@ -6,5 +6,5 @@ hivechain generate \
|
|||
--tx-interval 1 \
|
||||
--length 600 \
|
||||
--outdir testdata \
|
||||
--lastfork prague \
|
||||
--lastfork osaka \
|
||||
--outputs accounts,genesis,chain,headstate,txinfo,headblock,headfcu,newpayload,forkenv
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ const (
|
|||
// Unexported devp2p protocol lengths from p2p package.
|
||||
const (
|
||||
baseProtoLen = 16
|
||||
ethProtoLen = 18
|
||||
ethProtoLen = 22
|
||||
// snapProtoLen accommodates snap/2 (EIP-8189) which extends snap/1 with two
|
||||
// additional message codes (GetBlockAccessLists=0x08, BlockAccessLists=0x09).
|
||||
// Using 10 is safe for snap/1 connections because the extra codes are simply
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import (
|
|||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -55,10 +56,7 @@ func NewSuite(dest *enode.Node, chainDir, engineURL, jwt string) (*Suite, error)
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine, err := NewEngineClient(chainDir, engineURL, jwt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine := NewEngineClient(engineURL, jwt, chain)
|
||||
|
||||
return &Suite{
|
||||
Dest: dest,
|
||||
|
|
@ -93,6 +91,10 @@ func (s *Suite) EthTests() []utesting.Test {
|
|||
{Name: "BlobViolations", Fn: s.TestBlobViolations},
|
||||
{Name: "TestBlobTxWithoutSidecar", Fn: s.TestBlobTxWithoutSidecar},
|
||||
{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.
|
||||
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)
|
||||
if err != nil {
|
||||
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))
|
||||
}
|
||||
return
|
||||
case *eth.NewPooledTransactionHashesPacket:
|
||||
case *eth.NewPooledTransactionHashesPacket72:
|
||||
continue
|
||||
case *eth.TransactionsPacket:
|
||||
continue
|
||||
|
|
@ -1020,15 +1022,16 @@ func makeSidecar(data ...byte) *types.BlobTxSidecar {
|
|||
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)
|
||||
for i := 0; i < count; i++ {
|
||||
for i := 0; i < txCount; i++ {
|
||||
// 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 {
|
||||
blobdata[i] = discriminator
|
||||
blobs -= 1
|
||||
blobCount -= 1
|
||||
}
|
||||
sidecar := makeSidecar(blobdata...)
|
||||
inner := &types.BlobTx{
|
||||
ChainID: uint256.MustFromBig(s.chain.config.ChainID),
|
||||
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()),
|
||||
Gas: 100000,
|
||||
BlobFeeCap: uint256.MustFromBig(eip4844.CalcBlobFee(s.chain.config, s.chain.Head().Header())),
|
||||
BlobHashes: makeSidecar(blobdata...).BlobHashes(),
|
||||
Sidecar: makeSidecar(blobdata...),
|
||||
BlobHashes: sidecar.BlobHashes(),
|
||||
Sidecar: sidecar,
|
||||
}
|
||||
tx, err := s.chain.SignTx(from, types.NewTx(inner))
|
||||
if err != nil {
|
||||
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) {
|
||||
|
|
@ -1056,28 +1062,30 @@ func (s *Suite) TestBlobViolations(t *utesting.T) {
|
|||
}
|
||||
// Create blob txs for each tests with unique tx hashes.
|
||||
var (
|
||||
t1 = s.makeBlobTxs(2, 3, 0x1)
|
||||
t2 = s.makeBlobTxs(2, 3, 0x2)
|
||||
t1, _ = s.makeBlobTxs(2, 3, 0x1)
|
||||
t2, _ = s.makeBlobTxs(2, 3, 0x2)
|
||||
)
|
||||
for _, test := range []struct {
|
||||
ann eth.NewPooledTransactionHashesPacket
|
||||
ann eth.NewPooledTransactionHashesPacket72
|
||||
resp eth.PooledTransactionsResponse
|
||||
}{
|
||||
// Invalid tx size.
|
||||
{
|
||||
ann: eth.NewPooledTransactionHashesPacket{
|
||||
ann: eth.NewPooledTransactionHashesPacket72{
|
||||
Types: []byte{types.BlobTxType, types.BlobTxType},
|
||||
Sizes: []uint32{uint32(t1[0].Size()), uint32(t1[1].Size() + 10)},
|
||||
Hashes: []common.Hash{t1[0].Hash(), t1[1].Hash()},
|
||||
Mask: types.CustodyBitmapAll,
|
||||
},
|
||||
resp: eth.PooledTransactionsResponse(t1),
|
||||
},
|
||||
// Wrong tx type.
|
||||
{
|
||||
ann: eth.NewPooledTransactionHashesPacket{
|
||||
ann: eth.NewPooledTransactionHashesPacket72{
|
||||
Types: []byte{types.DynamicFeeTxType, types.BlobTxType},
|
||||
Sizes: []uint32{uint32(t2[0].Size()), uint32(t2[1].Size())},
|
||||
Hashes: []common.Hash{t2[0].Hash(), t2[1].Hash()},
|
||||
Mask: types.CustodyBitmapAll,
|
||||
},
|
||||
resp: eth.PooledTransactionsResponse(t2),
|
||||
},
|
||||
|
|
@ -1105,15 +1113,21 @@ func (s *Suite) TestBlobViolations(t *utesting.T) {
|
|||
if code, _, err := conn.Read(); err != nil {
|
||||
t.Fatalf("expected disconnect on blob violation, got err: %v", err)
|
||||
} else if code != discMsg {
|
||||
if code == protoOffset(ethProto)+eth.NewPooledTransactionHashesMsg {
|
||||
// sometimes we'll get a blob transaction hashes announcement before the disconnect
|
||||
// because blob transactions are scheduled to be fetched right away.
|
||||
if code, _, err = conn.Read(); err != nil {
|
||||
t.Fatalf("expected disconnect on blob violation, got err on second read: %v", err)
|
||||
for {
|
||||
code, _, err := conn.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("expected disconnect on blob violation, got err: %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()
|
||||
|
|
@ -1132,22 +1146,29 @@ func mangleSidecar(tx *types.Transaction) *types.Transaction {
|
|||
|
||||
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.`)
|
||||
tx := s.makeBlobTxs(1, 2, 42)[0]
|
||||
badTx := tx.WithoutBlobTxSidecar()
|
||||
s.testBadBlobTx(t, tx, badTx)
|
||||
tx, _ := s.makeBlobTxs(1, 2, 42)
|
||||
badTx := tx[0].WithoutBlobTxSidecar()
|
||||
s.testBadBlobTx(t, tx[0], badTx)
|
||||
}
|
||||
|
||||
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.`)
|
||||
tx := s.makeBlobTxs(1, 2, 43)[0]
|
||||
badTx := mangleSidecar(tx)
|
||||
s.testBadBlobTx(t, tx, badTx)
|
||||
tx, _ := s.makeBlobTxs(1, 2, 43)
|
||||
badTx := mangleSidecar(tx[0])
|
||||
s.testBadBlobTx(t, tx[0], badTx)
|
||||
}
|
||||
|
||||
// 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
|
||||
// is cancelled before a message of the desired type can be read.
|
||||
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 {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
|
|
@ -1161,11 +1182,10 @@ func readUntil[T any](ctx context.Context, conn *Conn) (*T, error) {
|
|||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch res := received.(type) {
|
||||
case *T:
|
||||
return res, nil
|
||||
if t, ok := received.(*T); ok {
|
||||
return t, nil
|
||||
}
|
||||
conn.pending = append(conn.pending, received)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1203,10 +1223,11 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types
|
|||
return
|
||||
}
|
||||
|
||||
ann := eth.NewPooledTransactionHashesPacket{
|
||||
ann := eth.NewPooledTransactionHashesPacket72{
|
||||
Types: []byte{types.BlobTxType},
|
||||
Sizes: []uint32{uint32(badTx.Size())},
|
||||
Hashes: []common.Hash{badTx.Hash()},
|
||||
Mask: types.CustodyBitmapAll,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
ann := eth.NewPooledTransactionHashesPacket{
|
||||
ann := eth.NewPooledTransactionHashesPacket72{
|
||||
Types: []byte{types.BlobTxType},
|
||||
Sizes: []uint32{uint32(tx.Size())},
|
||||
Hashes: []common.Hash{tx.Hash()},
|
||||
Mask: types.CustodyBitmapAll,
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -1311,3 +1333,292 @@ func (s *Suite) testBadBlobTx(t *utesting.T, tx *types.Transaction, badTx *types
|
|||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
cmd/devp2p/internal/ethtest/testdata/chain.rlp
vendored
BIN
cmd/devp2p/internal/ethtest/testdata/chain.rlp
vendored
Binary file not shown.
|
|
@ -4,6 +4,7 @@
|
|||
"HIVE_CANCUN_BLOB_TARGET": "3",
|
||||
"HIVE_CANCUN_TIMESTAMP": "60",
|
||||
"HIVE_CHAIN_ID": "3503995874084926",
|
||||
"HIVE_DEPOSIT_CONTRACT_ADDRESS": "0x0000000000000000000000000000000000000000",
|
||||
"HIVE_FORK_ARROW_GLACIER": "0",
|
||||
"HIVE_FORK_BERLIN": "0",
|
||||
"HIVE_FORK_BYZANTIUM": "0",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"shanghaiTime": 0,
|
||||
"cancunTime": 60,
|
||||
"pragueTime": 120,
|
||||
"osakaTime": 180,
|
||||
"terminalTotalDifficulty": 131072,
|
||||
"depositContractAddress": "0x0000000000000000000000000000000000000000",
|
||||
"ethash": {},
|
||||
|
|
@ -31,13 +32,18 @@
|
|||
"target": 6,
|
||||
"max": 9,
|
||||
"baseFeeUpdateFraction": 5007716
|
||||
},
|
||||
"osaka": {
|
||||
"target": 6,
|
||||
"max": 9,
|
||||
"baseFeeUpdateFraction": 5007716
|
||||
}
|
||||
}
|
||||
},
|
||||
"nonce": "0x0",
|
||||
"timestamp": "0x0",
|
||||
"extraData": "0x68697665636861696e",
|
||||
"gasLimit": "0x11e1a300",
|
||||
"gasLimit": "0x5f5e100",
|
||||
"difficulty": "0x20000",
|
||||
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"coinbase": "0x0000000000000000000000000000000000000000",
|
||||
|
|
@ -141,5 +147,6 @@
|
|||
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"baseFeePerGas": "0x3b9aca00",
|
||||
"excessBlobGas": null,
|
||||
"blobGasUsed": null
|
||||
"blobGasUsed": null,
|
||||
"slotNumber": null
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"parentHash": "0x7e80093a491eba0e5b2c1895837902f64f514100221801318fe391e1e09c96a6",
|
||||
"parentHash": "0xc1c57b56e4bcda7b97cb6e2c3a8be7f1bb5212973718d692641ee6957722468e",
|
||||
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
|
||||
"miner": "0x0000000000000000000000000000000000000000",
|
||||
"stateRoot": "0x8fcfb02cfca007773bd55bc1c3e50a3c8612a59c87ce057e5957e8bf17c1728b",
|
||||
"stateRoot": "0x8b65aad07dfb184a5977da83d1cc2101d35edd6e8d0e8b7bb2711be3b2c163d8",
|
||||
"transactionsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
"receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
|
||||
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
"difficulty": "0x0",
|
||||
"number": "0x258",
|
||||
"gasLimit": "0x11e1a300",
|
||||
"gasLimit": "0x5f5e100",
|
||||
"gasUsed": "0x0",
|
||||
"timestamp": "0x1770",
|
||||
"extraData": "0x",
|
||||
|
|
@ -20,5 +20,6 @@
|
|||
"excessBlobGas": "0x0",
|
||||
"parentBeaconBlockRoot": "0xf5003fc8f92358e790a114bce93ce1d9c283c85e1787f8d7d56714d3489b49e6",
|
||||
"requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"hash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a"
|
||||
"slotNumber": null,
|
||||
"hash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09"
|
||||
}
|
||||
|
|
@ -4,9 +4,9 @@
|
|||
"method": "engine_forkchoiceUpdatedV3",
|
||||
"params": [
|
||||
{
|
||||
"headBlockHash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a",
|
||||
"safeBlockHash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a",
|
||||
"finalizedBlockHash": "0x44e3809c9a3cda717f00aea3a9da336d149612c8d5657fbc0028176ef8d94d2a"
|
||||
"headBlockHash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09",
|
||||
"safeBlockHash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09",
|
||||
"finalizedBlockHash": "0xd210ab04b32502603afba778811dc880e86299cdaf5e35a298661fad9b2a3d09"
|
||||
},
|
||||
null
|
||||
]
|
||||
|
|
|
|||
1184
cmd/devp2p/internal/ethtest/testdata/headstate.json
vendored
1184
cmd/devp2p/internal/ethtest/testdata/headstate.json
vendored
File diff suppressed because it is too large
Load diff
7271
cmd/devp2p/internal/ethtest/testdata/newpayload.json
vendored
7271
cmd/devp2p/internal/ethtest/testdata/newpayload.json
vendored
File diff suppressed because it is too large
Load diff
180
cmd/devp2p/internal/ethtest/testdata/txinfo.json
vendored
180
cmd/devp2p/internal/ethtest/testdata/txinfo.json
vendored
|
|
@ -17,11 +17,11 @@
|
|||
"tx-eip7702": {
|
||||
"account": "0xeda8645ba6948855e3b3cd596bbb07596d59c603",
|
||||
"proxyAddr": "0x4dc5e971f8b11ace4f21d40b0ede74a07940f356",
|
||||
"authorizeTx": "0x64c47531984e4099548b480b5af0bce04ab89d29f2fe7ae36e97ba68d688539f"
|
||||
"authorizeTx": "0xa9f4f425f5ce4ed0d9d8d30a58cf206f1d3a3f0da7ad96134576e92540cc0d78"
|
||||
},
|
||||
"tx-emit-eip1559": [
|
||||
{
|
||||
"txhash": "0xf3b025eaf924500c53bc0b9f0f5733d56b59b7dade1df336ba8b39c83d92ac57",
|
||||
"txhash": "0xd4266bd85926a3521918c6e492cfc71264b881b8674c2e6f83d7422b3688b894",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x7",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"logtopic1": "0x3569f740e521d8bb11c5b72660dc96272ad66bfd811ed918c3a9e02acd4ade8f"
|
||||
},
|
||||
{
|
||||
"txhash": "0x62da81199d8ac0d4231dec90c6085f1153a58eba6bbac04f71da4d5076c9b7c1",
|
||||
"txhash": "0x0b2a5113ce4638598727c4c96441c0dd52b637c1f5f2846ac6ec15089c31dd26",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x15",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
"logtopic1": "0xf77c749ecb156f605e2334b14caea388100bed09b4c16579c952a96e90355629"
|
||||
},
|
||||
{
|
||||
"txhash": "0x7a8c4456e604a0b6210f29c6e68405d2ca5425fdce16bfcc6dfa3d3904b32740",
|
||||
"txhash": "0x61640b02a288372b365c490669125c7ca739aef03798943785d512e454c80284",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x20",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
"logtopic1": "0x415feb809041baabc4d9246223e40f1083963cbe1ef6dedb8b153e49d02ee7ce"
|
||||
},
|
||||
{
|
||||
"txhash": "0xd1ec37a2bc2841c4dc61de447dbe772a92284714f03752bc4056eafdac74dc21",
|
||||
"txhash": "0xf3a593ff76f4517239d0d721aa0b734b5bc4242e804ec92984539ddab1d88c37",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x2b",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
"logtopic1": "0x89c17d9392b73a55738ba19aae192f2f9c5612dc8bd803ca23b9c2fb9c309e56"
|
||||
},
|
||||
{
|
||||
"txhash": "0xa2b3275339e1237e6918726e0f0906f7f6a70a069cfefd3d9b5380e67c2d1305",
|
||||
"txhash": "0x9197e83d462cdd6e5df80f38c93f0e04a95f9b0b2db05d2f2774cedbc42a888e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x36",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -61,7 +61,7 @@
|
|||
"logtopic1": "0x9038344c39b01167bfa8e99a6425d34bca24c27ceb191e8eba70ab5a8f719ce5"
|
||||
},
|
||||
{
|
||||
"txhash": "0xc488c5aac30312fe8365ed78fc6cc12b531f9cfc315e6ec5ddb610eac4a48976",
|
||||
"txhash": "0x258f7587ac5c4e8df9d1c15dae6362132eac6c9678cb135ea884a7bf4fc4bb48",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x41",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
"logtopic1": "0x4b3120af8064823e074758c51cd6cd0954587c0d94b5b37b336261fc7aa2ddb3"
|
||||
},
|
||||
{
|
||||
"txhash": "0x7b43a1b8ddf97ab6ccb6dcab385184d1a1165e3c221013b34448223af45fc832",
|
||||
"txhash": "0x243b74358431651d598b6bb5dcdf7d9d1d034528bed76cfcbde96adf2fb4c995",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x4c",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
"logtopic1": "0x41565ae6f06f2555139f444c467d6b709b45180aa0c6b15bb5b1388d55ef952c"
|
||||
},
|
||||
{
|
||||
"txhash": "0x8954b9d95e7b0d5a726847449419e1cf77a624199e1fa89f311150dcac94b361",
|
||||
"txhash": "0x0fb1554b5657396671c88eb70d62f1555d4f268fc1f8cb08a5afbe47f24ad1c3",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x57",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -85,7 +85,7 @@
|
|||
"logtopic1": "0x00f7ca033c24d91f8fc39cbf0edc8a43192507f93d7316f311b05eeb85921eed"
|
||||
},
|
||||
{
|
||||
"txhash": "0xb2749d3a5d1b062f85a4717ee5c3a7a686a31b03ecb2e29394bce32e15bc3e08",
|
||||
"txhash": "0xc976b09bc8c092788dba199637de6d3749e87a34d39e349588fdc62bdbc6ad90",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x62",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
"logtopic1": "0x761bf5fb1730fee0e499bb1806b9ae14394e673ab9c1dc12e95b9d3f1647cecd"
|
||||
},
|
||||
{
|
||||
"txhash": "0xbf2b014779d3872e5b8ae89c65d8c5912e60c459698f44f7281b9f14e03e6baf",
|
||||
"txhash": "0x4fdc7d1c163e836542e16c678e3d1adf2ec1d1411feac7eb93139b0181dfcef7",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x6d",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -101,7 +101,7 @@
|
|||
"logtopic1": "0x468eae0ffdb87a4dc081a86c494969801637f690e1e1da15fb4a9d2c78272da8"
|
||||
},
|
||||
{
|
||||
"txhash": "0x1d27789de7fbb33eed6c51da18ee5e7cef7b242046842d42141cfb7f4893b624",
|
||||
"txhash": "0xf1acc26aefda71041e3eaa74484ba9ca857ae1e0b08df6771c689e9ef9fe09ae",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x78",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
"logtopic1": "0xdbc7a073eb54d33d8e6dec5b0b635a874204bda1c23234ff0cca057ff8ed77f5"
|
||||
},
|
||||
{
|
||||
"txhash": "0x48369062975939c17e4695b1368d7a1e0b60d145662ef9ba0b7a61b39c3f2cd4",
|
||||
"txhash": "0xe13b0234e1257800d844c0580a9f1cf1752be7a664597fc02963470f5da8aa58",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x83",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
"logtopic1": "0x7a9cae3647128ba14914f547c5f27444cd7325bbc37e5038abc31eea45003034"
|
||||
},
|
||||
{
|
||||
"txhash": "0x7fbb4a27af5e59af9219a6b3a63b8cedf03d7f4601f1cdb83d12ccb85f50d315",
|
||||
"txhash": "0xf8ad019fc3453ddf04e81cebba81c0a907f99503558b76af5001093aa07ba258",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x8e",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -455,7 +455,7 @@
|
|||
],
|
||||
"tx-emit-eip2930": [
|
||||
{
|
||||
"txhash": "0x76f2917dfdad8c636c493258e672938661a2044a05338bbad7d14d446fae4d45",
|
||||
"txhash": "0xbc3032e0d2a47879f46ae9b732fed10707ace1b240537f7a096fc23192def461",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x8",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -463,7 +463,7 @@
|
|||
"logtopic1": "0x881a8434f98b103a2ee48727304618ca54234f1474c44bef70c21accc4dbc0a7"
|
||||
},
|
||||
{
|
||||
"txhash": "0x30fc0bab35d4325db37b953f5cf26e05f2c18593adcc4c14c8fb04b28501c2c5",
|
||||
"txhash": "0x82d6115a8bbeeb9db51bfb660f1876cc8a0ee29944b7c61aa1bbb5ceda8d5ff7",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x16",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -471,7 +471,7 @@
|
|||
"logtopic1": "0xa41cb4f2ab2731a8889754ae1a340c666cb8107b497b922073df80a9b255e31b"
|
||||
},
|
||||
{
|
||||
"txhash": "0x07032ce65db029864ffcc0c3b00ed01bf8fa90da0efc781ecf517920ed762304",
|
||||
"txhash": "0x0b75c2ab2ba045d613d83f4d48ea6e286a31f41b56e6e841e97af2d0f21e5aeb",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x21",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -479,7 +479,7 @@
|
|||
"logtopic1": "0xb2416e7ca12669406e6cd5154ad5177841b7d0cddeb2760249c28e1aa151f970"
|
||||
},
|
||||
{
|
||||
"txhash": "0x61a1ed16c11ef4e4c8036233b87753a761be8c9702514ea87fe3bc82345bbbba",
|
||||
"txhash": "0xa0fd707e7ec4a17c09bf0652b4dd2800aa759241b95d4ae14af954c0a0348ebb",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x2c",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -487,7 +487,7 @@
|
|||
"logtopic1": "0x11f0a8ac2adda075c95bbf6be534e3254dafa759f62cbcf0e91bc6f0335e70aa"
|
||||
},
|
||||
{
|
||||
"txhash": "0x24a0922096c6a6b34668d0b06c4724db961e3b4c462c6fa645517e19293ec37d",
|
||||
"txhash": "0x79a667af722e7558a947286047c69df7b3e8844f1ef37512e3ed2a642d7c6579",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x37",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -495,7 +495,7 @@
|
|||
"logtopic1": "0x8460e232c64e6cd9f816c02d855c892755984ebbb91592e683cda80aaba4ba22"
|
||||
},
|
||||
{
|
||||
"txhash": "0xdce27eb7c2b44d61022e4923329a9b18f0dd26093a88dd5efa772fb1e52273eb",
|
||||
"txhash": "0x727925c9a811cbcf1d005949ccec1a75391c7ee32513080a4d60789af589519e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x42",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -503,7 +503,7 @@
|
|||
"logtopic1": "0xe7d55978188f31ab090b1f10d8d401a66356b11ca8c296384a0a51e36e6ec11f"
|
||||
},
|
||||
{
|
||||
"txhash": "0xdb7b8df9ad36fc8838cfedc2f272cb205f7ad2860cd1f5ac5e739ed1a2973784",
|
||||
"txhash": "0x23660f1fb5a9f1da011df2e52a020854546433daa90df0ee8fd96c2e5180864e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x4d",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -511,7 +511,7 @@
|
|||
"logtopic1": "0x24a4daf5b3cac3bf3066902cda09da0fc862e0a6723c47981ed601782ad69079"
|
||||
},
|
||||
{
|
||||
"txhash": "0xccf0da8511e51af458ef4ea7734531fdb77b8bb48191efd8257cf6ac16daf3f6",
|
||||
"txhash": "0x93f5c00dce3a4fe9c5f52f133e58ba0175a68c8834bc6b5b69ee82edb284e235",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x58",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -519,7 +519,7 @@
|
|||
"logtopic1": "0x7c24a68c92e3b68daa153ae82eff9be1ebbab973384e0f4b256f158f93c5d525"
|
||||
},
|
||||
{
|
||||
"txhash": "0xe78dac2c962f7fc240909233c4b8bbf47a6ab4b9b3b4376ecb92a4eff4e147e3",
|
||||
"txhash": "0x99fb0fa518cd6f627acb5d3a0315e0bfb2b1240c86a183ce3e88d495ccecd996",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x63",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -527,7 +527,7 @@
|
|||
"logtopic1": "0x02bd9d62880450596e11c3417f2644a81f7cc233a05394bbbfb58428ed53f413"
|
||||
},
|
||||
{
|
||||
"txhash": "0x2e5f23d99b09fd9f81f2fc7013c3a3bd8fea88bd6df07e2b236addad4e9e94e3",
|
||||
"txhash": "0xf9435fd557962bd981ceab8bf73f461911cfb6c6f858ae0f24a073dccc7e5b4a",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x6e",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -535,7 +535,7 @@
|
|||
"logtopic1": "0x0dcf6219856f226889a2440b388d8e15f5df0eb64a7b443f3a7a5dca7b87b0f2"
|
||||
},
|
||||
{
|
||||
"txhash": "0x37010507fe716034116f8d945ce2bc35d3718e7c10b78ba34e4ddf780c6063b7",
|
||||
"txhash": "0xfb39aee0fb8ca73b28c536b84611084acefa97131136506677fee0028c5529a6",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x79",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -543,7 +543,7 @@
|
|||
"logtopic1": "0x0f624930606bfcd2386d583abca6ab10227d71fc1633fea53f94bd146c152b8f"
|
||||
},
|
||||
{
|
||||
"txhash": "0xb53c6450c32830cf3be18b73370a0a290a12acbf4d734e8ad8785d4742e8035b",
|
||||
"txhash": "0xc6241637f7a892ef60a47f0022820001b7d8844408f1dee98280e1b45ab7e5c2",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x84",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -551,7 +551,7 @@
|
|||
"logtopic1": "0x2daaea9286d7edb7568e0803a61bfdb1e1506156d27e93bdf1942564850646c6"
|
||||
},
|
||||
{
|
||||
"txhash": "0xb7b48df8d1f44601669025fe789c2a5aa05771ac35ea90bd5bef536ecdc4f92e",
|
||||
"txhash": "0x6d595d077cd7ef22edcdebac2c6c88884d78c06bd126cfd0386b429920eafd5c",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x8f",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -889,7 +889,7 @@
|
|||
],
|
||||
"tx-emit-eip4844": [
|
||||
{
|
||||
"txhash": "0x87630fa02828dad0c04ea8ffa7c5d742bf6aac72b8b0a7e72a28b7d2c0d9fdcb",
|
||||
"txhash": "0xcae406f12b53284afa64066b311c5a2dda207a29d613a13cdb40e69972b7c807",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x9",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -897,7 +897,7 @@
|
|||
"logtopic1": "0x63cde520fb894276a981d2c9099bef9beb949121c1be98f3abe1b721d880899f"
|
||||
},
|
||||
{
|
||||
"txhash": "0x75118b029d56f192f458f1a2a38238237f19c11f88eb97e0483ad26e021ae15c",
|
||||
"txhash": "0xe5a5c803e6d246a5e3be1ec77345e229ab5002f19fba46e77ebbe2e532f1568e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x17",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -905,7 +905,7 @@
|
|||
"logtopic1": "0xa6d01173df2aa437fb0118d181e64a8f8e05713fc01c42fbfd2250516639ae95"
|
||||
},
|
||||
{
|
||||
"txhash": "0x99ba6434209d91a5fc015f2c409877d1d5567287b64c46f984a64d631efa866d",
|
||||
"txhash": "0x71c6f6ae274695f1f7c4cee7a0811766a98c1f418984da6cf149881fe882183a",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x22",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -913,7 +913,7 @@
|
|||
"logtopic1": "0xe94d0b2545ec05c3ce3431c4d45c3b62fcab156563e8308fae1ebd27a2810c1a"
|
||||
},
|
||||
{
|
||||
"txhash": "0x17eced62882b234b280f2382a0225cf218e59de7124bd3d7214fa2da8b766188",
|
||||
"txhash": "0x3510d0abeb988608581261e85d509ac14c63039ed361106eb59a397facf2dd6e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x2d",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -921,7 +921,7 @@
|
|||
"logtopic1": "0x6551251b96ca27f3af8a2c500d6dd1ea5b9ab7002b3d923b66db0493f4a7123e"
|
||||
},
|
||||
{
|
||||
"txhash": "0x7cb0ae261889695ae99b64ea9f6468a493f87b90fccf073761daf15ed4868551",
|
||||
"txhash": "0x1898080cc14ae4c05be4a395ebfeda29658c72a537ee2f2f812d9c735b803ef9",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x38",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -929,7 +929,7 @@
|
|||
"logtopic1": "0xfa29cff134420b6526f434ab690a9c3a140aa27b8479ae3d8d83b6c799acbc23"
|
||||
},
|
||||
{
|
||||
"txhash": "0x0e6a717775a24331736f5062466715bdf7a4330466d235a6ffdd746bffdcaf22",
|
||||
"txhash": "0xc8517fc20db042e5211607716c47aea078ab33262243858499c3a1755dc3e5d3",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x43",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -937,7 +937,7 @@
|
|||
"logtopic1": "0x412379b7f583981ea6e84408cba75ced69039e07ce9cdaa32a8a9dac997aaafb"
|
||||
},
|
||||
{
|
||||
"txhash": "0x153401e45e9053f121b36d88763bec1515553e3762cf5f46f4d125b0700435aa",
|
||||
"txhash": "0xf817888429c51c567d479b9bd40bbdfb93fd2ee61b19bddc52551c22fb71fae5",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x4e",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -945,7 +945,7 @@
|
|||
"logtopic1": "0x87dfa85154edde1626e3a09196eab4b60f71887ec7b50ccbbe7ec76c0be6bdff"
|
||||
},
|
||||
{
|
||||
"txhash": "0x559f888f55e908ab3f4ad9d0d693b4d572ff9ab00058df6465cfe43722b45c6b",
|
||||
"txhash": "0xa2517a73503fe83df09db0aab07a4ee54eb0ed86b4b0436093417286a4148d12",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x59",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -953,7 +953,7 @@
|
|||
"logtopic1": "0x6e2466f20ef20cb42d216dbf4a0d934199213e9b8d75bedc9c2d3e038a587474"
|
||||
},
|
||||
{
|
||||
"txhash": "0x154de45bd8bde35c256318cb983131890d97b99689ec175d686cf88742e82fc9",
|
||||
"txhash": "0x4f12711cb97d5d47be629e8b3f0738db8f16ae39461b6b9184108d20a0057153",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x64",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -961,7 +961,7 @@
|
|||
"logtopic1": "0xd75c9abb1414054ca164bba2f8c09917fb90c24789feaa311ee34a0b3f4a82f0"
|
||||
},
|
||||
{
|
||||
"txhash": "0x67de1da7ebd65a1f384814112a0ef54c3907e09e18c8a0ddb5630a6883a3c8c5",
|
||||
"txhash": "0x663ef65cec978da040307899739da355fac5aad6feb15df48becb3f64033f76f",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x6f",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -969,7 +969,7 @@
|
|||
"logtopic1": "0x165e0e0cc13ca53c5af4860637550364c5c90a512906490ace14efb534873741"
|
||||
},
|
||||
{
|
||||
"txhash": "0x478293db9f43eec14e4a54b90cb07d6e85e72095f3729d3f80c7e8c33f4b0f19",
|
||||
"txhash": "0xbda32538691913b1eae9a4d22083143adadbfd374d15cd5393c39e1d27137a73",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x7a",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -977,7 +977,7 @@
|
|||
"logtopic1": "0x16bee816935475cd45501fc5fd01bf913f8ef54330a43d80ef73101a4c728b34"
|
||||
},
|
||||
{
|
||||
"txhash": "0xd229b870b66a6c5452c88260ef6cc32ffa71363ddbd09340ccda6173af447fc8",
|
||||
"txhash": "0xc35946ee7a8d5c109f7925c287ed8b54819d58d09f7aec26e3516f0ac171ec9d",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x85",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -985,7 +985,7 @@
|
|||
"logtopic1": "0xaf1f0d50933e49dd24b61a24c670809a5b875e3b746862636288dead8579dc4e"
|
||||
},
|
||||
{
|
||||
"txhash": "0xf70f764ffe1e6c2e2770eab2bf0137ece8563607cc3578c8b0e76709105553fd",
|
||||
"txhash": "0x57c99e9085ec43eb172d51b4d535fb50529ce90dba93a2dd984d6e6b235b8f73",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x90",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1323,7 +1323,7 @@
|
|||
],
|
||||
"tx-emit-legacy": [
|
||||
{
|
||||
"txhash": "0xc9e729de373f2fcab9ddd36402c047cb117522ef48b25435bd415b9bcf794bb9",
|
||||
"txhash": "0x7dcf6bcb38bc1286315b603160051cb35d0a582df99ac8901c6dd640aa14b0e9",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0xa",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1331,7 +1331,7 @@
|
|||
"logtopic1": "0xb8d28e7b703baf999848ecbba44026cb6479b3f0466037bcf2221ffc3f8549f9"
|
||||
},
|
||||
{
|
||||
"txhash": "0xfe4c001ff9cbf3338dd712296b718a6467ab734617ca225ce7ce4e3a9547be41",
|
||||
"txhash": "0x90929818f801394ad3c7d13d2bcb9409442f04e569ebc049825686c37c12cbe2",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x18",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1339,7 +1339,7 @@
|
|||
"logtopic1": "0x95104e47e1982aba633477f377b1511396c3fe83600224bcb0c78949be705b33"
|
||||
},
|
||||
{
|
||||
"txhash": "0xffde4e2fd1cd2f5e51e564b08216b9ef80978c586098221cd19f1ee742db1c03",
|
||||
"txhash": "0xe78692021910fd245d1829d0a9417a24812988f6eccc0ec010faa282d613fd5d",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x23",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1347,7 +1347,7 @@
|
|||
"logtopic1": "0x2acfc92a1cc51397c95e434631e449d83a81de91964ed735a8c8b71b35e1a626"
|
||||
},
|
||||
{
|
||||
"txhash": "0x965195498584dd1a4ee5ae7d60e5bb4dae1254b2ec61023f7b86678d75643201",
|
||||
"txhash": "0x3b54d3d700d5822e6c67ac0f5e4340168864808dd28f7802cedfff4c85eaf9f5",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x2e",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1355,7 +1355,7 @@
|
|||
"logtopic1": "0x5b6618f0def0d634d51118d232eadc26ecbc8d54a7efaa225afc472f0a611c69"
|
||||
},
|
||||
{
|
||||
"txhash": "0x209a012091e6c869a96f21aee8de7ca44490a593d0f619793b961b2ba4a777b2",
|
||||
"txhash": "0xd153e85fb648f702df7ae218351dd0769a49b2fc48dafd7bd6f4aff55c701c80",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x39",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1363,7 +1363,7 @@
|
|||
"logtopic1": "0x349c26db328204bd2527eb45003b0039d5a636f76c8849bca0b34e8fb134f505"
|
||||
},
|
||||
{
|
||||
"txhash": "0xff76c1915b804a9f4abe17f6564b278ece6cbc1ceca0604856e7f440cbd6d9c8",
|
||||
"txhash": "0x50ad4ad01002d9962c6b65c8e161c6bf70ebbad6ca1bd4066dfaa099bc8fb159",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x44",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1371,7 +1371,7 @@
|
|||
"logtopic1": "0xb66fab7dddd4d16174b227a6f958d7ba2ae8ebc52d763b02c1ff944362755e6e"
|
||||
},
|
||||
{
|
||||
"txhash": "0x39c37d8cff047f5047fce694f7703b57b08cf414710cd94fc3daea5902330559",
|
||||
"txhash": "0xca23bea2a2bf588eea4377dd466cf398ac58a788eca44b49701cc1d7e5d7c55d",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x4f",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1379,7 +1379,7 @@
|
|||
"logtopic1": "0x780fa5a814a83baf682b2f170be956308be6ce1bf84ce68ca5f3c59cc41c7c28"
|
||||
},
|
||||
{
|
||||
"txhash": "0xbac05861c5a87e48448ca240659aca18658a08c7d0b6318690809b0c227470db",
|
||||
"txhash": "0xfae95cd71c7a11c43addca3cc1defb2ca7fc42874fbc2b19563267fc13a0b3f5",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x5a",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1387,7 +1387,7 @@
|
|||
"logtopic1": "0x9739ae7192ad23a41778719941582886701a0830589c7ebfc5db094037635d82"
|
||||
},
|
||||
{
|
||||
"txhash": "0xf24d6d5bd0136dc7a2cf4ce1e2daf6191b415af43dcaf6bf1d43ae69ef2a306c",
|
||||
"txhash": "0x3de36576222ddf2638908d967047eb6f2ee53ddb914d048963e369eeee787f41",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x65",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1395,7 +1395,7 @@
|
|||
"logtopic1": "0xf5acd98a17a3425f113b869e0dd03f82ee696401d2e7f59e8902610150a95a20"
|
||||
},
|
||||
{
|
||||
"txhash": "0xaf83fcaed15f46e777c9cb54fd6465f00dd6d929c556bd5644ea39fce51be74a",
|
||||
"txhash": "0xfcb2270a6f82e1c8af14f5bd12fb9dd37f4b5ea9cf40afb80ec4f2028231e31a",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x70",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1403,7 +1403,7 @@
|
|||
"logtopic1": "0x5cd04d660080fb51a0cc8df0d716e1bff4eff98c887cf3274aabe7ec53dc3615"
|
||||
},
|
||||
{
|
||||
"txhash": "0x7382ad28a6589fcf07195874a36eee8f4c5fa22f3288a1979b374d80e7c6fb88",
|
||||
"txhash": "0x60459b81f4643d1658d49b39dcf461115315623564c2f9933786ea93e7dc6dd9",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x7b",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1411,7 +1411,7 @@
|
|||
"logtopic1": "0xf199b2d65bb711d578312320d210574bcc79d63c841d7dcf96ee3604140a7353"
|
||||
},
|
||||
{
|
||||
"txhash": "0x730f124954d2b20c9ebfdf6715da7192a570450b485ae60c12a102fecd80d21d",
|
||||
"txhash": "0x911b91ea0109bdb6b7e799f7709eca8e3c618040cde2b4940cccb8bb1b6aaa41",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x86",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1419,7 +1419,7 @@
|
|||
"logtopic1": "0xe9bc0af4e1917255f83262d4d61622be8b86fcb24a5810c7b592dd6da6861d56"
|
||||
},
|
||||
{
|
||||
"txhash": "0x69e193cbcc3231501957748043aa48e52b141d0fa38dc6f5027e06790e3088fe",
|
||||
"txhash": "0x95cb4ad3fc4adef8939ec2fb223fece8763016de2b1f2ec49e15837988d651c5",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x91",
|
||||
"indexInBlock": 0,
|
||||
|
|
@ -1757,78 +1757,78 @@
|
|||
],
|
||||
"tx-largereceipt": 11,
|
||||
"tx-request-eip7002": {
|
||||
"txhash": "0x27d8ca12587f28aa0184966fabcbeb321b438a6495d5e8294dc9c62525f41b8c",
|
||||
"txhash": "0xb42e37aec62317cd546baa1a89ce962434f44f9ea6a6a41a8dabff9c4a19e6c8",
|
||||
"block": "0xd"
|
||||
},
|
||||
"tx-transfer-eip1559": [
|
||||
{
|
||||
"txhash": "0x4feb87725c3d45aef205359d7d200f523004b2961282c47736c49e3a453a3ab8",
|
||||
"txhash": "0xcedc0fddca312530a8b5651470fb15360ed9b305ef689c2b39e8a8f9fae734de",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0xe",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xf282b242af9a863a650850cd7390dc3b3b485845e6290ee171edffab96a7c6dc",
|
||||
"txhash": "0x53b3a68202af8f71fecda9dbdce6bc339fd2b1ab74db91a1e17674e845f9031d",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x19",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xebf2517adcc7342a874af927a17590156aa85059fc903ab1b09f97c34c8b8b4e",
|
||||
"txhash": "0xdc35055a250fa2d2e831885cb3c46c3812f9e758daf507c98550d5146aa2635d",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x24",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x926f6bb97f760b2ac6c523e975d2697276ee3d881dd8348a4e7b7f2c846da188",
|
||||
"txhash": "0xa4ec63edd50e820f2324532f814bf7d1511e44e5c3a594c4a2d727ad2b0f8915",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x2f",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xef9dea375d62f1406d8777e70192ba24af24044c356da063eb71712eb7499356",
|
||||
"txhash": "0x562f9ac6a63e6ae37a85aa33ca288f54e32af561c3217b52fecfcf0bc291da2e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x3a",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x0816a60a0e6fd2afe45e7db877e33d055c3e4549037eb1e5209a21c4018250bb",
|
||||
"txhash": "0xd0092f480fffa51f656c71e7957d47cac2e424baa4d279706409b9c3573c904e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x45",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x9720170ff561812f8bc717c251fbc942af99eefb8b5be1296c0dc3b331da7b33",
|
||||
"txhash": "0x98fd345c029085610b9e2ec506cf7af62dd030eb7879b4727b5dc5bf98ba48e4",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x50",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xceda26628e2eecad063f6a4341ea4043fcce307a748b5a41c2a3d0e45721bb0f",
|
||||
"txhash": "0x6f35a7b982016ca589c769b04d3569cfd689a4ececd953f690a1eafbcd1af0ca",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x5b",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x0d8c55478b560a6305ea886fbbfcb36441767c65270c9618ceb33b22f2ee4eef",
|
||||
"txhash": "0xe8e51ddc809af0bcda8dcef19d25e052edcbc727507f0cb631a3100e33ce9ca6",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x66",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x12b7508b1d9a5f9afb9de821961aca87f93d97849b8a1b5065a0e50cfc2e8a5e",
|
||||
"txhash": "0xe395cb327beb13b61c158edc2245e55f32d2d039059fd42a266b967c16e8c35c",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x71",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xebe88d489dad8695b60b9e8370ce0e389e7cb9e49670980919aac829e2abb83b",
|
||||
"txhash": "0xbdd4c75b7c14d106d77035e0a2eebf332a3e315d4ce0349c00770f2bab8bf050",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x7c",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xdd63b0ac29f1b5b9911a159e704a148fa190ae321158a23e2910b52b69f810ce",
|
||||
"txhash": "0xe5ca0952b6f89e9996f714e69dc89395ec547617ee051c7ce00684f2911c7aba",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x87",
|
||||
"indexInBlock": 0
|
||||
|
|
@ -2088,73 +2088,73 @@
|
|||
],
|
||||
"tx-transfer-eip2930": [
|
||||
{
|
||||
"txhash": "0xf278a97c1c92c517531ae9f24179862518cf6397cd3615deebe37049d5a6c695",
|
||||
"txhash": "0xb6ad3492b1c07503a7d15857c641c16f9250482f645ecc84efc83c88c54ba2e1",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0xf",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xc33009465ef1eb2ff62d02d50cbaaf73fee9dff8be51f71ac7976367ddfc4541",
|
||||
"txhash": "0x7c0c5c8ed14c8ec3d60997eb6e92830c31921d9344eca774c2c571bc8c2b0bdc",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x1a",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xa6598b072ed0598260358ed568e3c907d36e422b14e35ab459e9e24ae98251ef",
|
||||
"txhash": "0x973b494d5bfa55b1fbf4c72122d42ceb8b8667a598948aa24d6a6026de4a7999",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x25",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x61aa0de65eb4a0db81573b043c35a4de3dcfabf37117a3e67905c7d3038dad23",
|
||||
"txhash": "0x93de7571e134adbcd98920fb7fbaf0a62b2881896adf2b22f4191bed28ef9e98",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x30",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xdcf01b9729cddeefec7be5348f0d364ee1295831f999c0f5a49b047c9694e970",
|
||||
"txhash": "0x79dbf4a386f7865dcafb0d03bd8ff6c0faa570a5ca70eb1b70e656b2d1b1615a",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x3b",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x7867b39e0b2f176fbd61364bce00443bd3f2ca5d743d3ab95104cb13a5cf5208",
|
||||
"txhash": "0x66354d493e12db97bd095e4e038b1e685dbdaf4162582b32d8d336463a7951e7",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x46",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x2ba84167cbba25e83e98cbca146d39a4a36d29dc2c9d971b41bf5ea1b9c9425a",
|
||||
"txhash": "0xe6dd0491c7bbc402007f13a14c2ed84f2811cf77f8aeb825551cf00daf942d81",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x51",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x203b1d7997f674fd45fe2eb6af5f0a88b59eaa8598b86f408ca44763e034bd36",
|
||||
"txhash": "0x1fb266110683f2166d5eb642c320a536b19768fe304f6594f2596d3258094774",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x5c",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x8fd08a67b065db1005917aea2934104c6132cad339c2fe07f51e7717bbe4ffe7",
|
||||
"txhash": "0x2eabf1863fd795c62b57ca18f88e0a5f4975daf8af8a125bd61c1d10ed910cfa",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x67",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xf0a9ae994a7741fcbe854f2d4a684f5fd434d40badc2ae36611f5764aaeb08b3",
|
||||
"txhash": "0x382f8b760cf6025b553959a625090243dc294eec54e3d6e667c031bc9af679b6",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x72",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x090fcb3259cccab1bad9dd572493e4ee05df3f53137ca16b4e7ea2f5b5c1d7d3",
|
||||
"txhash": "0x7e930c8cb726972a14b38961c161efdfd5e088f0796ba1e8008869655200a192",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x7d",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xe79b88caea408e8be7e3ac3269889b8f59fd4c1ca612213b9a7999ffe5b72e52",
|
||||
"txhash": "0xbb0ec7a4e53d83b5c746a097cd57ec5607cf0770832f15c390bb08a5a362f17f",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x88",
|
||||
"indexInBlock": 0
|
||||
|
|
@ -2414,73 +2414,73 @@
|
|||
],
|
||||
"tx-transfer-legacy": [
|
||||
{
|
||||
"txhash": "0x4845dfdee733cb11e204253aa9b2e0a9597c9b7f700642d084fd5aad365dbeff",
|
||||
"txhash": "0xacbd2cf02cbc7346d3f58be2c8d7fe1bd7d63608b93048fae4f41ccb8d732a16",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x10",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xe4ba4ed6784f2f452274186824174874661f76c1961d34fee86371da194a428f",
|
||||
"txhash": "0x5058b2f5d42ca2af87b9e6a322ec27058e3ab8efa6de89147228c1d07526e667",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x1b",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xe30c98691cc89f2db31aec5c2b3cdfd720015210fce4b48a8ce561d7f179af8b",
|
||||
"txhash": "0x085533a791ba8c5f51334ed52f0544884cbb7816c262176808a9aa8a496e4d60",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x26",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x7538bb702539b67fbb8d44d2e946d1d241ddc4c2943e48ecb3107c8868184775",
|
||||
"txhash": "0xc8f4003509b7bad353fe440fd24bd3982e2661c16d93c75140cca6735c46014b",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x31",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x88edfa85d3e3bab608081a9421e0eab5fbd09ad10db58c07c178c6ac74617233",
|
||||
"txhash": "0xd2ba3607ed8239e11b1cc2c53bd45694601ec7a6d1020a5e7f2c5ebf1206073b",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x3c",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x41aec073b267edc44e28b0c33f8c20e7619687ca5d7f09faa30e49621cdd3e70",
|
||||
"txhash": "0xd06ab6b82ec91be99bea3bedeb4929233c14650c6ef76fe9d6fbe782dc94205e",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x47",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xe0854da80105ea343c29cb7a8db30413b59f5b62a1f42793ce635be4e7536cc9",
|
||||
"txhash": "0x93e8dc866ce419e01221ff657eb79ac65a4361499d6cea117ca4de09023c1527",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x52",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x0cf942350189ee11294379c2a16c9744bc7502f127f4f71ac95cf0a2f681aa3a",
|
||||
"txhash": "0x965ddac3d0550607ba9e6e8bec74c3d8f881ee65758e38306790707cf27956df",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x5d",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0xd521a3fccc17113eac1a50bd0a34a3ca87ee5234c51fe41ba8eb8c26ad142363",
|
||||
"txhash": "0x265395037938c0d7312559ea4c10c211be89d04008dd7fc5752b3f633cc22404",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x68",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x1bcb6ff54f0f244c3b7390b488e76c3d1d95ff7f340adb81f785820930edc010",
|
||||
"txhash": "0x0dae10c3d6a4640ec7a257131ba77a6aaf5e00e9e3dc09b291dfba51dd0134c8",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x73",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x7571a5b0881a76054142ad7af3924bd225a67a4705479f4f87e6a68388a5f0aa",
|
||||
"txhash": "0x629e56214deda1fdc4ad3db433c3034816e93752258037bf6d5fce4525061d03",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x7e",
|
||||
"indexInBlock": 0
|
||||
},
|
||||
{
|
||||
"txhash": "0x999f17b911d92aaed0c83e37a545cf44f1effaf1245bddc7c63aae3c092e8200",
|
||||
"txhash": "0x60b9a9cff166acc4822d235ae6c636dd7a9394ffc9ebc30ce2769c9bd02ae2fc",
|
||||
"sender": "0x7435ed30a8b4aeb0877cef0c6e8cffe834eb865f",
|
||||
"block": "0x89",
|
||||
"indexInBlock": 0
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ func (s *Suite) sendTxs(t *utesting.T, txs []*types.Transaction) error {
|
|||
for _, tx := range txs {
|
||||
got[tx.Hash()] = true
|
||||
}
|
||||
case *eth.NewPooledTransactionHashesPacket:
|
||||
case *eth.NewPooledTransactionHashesPacket72:
|
||||
for _, hash := range msg.Hashes {
|
||||
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())
|
||||
}
|
||||
}
|
||||
case *eth.NewPooledTransactionHashesPacket:
|
||||
case *eth.NewPooledTransactionHashesPacket72:
|
||||
for _, hash := range msg.Hashes {
|
||||
if _, ok := invalids[hash]; ok {
|
||||
return fmt.Errorf("received bad tx: %s", hash)
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ var (
|
|||
utils.BlobPoolDataDirFlag,
|
||||
utils.BlobPoolDataCapFlag,
|
||||
utils.BlobPoolPriceBumpFlag,
|
||||
utils.BlobPoolFetchProbabilityFlag,
|
||||
utils.SyncModeFlag,
|
||||
utils.SyncTargetFlag,
|
||||
utils.ExitWhenSyncedFlag,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/crypto/kzg4844"
|
||||
"github.com/ethereum/go-ethereum/eth"
|
||||
"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/gasprice"
|
||||
"github.com/ethereum/go-ethereum/eth/syncer"
|
||||
|
|
@ -514,6 +515,12 @@ var (
|
|||
Value: ethconfig.Defaults.BlobPool.PriceBump,
|
||||
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
|
||||
CacheFlag = &cli.IntFlag{
|
||||
Name: "cache",
|
||||
|
|
@ -1692,6 +1699,9 @@ func setBlobPool(ctx *cli.Context, cfg *blobpool.Config) {
|
|||
if ctx.IsSet(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) {
|
||||
|
|
|
|||
|
|
@ -129,9 +129,12 @@ type blobTxMeta struct {
|
|||
|
||||
announced bool // Whether the tx has been announced to listeners
|
||||
|
||||
id uint64 // Storage ID 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
|
||||
id uint64 // Storage ID 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
|
||||
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
|
||||
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
|
||||
}
|
||||
|
||||
// blobTxForPool is the storage representation of a blob transaction in the
|
||||
// BlobTxForPool is the storage representation of a blob transaction in the
|
||||
// blobpool.
|
||||
type blobTxForPool struct {
|
||||
type BlobTxForPool struct {
|
||||
Tx *types.Transaction // tx without sidecar
|
||||
Version byte
|
||||
Commitments []kzg4844.Commitment
|
||||
Proofs []kzg4844.Proof
|
||||
Blobs []kzg4844.Blob
|
||||
CellSidecar *types.BlobTxCellSidecar
|
||||
}
|
||||
|
||||
// Sidecar returns BlobTxSidecar of ptx.
|
||||
func (ptx *blobTxForPool) Sidecar() *types.BlobTxSidecar {
|
||||
return types.NewBlobTxSidecar(ptx.Version, ptx.Blobs, ptx.Commitments, ptx.Proofs)
|
||||
// Sidecar returns BlobTxSidecar of pooled transaction. Since this function
|
||||
// recovers the blob field in sidecar, it is expansive and needs to be
|
||||
// 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
|
||||
// reconstructing the transaction.
|
||||
func (ptx *blobTxForPool) TxSize() uint64 {
|
||||
var blobs, commitments, proofs uint64
|
||||
for i := range ptx.Blobs {
|
||||
blobs += rlp.BytesSize(ptx.Blobs[i][:])
|
||||
func (ptx *BlobTxForPool) txSize() uint64 {
|
||||
sidecar := ptx.CellSidecar
|
||||
|
||||
var commitments, proofs uint64
|
||||
for i := range sidecar.Commitments {
|
||||
commitments += rlp.BytesSize(sidecar.Commitments[i][:])
|
||||
}
|
||||
for i := range ptx.Commitments {
|
||||
commitments += rlp.BytesSize(ptx.Commitments[i][:])
|
||||
}
|
||||
for i := range ptx.Proofs {
|
||||
proofs += rlp.BytesSize(ptx.Proofs[i][:])
|
||||
for i := range sidecar.Proofs {
|
||||
proofs += rlp.BytesSize(sidecar.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))
|
||||
}
|
||||
|
||||
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.
|
||||
func (ptx *blobTxForPool) ToTx() *types.Transaction {
|
||||
return ptx.Tx.WithBlobTxSidecar(ptx.Sidecar())
|
||||
func (ptx *BlobTxForPool) toTx() (*types.Transaction, error) {
|
||||
sc, err := ptx.sidecar()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ptx.Tx.WithBlobTxSidecar(sc), nil
|
||||
}
|
||||
|
||||
// newBlobTxForPool decomposes a blob transaction into blobTxForPool type.
|
||||
func newBlobTxForPool(tx *types.Transaction) *blobTxForPool {
|
||||
func newBlobTxForPool(tx *types.Transaction) (*BlobTxForPool, error) {
|
||||
sc := tx.BlobTxSidecar()
|
||||
if sc == nil {
|
||||
panic("missing blob tx sidecar")
|
||||
return nil, errors.New("missing blob tx sidecar")
|
||||
}
|
||||
return &blobTxForPool{
|
||||
Tx: tx.WithoutBlobTxSidecar(),
|
||||
cells, err := kzg4844.ComputeCells(sc.Blobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sidecar := types.BlobTxCellSidecar{
|
||||
Version: sc.Version,
|
||||
Commitments: sc.Commitments,
|
||||
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
|
||||
// network transaction encoding. This is used for getRLP.
|
||||
// encodeForNetwork transforms stored BlobTxForPool RLP into the network
|
||||
// transaction encoding for the given eth protocol version. Used for getRLP.
|
||||
//
|
||||
// Stored RLP: [type_byte || tx_fields, version, [comms], [proofs], [blobs]]
|
||||
// V0: type_byte || rlp([tx_fields, [blobs], [comms], [proofs]])
|
||||
// V1: type_byte || rlp([tx_fields, version, [blobs], [comms], [proofs]])
|
||||
func encodeForNetwork(storedRLP []byte) ([]byte, error) {
|
||||
// Stored RLP: [type_byte || tx_fields, [version, [cells], [comms], [proofs], custody]]
|
||||
//
|
||||
// eth/69, eth/70: [blobs] is recovered from stored cells via kzg.
|
||||
//
|
||||
// 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)
|
||||
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 {
|
||||
return nil, fmt.Errorf("blobTxForPool has %d elements, need at least 5", len(elems))
|
||||
if len(elems) < 2 {
|
||||
return nil, fmt.Errorf("BlobTxForPool has %d elements, need at least 2", len(elems))
|
||||
}
|
||||
|
||||
// 1. Extract tx byte and other tx fields
|
||||
|
|
@ -226,19 +268,53 @@ func encodeForNetwork(storedRLP []byte) ([]byte, error) {
|
|||
typeByte := txBytes[0]
|
||||
txRLP := txBytes[1:]
|
||||
|
||||
// 2. Find the version of sidecar.
|
||||
version, _, err := rlp.SplitUint64(elems[1])
|
||||
if err != nil || version > 255 || version == 0 {
|
||||
// 2. Split the nested CellSidecar list.
|
||||
sidecarElems, err := rlp.SplitListValues(elems[1])
|
||||
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)
|
||||
}
|
||||
// 3. Extract sidecar elements.
|
||||
commitmentsRLP := elems[2]
|
||||
proofsRLP := elems[3]
|
||||
blobsRLP := elems[4]
|
||||
|
||||
// 4. Reconstruct into the network format.
|
||||
outer := [][]byte{txRLP, elems[1], blobsRLP, commitmentsRLP, proofsRLP}
|
||||
// 4. Extract sidecar elements.
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -252,21 +328,23 @@ func encodeForNetwork(storedRLP []byte) ([]byte, error) {
|
|||
|
||||
// newBlobTxMeta retrieves the indexed metadata fields from a pooled blob
|
||||
// transaction and assembles a helper struct to track in memory.
|
||||
func newBlobTxMeta(id uint64, size uint64, storageSize uint32, ptx *blobTxForPool) *blobTxMeta {
|
||||
func newBlobTxMeta(id uint64, storageSize uint32, ptx *BlobTxForPool) *blobTxMeta {
|
||||
meta := &blobTxMeta{
|
||||
hash: ptx.Tx.Hash(),
|
||||
vhashes: ptx.Tx.BlobHashes(),
|
||||
version: ptx.Version,
|
||||
id: id,
|
||||
storageSize: storageSize,
|
||||
size: size,
|
||||
nonce: ptx.Tx.Nonce(),
|
||||
costCap: uint256.MustFromBig(ptx.Tx.Cost()),
|
||||
execTipCap: uint256.MustFromBig(ptx.Tx.GasTipCap()),
|
||||
execFeeCap: uint256.MustFromBig(ptx.Tx.GasFeeCap()),
|
||||
blobFeeCap: uint256.MustFromBig(ptx.Tx.BlobGasFeeCap()),
|
||||
execGas: ptx.Tx.Gas(),
|
||||
blobGas: ptx.Tx.BlobGas(),
|
||||
hash: ptx.Tx.Hash(),
|
||||
vhashes: ptx.Tx.BlobHashes(),
|
||||
version: ptx.CellSidecar.Version,
|
||||
id: id,
|
||||
storageSize: storageSize,
|
||||
size: ptx.txSize(),
|
||||
sizeWithoutBlob: ptx.txSizeWithoutBlob(),
|
||||
nonce: ptx.Tx.Nonce(),
|
||||
costCap: uint256.MustFromBig(ptx.Tx.Cost()),
|
||||
execTipCap: uint256.MustFromBig(ptx.Tx.GasTipCap()),
|
||||
execFeeCap: uint256.MustFromBig(ptx.Tx.GasFeeCap()),
|
||||
blobFeeCap: uint256.MustFromBig(ptx.Tx.BlobGasFeeCap()),
|
||||
execGas: ptx.Tx.Gas(),
|
||||
blobGas: ptx.Tx.BlobGas(),
|
||||
custody: &ptx.CellSidecar.Custody,
|
||||
}
|
||||
meta.basefeeJumps = dynamicFeeJumps(meta.execFeeCap)
|
||||
meta.blobfeeJumps = dynamicBlobFeeJumps(meta.blobFeeCap)
|
||||
|
|
@ -465,8 +543,10 @@ type BlobPool struct {
|
|||
stored uint64 // Useful data size of all transactions on disk
|
||||
limbo *limbo // Persistent data store for the non-finalized blobs
|
||||
|
||||
gapped map[common.Address][]*types.Transaction // Transactions that are currently gapped (nonce too high)
|
||||
gappedSource map[common.Hash]common.Address // Source of gapped transactions to allow rechecking on inclusion
|
||||
cQueue *conversionQueue
|
||||
|
||||
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
|
||||
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(),
|
||||
index: make(map[common.Address][]*blobTxMeta),
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
|
@ -564,11 +644,14 @@ func (p *BlobPool) Init(gasTip uint64, head *types.Header, reserver txpool.Reser
|
|||
)
|
||||
index := func(id uint64, size uint32, blob []byte) {
|
||||
err := p.parseTransaction(id, size, blob)
|
||||
if err != nil {
|
||||
toDelete = append(toDelete, id)
|
||||
}
|
||||
// Transactions in legacy format will be queued for cell computation.
|
||||
// This entry will be swapped from the store after the conversion.
|
||||
if errors.Is(err, errLegacyTx) {
|
||||
convertTxs = append(convertTxs, id)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
toDelete = append(toDelete, id)
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
// Migrate legacy transactions (types.Transaction) to pooledBlobTx format.
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
p.cQueue = newConversionQueue()
|
||||
|
||||
if len(toDelete) > 0 {
|
||||
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
|
||||
// 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 {
|
||||
p.Close()
|
||||
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
|
||||
datacapGauge.Update(int64(p.config.Datacap))
|
||||
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
|
||||
}
|
||||
|
||||
// Close closes down the underlying persistent store.
|
||||
func (p *BlobPool) Close() 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 err := p.limbo.Close(); err != nil {
|
||||
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
|
||||
// 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 {
|
||||
var ptx blobTxForPool
|
||||
var ptx BlobTxForPool
|
||||
if err := rlp.DecodeBytes(blob, &ptx); err != nil {
|
||||
kind, content, _, splitErr := rlp.Split(blob)
|
||||
// check whether it is legacy tx type
|
||||
if splitErr == nil && kind == rlp.String && len(content) > 1 && content[0] == 3 {
|
||||
return errLegacyTx
|
||||
}
|
||||
log.Error("Failed to decode blob pool entry", "id", id, "err", err)
|
||||
return err
|
||||
}
|
||||
meta := newBlobTxMeta(id, ptx.TxSize(), size, &ptx)
|
||||
meta := newBlobTxMeta(id, size, &ptx)
|
||||
sender, err := types.Sender(p.signer, ptx.Tx)
|
||||
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 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.
|
||||
func (p *BlobPool) trackTransaction(meta *blobTxMeta, sender common.Address) error {
|
||||
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)
|
||||
return
|
||||
}
|
||||
var ptx blobTxForPool
|
||||
var ptx BlobTxForPool
|
||||
if err := rlp.DecodeBytes(data, &ptx); err != nil {
|
||||
log.Error("Blobs corrupted for included transaction", "from", addr, "nonce", nonce, "id", id, "err", err)
|
||||
return
|
||||
|
|
@ -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)
|
||||
continue
|
||||
}
|
||||
var ptx blobTxForPool
|
||||
var ptx BlobTxForPool
|
||||
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)
|
||||
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
|
||||
// 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()
|
||||
txID, exists := p.lookup.storeidOfBlob(vhash)
|
||||
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)
|
||||
return nil
|
||||
}
|
||||
var ptx blobTxForPool
|
||||
var ptx BlobTxForPool
|
||||
if err := rlp.DecodeBytes(data, &ptx); err != nil {
|
||||
log.Error("Blobs corrupted for tracked transaction", "id", txID, "err", err)
|
||||
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
|
||||
// it on the floor instead of putting it back.
|
||||
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)
|
||||
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)
|
||||
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 err := p.reserver.Hold(addr); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// getRLP returns the raw RLP-encoded pooledBlobTx data from the store.
|
||||
func (p *BlobPool) getRLP(hash common.Hash) []byte {
|
||||
// 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.
|
||||
|
|
@ -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.
|
||||
// Note that this function always try to recover full blobs
|
||||
func (p *BlobPool) Get(hash common.Hash) *types.Transaction {
|
||||
data := p.getRLP(hash)
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
var ptx blobTxForPool
|
||||
var ptx BlobTxForPool
|
||||
if err := rlp.DecodeBytes(data, &ptx); err != nil {
|
||||
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 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.
|
||||
// It converts the pool's internal type to the RLP format used by the eth protocol:
|
||||
// e.g. type_byte || [..., version, [blobs], [comms], [proofs]]
|
||||
func (p *BlobPool) GetRLP(hash common.Hash) []byte {
|
||||
// GetRLP returns an RLP-encoded transaction if it is contained in the pool.
|
||||
func (p *BlobPool) GetRLP(hash common.Hash, version uint) []byte {
|
||||
data := p.getRLP(hash)
|
||||
if len(data) == 0 {
|
||||
// Not in this pool, do not log.
|
||||
return nil
|
||||
}
|
||||
rlp, err := encodeForNetwork(data)
|
||||
rlp, err := encodeForNetwork(data, version)
|
||||
if err != nil {
|
||||
log.Error("Failed to encode pooled tx into the network type", "hash", hash, "err", err)
|
||||
return nil
|
||||
|
|
@ -1611,13 +1764,14 @@ func (p *BlobPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
|
|||
p.lock.RLock()
|
||||
defer p.lock.RUnlock()
|
||||
|
||||
size, ok := p.lookup.sizeOfTx(hash)
|
||||
meta, ok := p.lookup.txIndex[hash]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &txpool.TxMetadata{
|
||||
Type: types.BlobTxType,
|
||||
Size: size,
|
||||
Type: types.BlobTxType,
|
||||
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
|
||||
var ptx blobTxForPool
|
||||
var ptx BlobTxForPool
|
||||
if err := rlp.DecodeBytes(data, &ptx); err != nil {
|
||||
log.Error("Blobs corrupted for traced transaction", "id", txID, "err", err)
|
||||
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
|
||||
for i, hash := range ptx.Tx.BlobHashes() {
|
||||
list, ok := indices[hash]
|
||||
|
|
@ -1699,6 +1857,90 @@ func (p *BlobPool) getBlobs(vhashes []common.Hash, version byte) (_ []*kzg4844.B
|
|||
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.
|
||||
func (p *BlobPool) availableBlobs(vhashes []common.Hash) []bool {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
// add inserts a new blob transaction into the pool if it passes validation (both
|
||||
// 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
|
||||
// only even pulled from the network, so this method will act as the overload
|
||||
// protection for fetches.
|
||||
|
|
@ -1738,13 +1985,15 @@ func (p *BlobPool) add(tx *types.Transaction) (err error) {
|
|||
addtimeHist.Update(time.Since(start).Nanoseconds())
|
||||
}(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
|
||||
// consensus validity and pool restrictions). It must be called with the pool lock held.
|
||||
// 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
|
||||
if err := p.validateTx(tx); err != nil {
|
||||
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)
|
||||
allowance := p.gappedAllowance(from)
|
||||
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
|
||||
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]))
|
||||
|
|
@ -1785,6 +2034,10 @@ func (p *BlobPool) addLocked(tx *types.Transaction, checkGapped bool) (err error
|
|||
}
|
||||
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
|
||||
// only by this subpool until all transactions are evicted
|
||||
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,
|
||||
// insert it into the database and update the indices
|
||||
ptx := newBlobTxForPool(tx)
|
||||
blob, err := rlp.EncodeToBytes(ptx)
|
||||
if err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
meta := newBlobTxMeta(id, tx.Size(), p.store.Size(id), ptx)
|
||||
meta := newBlobTxMeta(id, p.store.Size(id), ptx)
|
||||
|
||||
var (
|
||||
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
|
||||
// where transactions are replaced, keeping the original receive order for same nonce
|
||||
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 {
|
||||
stateNonce := p.state.GetNonce(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
|
||||
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)
|
||||
// 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.
|
||||
tx := gtxs[0]
|
||||
ptx := gtxs[0]
|
||||
gtxs[0] = nil
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
// We do not recurse here, but continue to loop instead.
|
||||
// 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)
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
// 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
|
||||
lazies = append(lazies, &txpool.LazyTransaction{
|
||||
Pool: p,
|
||||
|
|
@ -2229,10 +2485,10 @@ func (p *BlobPool) evictGapped() {
|
|||
// and we overwrite the slice for this account after filtering.
|
||||
keep := txs[:0]
|
||||
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
|
||||
// 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
|
||||
} else {
|
||||
keep = append(keep, gtx)
|
||||
|
|
@ -2355,7 +2611,7 @@ func (p *BlobPool) Clear() {
|
|||
|
||||
// Reset counters and the gapped buffer
|
||||
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)
|
||||
|
||||
var (
|
||||
|
|
@ -2364,3 +2620,13 @@ func (p *BlobPool) Clear() {
|
|||
)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import (
|
|||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"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.
|
||||
func encodeForPool(tx *types.Transaction) []byte {
|
||||
blob, _ := rlp.EncodeToBytes(newBlobTxForPool(tx))
|
||||
ptx, _ := newBlobTxForPool(tx)
|
||||
blob, _ := rlp.EncodeToBytes(ptx)
|
||||
return blob
|
||||
}
|
||||
|
||||
|
|
@ -267,6 +269,15 @@ func makeMultiBlobTx(nonce uint64, gasTipCap uint64, gasFeeCap uint64, blobFeeCa
|
|||
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
|
||||
// without signing it.
|
||||
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
|
||||
// - 9. Duplicate nonces from the same account must be dropped
|
||||
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
|
||||
storage := t.TempDir()
|
||||
|
|
@ -498,75 +509,76 @@ func TestOpenDrops(t *testing.T) {
|
|||
S: new(uint256.Int),
|
||||
})
|
||||
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
|
||||
// that anything gapped will get evicted (case 3).
|
||||
var (
|
||||
gapper, _ = crypto.GenerateKey()
|
||||
|
||||
valids = make(map[uint64]struct{})
|
||||
gapped = make(map[uint64]struct{})
|
||||
valids = make(map[common.Hash]struct{})
|
||||
gapped = make(map[common.Hash]struct{})
|
||||
)
|
||||
for _, nonce := range []uint64{0, 1, 3, 4, 6, 7} { // first gap at #2, another at #5
|
||||
tx := makeTx(nonce, 1, 1, 1, gapper)
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
store.Put(blob)
|
||||
if nonce < 2 {
|
||||
valids[id] = struct{}{}
|
||||
valids[tx.Hash()] = struct{}{}
|
||||
} else {
|
||||
gapped[id] = struct{}{}
|
||||
gapped[tx.Hash()] = struct{}{}
|
||||
}
|
||||
}
|
||||
// Insert a sequence of transactions with a gapped starting nonce to verify
|
||||
// that the entire set will get dropped (case 3).
|
||||
var (
|
||||
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
|
||||
tx := makeTx(nonce, 1, 1, 1, dangler)
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
dangling[id] = struct{}{}
|
||||
store.Put(blob)
|
||||
dangling[tx.Hash()] = struct{}{}
|
||||
}
|
||||
// Insert a sequence of transactions with already passed nonces to verify
|
||||
// that the entire set will get dropped (case 4).
|
||||
var (
|
||||
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
|
||||
tx := makeTx(nonce, 1, 1, 1, filler)
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
filled[id] = struct{}{}
|
||||
store.Put(blob)
|
||||
filled[tx.Hash()] = struct{}{}
|
||||
}
|
||||
// Insert a sequence of transactions with partially passed nonces to verify
|
||||
// that the included part of the set will get dropped (case 4).
|
||||
var (
|
||||
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
|
||||
tx := makeTx(nonce, 1, 1, 1, overlapper)
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
store.Put(blob)
|
||||
if nonce >= 2 {
|
||||
valids[id] = struct{}{}
|
||||
valids[tx.Hash()] = struct{}{}
|
||||
} else {
|
||||
overlapped[id] = struct{}{}
|
||||
overlapped[tx.Hash()] = struct{}{}
|
||||
}
|
||||
}
|
||||
// Insert a sequence of transactions with an underpriced first to verify that
|
||||
// the entire set will get dropped (case 5).
|
||||
var (
|
||||
underpayer, _ = crypto.GenerateKey()
|
||||
underpaid = make(map[uint64]struct{})
|
||||
underpaid = make(map[common.Hash]struct{})
|
||||
)
|
||||
for i := 0; i < 5; i++ { // make #0 underpriced
|
||||
var tx *types.Transaction
|
||||
|
|
@ -577,15 +589,15 @@ func TestOpenDrops(t *testing.T) {
|
|||
}
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
underpaid[id] = struct{}{}
|
||||
store.Put(blob)
|
||||
underpaid[tx.Hash()] = struct{}{}
|
||||
}
|
||||
|
||||
// Insert a sequence of transactions with an underpriced in between to verify
|
||||
// that it and anything newly gapped will get evicted (case 5).
|
||||
var (
|
||||
outpricer, _ = crypto.GenerateKey()
|
||||
outpriced = make(map[uint64]struct{})
|
||||
outpriced = make(map[common.Hash]struct{})
|
||||
)
|
||||
for i := 0; i < 5; i++ { // make #2 underpriced
|
||||
var tx *types.Transaction
|
||||
|
|
@ -596,18 +608,18 @@ func TestOpenDrops(t *testing.T) {
|
|||
}
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
store.Put(blob)
|
||||
if i < 2 {
|
||||
valids[id] = struct{}{}
|
||||
valids[tx.Hash()] = struct{}{}
|
||||
} else {
|
||||
outpriced[id] = struct{}{}
|
||||
outpriced[tx.Hash()] = struct{}{}
|
||||
}
|
||||
}
|
||||
// Insert a sequence of transactions fully overdrafted to verify that the
|
||||
// entire set will get invalidated (case 6).
|
||||
var (
|
||||
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
|
||||
var tx *types.Transaction
|
||||
|
|
@ -618,14 +630,14 @@ func TestOpenDrops(t *testing.T) {
|
|||
}
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
exceeded[id] = struct{}{}
|
||||
store.Put(blob)
|
||||
exceeded[tx.Hash()] = struct{}{}
|
||||
}
|
||||
// Insert a sequence of transactions partially overdrafted to verify that part
|
||||
// of the set will get invalidated (case 6).
|
||||
var (
|
||||
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
|
||||
var tx *types.Transaction
|
||||
|
|
@ -636,44 +648,46 @@ func TestOpenDrops(t *testing.T) {
|
|||
}
|
||||
blob := encodeForPool(tx)
|
||||
|
||||
id, _ := store.Put(blob)
|
||||
store.Put(blob)
|
||||
if nonce < 1 {
|
||||
valids[id] = struct{}{}
|
||||
valids[tx.Hash()] = struct{}{}
|
||||
} else {
|
||||
overdrafted[id] = struct{}{}
|
||||
overdrafted[tx.Hash()] = struct{}{}
|
||||
}
|
||||
}
|
||||
// Insert a sequence of transactions overflowing the account cap to verify
|
||||
// that part of the set will get invalidated (case 7).
|
||||
var (
|
||||
overcapper, _ = crypto.GenerateKey()
|
||||
overcapped = make(map[uint64]struct{})
|
||||
overcapped = make(map[common.Hash]struct{})
|
||||
)
|
||||
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 {
|
||||
valids[id] = struct{}{}
|
||||
valids[tx.Hash()] = struct{}{}
|
||||
} else {
|
||||
overcapped[id] = struct{}{}
|
||||
overcapped[tx.Hash()] = struct{}{}
|
||||
}
|
||||
}
|
||||
// Insert a batch of duplicated transactions to verify that only one of each
|
||||
// version will remain (case 8).
|
||||
var (
|
||||
duplicater, _ = crypto.GenerateKey()
|
||||
duplicated = make(map[uint64]struct{})
|
||||
duplicated = make(map[common.Hash]struct{})
|
||||
)
|
||||
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++ {
|
||||
id, _ := store.Put(blob)
|
||||
store.Put(blob)
|
||||
if i == 0 {
|
||||
valids[id] = struct{}{}
|
||||
valids[tx.Hash()] = struct{}{}
|
||||
} else {
|
||||
duplicated[id] = struct{}{}
|
||||
duplicated[tx.Hash()] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -681,17 +695,18 @@ func TestOpenDrops(t *testing.T) {
|
|||
// remain (case 9).
|
||||
var (
|
||||
repeater, _ = crypto.GenerateKey()
|
||||
repeated = make(map[uint64]struct{})
|
||||
repeated = make(map[common.Hash]struct{})
|
||||
)
|
||||
for _, nonce := range []uint64{0, 1, 2} {
|
||||
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 {
|
||||
valids[id] = struct{}{}
|
||||
valids[tx.Hash()] = struct{}{}
|
||||
} 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
|
||||
// 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 _, tx := range txs {
|
||||
switch tx.id {
|
||||
case malformed:
|
||||
t.Errorf("malformed RLP transaction remained in storage")
|
||||
case badsig:
|
||||
t.Errorf("invalidly signed transaction remained in storage")
|
||||
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)
|
||||
} else if _, ok := filled[tx.id]; ok {
|
||||
} else if _, ok := filled[tx.hash]; ok {
|
||||
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)
|
||||
} else if _, ok := gapped[tx.id]; ok {
|
||||
} else if _, ok := gapped[tx.hash]; ok {
|
||||
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)
|
||||
} else if _, ok := outpriced[tx.id]; ok {
|
||||
} else if _, ok := outpriced[tx.hash]; ok {
|
||||
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)
|
||||
} 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)
|
||||
} else if _, ok := overcapped[tx.id]; ok {
|
||||
} else if _, ok := overcapped[tx.hash]; ok {
|
||||
t.Errorf("overcapped transaction remained in storage: %d", tx.id)
|
||||
} else if _, ok := duplicated[tx.id]; ok {
|
||||
t.Errorf("duplicated transaction remained in storage: %d", tx.id)
|
||||
} else if _, ok := repeated[tx.id]; ok {
|
||||
} else if _, ok := repeated[tx.hash]; ok {
|
||||
t.Errorf("repeated nonce transaction remained in storage: %d", tx.id)
|
||||
} 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) {
|
||||
t.Errorf("valid transaction count mismatch: have %d, want %d", len(alive), len(valids))
|
||||
}
|
||||
for id := range alive {
|
||||
if _, ok := valids[id]; !ok {
|
||||
t.Errorf("extra transaction %d", id)
|
||||
for hash := range alive {
|
||||
if _, ok := valids[hash]; !ok {
|
||||
t.Errorf("extra transaction %s", hash)
|
||||
}
|
||||
}
|
||||
for id := range valids {
|
||||
if _, ok := alive[id]; !ok {
|
||||
t.Errorf("missing transaction %d", id)
|
||||
for hash := range valids {
|
||||
if _, ok := alive[hash]; !ok {
|
||||
t.Errorf("missing transaction %s", hash)
|
||||
}
|
||||
}
|
||||
// Verify all the calculated pool internals. Interestingly, this is **not**
|
||||
|
|
@ -995,7 +1012,10 @@ func TestOpenCap(t *testing.T) {
|
|||
|
||||
keep = []common.Address{addr1, addr3}
|
||||
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(blob2)
|
||||
|
|
@ -1004,7 +1024,7 @@ func TestOpenCap(t *testing.T) {
|
|||
|
||||
// Verify pool capping twice: first by reducing the data cap, then restarting
|
||||
// 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
|
||||
statedb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting())
|
||||
statedb.AddBalance(addr1, uint256.NewInt(1_000_000_000), tracing.BalanceChangeUnspecified)
|
||||
|
|
@ -1315,6 +1335,14 @@ func TestLegacyTxConversion(t *testing.T) {
|
|||
}
|
||||
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.
|
||||
for _, want := range []*types.Transaction{tx1, tx2} {
|
||||
got := pool.Get(want.Hash())
|
||||
|
|
@ -1331,7 +1359,7 @@ func TestLegacyTxConversion(t *testing.T) {
|
|||
|
||||
// Legacy formats should not exist on pool.store
|
||||
pool.store.Iterate(func(id uint64, size uint32, blob []byte) {
|
||||
var ptx blobTxForPool
|
||||
var ptx BlobTxForPool
|
||||
if err := rlp.DecodeBytes(blob, &ptx); err != nil {
|
||||
t.Errorf("entry %d not in new blobTxForPool format: %v", id, err)
|
||||
}
|
||||
|
|
@ -1340,6 +1368,75 @@ func TestLegacyTxConversion(t *testing.T) {
|
|||
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.
|
||||
func TestBlobCountLimit(t *testing.T) {
|
||||
var (
|
||||
|
|
@ -1389,7 +1486,7 @@ func TestBlobCountLimit(t *testing.T) {
|
|||
|
||||
// Check that first succeeds second fails.
|
||||
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) {
|
||||
t.Fatalf("expected tx with 8 blobs to fail, got: %v", errs[1])
|
||||
|
|
@ -2108,23 +2205,40 @@ func TestGetBlobs(t *testing.T) {
|
|||
pool.Close()
|
||||
}
|
||||
|
||||
// TestEncodeForNetwork verifies that encodeForNetwork produces output identical
|
||||
// to rlp.EncodeToBytes on the original transaction, for both V0 and V1 sidecars.
|
||||
// TestEncodeForNetwork verifies that encodeForNetwork produces the correct wire
|
||||
// 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) {
|
||||
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()
|
||||
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 {
|
||||
t.Fatalf("failed to encode tx: %v", err)
|
||||
}
|
||||
storedRLP := encodeForPool(tx)
|
||||
|
||||
gotRLP, err := encodeForNetwork(storedRLP)
|
||||
gotRLP, err := encodeForNetwork(storedRLP, ethVer)
|
||||
if err != nil {
|
||||
t.Fatalf("encodeForNetwork failed: %v", err)
|
||||
}
|
||||
|
|
@ -2195,7 +2309,8 @@ func benchmarkPoolPending(b *testing.B, datacap uint64) {
|
|||
b.Fatal(err)
|
||||
}
|
||||
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)
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
317
core/txpool/blobpool/buffer.go
Normal file
317
core/txpool/blobpool/buffer.go
Normal 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
|
||||
}
|
||||
206
core/txpool/blobpool/buffer_test.go
Normal file
206
core/txpool/blobpool/buffer_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -36,7 +36,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
topKTimeout = 4 * time.Second
|
||||
topKTimeout = 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
|
||||
// we failed to predict) can be calculated with the telemetry span
|
||||
// (blobs.filled - cache.hit).
|
||||
cacheHitMeter = metrics.NewRegisteredMeter("blobpool/cache/hit", nil)
|
||||
cacheMissMeter = metrics.NewRegisteredMeter("blobpool/cache/miss", nil)
|
||||
cacheBlobsGauge = metrics.NewRegisteredGauge("blobpool/cache/blobs", nil)
|
||||
cacheHitMeter = metrics.NewRegisteredMeter("blobpool/cache/hit", nil)
|
||||
cacheMissMeter = metrics.NewRegisteredMeter("blobpool/cache/miss", nil)
|
||||
cacheEntriesGauge = metrics.NewRegisteredGauge("blobpool/cache/entries", nil)
|
||||
)
|
||||
|
||||
type cachedBlob struct {
|
||||
cell []kzg4844.Cell
|
||||
custody types.CustodyBitmap
|
||||
blob *kzg4844.Blob
|
||||
commitment kzg4844.Commitment
|
||||
proofs []kzg4844.Proof
|
||||
|
|
@ -74,11 +76,16 @@ type Cache struct {
|
|||
mu sync.Mutex
|
||||
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
|
||||
quit chan struct{}
|
||||
topkRequest chan struct{}
|
||||
topkTimer mclock.Timer
|
||||
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
|
||||
|
||||
|
|
@ -103,6 +110,7 @@ func newCache(p *BlobPool, clock mclock.Clock, step func()) *Cache {
|
|||
step: step,
|
||||
quit: make(chan struct{}),
|
||||
topkRequest: make(chan struct{}, 1),
|
||||
cellModeCh: make(chan bool, 1),
|
||||
}
|
||||
|
||||
c.wg.Add(1)
|
||||
|
|
@ -197,13 +205,10 @@ func (c *Cache) GetBlobs(ctx context.Context, vhashes []common.Hash, version byt
|
|||
c.mu.Lock()
|
||||
for vhash, idxs := range indices {
|
||||
n := len(idxs)
|
||||
|
||||
cached := c.entries[vhash]
|
||||
if cached == nil || cached.version != version {
|
||||
cached, ok := c.entries[vhash]
|
||||
if !ok || cached.version != version || cached.blob == nil {
|
||||
cacheMiss += n
|
||||
if cached == nil {
|
||||
misses = append(misses, vhash)
|
||||
}
|
||||
misses = append(misses, vhash)
|
||||
continue
|
||||
}
|
||||
cacheHits += n
|
||||
|
|
@ -241,6 +246,82 @@ func (c *Cache) GetBlobs(ctx context.Context, vhashes []common.Hash, version byt
|
|||
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() {
|
||||
defer c.wg.Done()
|
||||
|
||||
|
|
@ -258,6 +339,14 @@ func (c *Cache) loop() {
|
|||
c.update(want)
|
||||
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:
|
||||
c.cancelUpdate()
|
||||
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
|
||||
// background.
|
||||
func (c *Cache) update(want []common.Hash) {
|
||||
cellMode := c.needCell
|
||||
|
||||
wantSet := make(map[common.Hash]struct{}, len(want))
|
||||
for _, vh := range want {
|
||||
wantSet[vh] = struct{}{}
|
||||
|
|
@ -298,7 +389,13 @@ func (c *Cache) update(want []common.Hash) {
|
|||
c.mu.Lock()
|
||||
var missing []common.Hash
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -307,7 +404,7 @@ func (c *Cache) update(want []common.Hash) {
|
|||
continue
|
||||
}
|
||||
delete(c.entries, vh)
|
||||
cacheBlobsGauge.Dec(1)
|
||||
cacheEntriesGauge.Dec(1)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
|
|
@ -330,37 +427,92 @@ func (c *Cache) update(want []common.Hash) {
|
|||
if ptx == nil {
|
||||
continue
|
||||
}
|
||||
sidecar := ptx.Sidecar()
|
||||
if sidecar == nil {
|
||||
continue
|
||||
if cellMode {
|
||||
c.loadCells(ptx, wantSet)
|
||||
} 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
|
||||
// transactions, up to the active fork's maxBlobsPerBlock.
|
||||
func (c *Cache) selectTopTxs() []common.Hash {
|
||||
|
|
@ -373,8 +525,9 @@ func (c *Cache) selectTopTxs() []common.Hash {
|
|||
baseFee := eip1559.CalcBaseFee(config, head)
|
||||
|
||||
filter := txpool.PendingFilter{
|
||||
BlobTxs: true,
|
||||
BaseFee: uint256.MustFromBig(baseFee),
|
||||
BlobTxs: true,
|
||||
BaseFee: uint256.MustFromBig(baseFee),
|
||||
PartialCells: c.needCell,
|
||||
}
|
||||
if head.ExcessBlobGas != nil {
|
||||
filter.BlobFee = uint256.MustFromBig(eip4844.CalcBlobFee(config, head))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
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()
|
||||
defer tc.blobpool.lock.Unlock()
|
||||
|
|
@ -155,7 +158,7 @@ func (tc *testCache) inject(t *testing.T, spec txSpec) []common.Hash {
|
|||
if err != nil {
|
||||
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)
|
||||
tc.blobpool.index[addr] = append(tc.blobpool.index[addr], meta)
|
||||
tc.blobpool.lookup.track(meta)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ type Config struct {
|
|||
Datadir string // Data directory containing the currently executable blobs
|
||||
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
|
||||
|
||||
FetchProbability uint64 // EIP-8070: full blob fetch probability for sparse blobpool
|
||||
}
|
||||
|
||||
// DefaultConfig contains the default configurations for the transaction pool.
|
||||
|
|
|
|||
207
core/txpool/blobpool/conversion.go
Normal file
207
core/txpool/blobpool/conversion.go
Normal 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
|
||||
}
|
||||
114
core/txpool/blobpool/conversion_test.go
Normal file
114
core/txpool/blobpool/conversion_test.go
Normal 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()
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ import (
|
|||
type limboBlob struct {
|
||||
TxHash common.Hash // Owner transaction's hash to support resurrecting reorged txs
|
||||
Block uint64 // Block in which the blob transaction was included
|
||||
Ptx *blobTxForPool
|
||||
Ptx *BlobTxForPool
|
||||
}
|
||||
|
||||
// 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.
|
||||
func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) {
|
||||
func newLimbo(config *params.ChainConfig, datadir string) (*limbo, []uint64, error) {
|
||||
l := &limbo{
|
||||
index: make(map[common.Hash]uint64),
|
||||
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.
|
||||
slotter, err := tryMigrate(config, slotter, datadir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
store, err := billy.Open(billy.Options{Path: datadir, Repair: true}, slotter, index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
l.store = store
|
||||
|
||||
|
|
@ -82,11 +90,11 @@ func newLimbo(config *params.ChainConfig, datadir string) (*limbo, error) {
|
|||
for _, id := range fails {
|
||||
if err := l.store.Delete(id); err != nil {
|
||||
l.Close()
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return l, nil
|
||||
return l, convert, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
item := new(limboBlob)
|
||||
if err := rlp.DecodeBytes(data, item); err != nil {
|
||||
if isLegacyLimboBlob(data) {
|
||||
return errLegacyTx
|
||||
}
|
||||
// This path is impossible unless the disk data representation changes
|
||||
// across restarts. For that ever improbable case, recover gracefully
|
||||
// by ignoring this data entry.
|
||||
|
|
@ -122,6 +133,17 @@ func (l *limbo) parseBlob(id uint64, data []byte) error {
|
|||
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.
|
||||
func (l *limbo) finalize(final *types.Header) {
|
||||
// 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
|
||||
// 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()
|
||||
if _, ok := l.index[hash]; ok {
|
||||
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
|
||||
// it at the same time. This method should be used when a previously included blob
|
||||
// transaction gets reorged out.
|
||||
func (l *limbo) pull(tx common.Hash) (*blobTxForPool, error) {
|
||||
func (l *limbo) pull(tx common.Hash) (*BlobTxForPool, error) {
|
||||
// If the blobs are not tracked by the limbo, there's not much to do. This
|
||||
// can happen for example if a blob transaction is mined without pushing it
|
||||
// into the network first.
|
||||
|
|
@ -239,7 +263,7 @@ func (l *limbo) getAndDrop(id uint64) (*limboBlob, error) {
|
|||
|
||||
// setAndIndex assembles a limbo blob database entry and stores it, also updating
|
||||
// the in-memory indices.
|
||||
func (l *limbo) setAndIndex(ptx *blobTxForPool, block uint64) error {
|
||||
func (l *limbo) setAndIndex(ptx *BlobTxForPool, block uint64) error {
|
||||
txhash := ptx.Tx.Hash()
|
||||
item := &limboBlob{
|
||||
TxHash: txhash,
|
||||
|
|
|
|||
|
|
@ -18,11 +18,15 @@ package blobpool
|
|||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
)
|
||||
|
||||
type txMetadata struct {
|
||||
id uint64 // the billy id of transaction
|
||||
size uint64 // the RLP encoded size of transaction (blobs are included)
|
||||
id uint64 // the billy id of transaction
|
||||
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,
|
||||
|
|
@ -56,6 +60,15 @@ func (l *lookup) storeidOfTx(txhash common.Hash) (uint64, bool) {
|
|||
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.
|
||||
func (l *lookup) storeidOfBlob(vhash common.Hash) (uint64, bool) {
|
||||
// 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
|
||||
l.txIndex[tx.hash] = &txMetadata{
|
||||
id: tx.id,
|
||||
size: tx.size,
|
||||
id: tx.id,
|
||||
size: tx.size,
|
||||
sizeWithoutBlob: tx.sizeWithoutBlob,
|
||||
custody: *tx.custody,
|
||||
vhashes: tx.vhashes,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,4 +74,7 @@ var (
|
|||
|
||||
// ErrKZGVerificationError is returned when a KZG proof was not verified correctly.
|
||||
ErrKZGVerificationError = errors.New("KZG verification error")
|
||||
|
||||
// ErrSidecarFormatError is returned when sidecar is malformed
|
||||
ErrSidecarFormatError = errors.New("Wrong sidecar format")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
func (pool *LegacyPool) GetRLP(hash common.Hash) []byte {
|
||||
func (pool *LegacyPool) GetRLP(hash common.Hash, _ uint) []byte {
|
||||
tx := pool.all.Get(hash)
|
||||
if tx == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -80,14 +80,16 @@ type PendingFilter struct {
|
|||
|
||||
// When BlobTxs true, return only blob transactions (block blob-space filling)
|
||||
// when false, return only non-blob txs (peer-join announces, block space filling)
|
||||
BlobTxs bool
|
||||
BlobVersion byte // Blob tx version to include. 0 means pre-Osaka, 1 means Osaka and later
|
||||
BlobTxs bool
|
||||
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.
|
||||
type TxMetadata struct {
|
||||
Type uint8 // The type of the transaction
|
||||
Size uint64 // The length of the 'rlp encoding' of a transaction
|
||||
Type uint8 // The type of the 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.
|
||||
|
|
@ -132,7 +134,7 @@ type SubPool interface {
|
|||
Get(hash common.Hash) *types.Transaction
|
||||
|
||||
// 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
|
||||
// given transaction hash.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
func (p *TxPool) GetRLP(hash common.Hash) []byte {
|
||||
func (p *TxPool) GetRLP(hash common.Hash, version uint) []byte {
|
||||
for _, subpool := range p.subpools {
|
||||
encoded := subpool.GetRLP(hash)
|
||||
encoded := subpool.GetRLP(hash, version)
|
||||
if len(encoded) != 0 {
|
||||
return encoded
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,9 +65,6 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
|
|||
if opts.Accept&(1<<tx.Type()) == 0 {
|
||||
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
|
||||
// smaller than the maximum limit the pool can meaningfully handle
|
||||
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)
|
||||
}
|
||||
if tx.Type() == types.BlobTxType {
|
||||
return validateBlobTx(tx)
|
||||
return validateBlobSidecar(tx, head, opts)
|
||||
}
|
||||
if tx.Type() == types.SetCodeTxType {
|
||||
if len(tx.SetCodeAuthorizations()) == 0 {
|
||||
|
|
@ -171,42 +168,64 @@ func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types
|
|||
return nil
|
||||
}
|
||||
|
||||
// validateBlobTx implements the blob-transaction specific validations.
|
||||
func validateBlobTx(tx *types.Transaction) error {
|
||||
// validateBlobSidecar implements the blob sidecar validation.
|
||||
// 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()
|
||||
if sidecar == nil {
|
||||
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()
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return validateBlobSidecarOsaka(sidecar, hashes)
|
||||
}
|
||||
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 len(hashes) > opts.MaxBlobCount {
|
||||
return fmt.Errorf("%w: blob count %v, limit %v", ErrTxBlobLimitExceeded, len(hashes), opts.MaxBlobCount)
|
||||
}
|
||||
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 nil
|
||||
|
|
|
|||
152
core/types/custody_bitmap.go
Normal file
152
core/types/custody_bitmap.go
Normal 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
|
||||
}
|
||||
|
|
@ -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.
|
||||
// This is an interface because sidecars are versioned.
|
||||
type blobTxWithBlobs interface {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/vm"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"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/protocols/eth"
|
||||
"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
|
||||
cacheLimit := options.TrieCleanLimit + options.TrieDirtyLimit + options.SnapshotLimit
|
||||
if eth.handler, err = newHandler(&handlerConfig{
|
||||
NodeID: eth.p2pServer.Self().ID(),
|
||||
Database: chainDb,
|
||||
Chain: eth.blockchain,
|
||||
TxPool: eth.txPool,
|
||||
Network: networkID,
|
||||
Sync: config.SyncMode,
|
||||
BloomCache: uint64(cacheLimit),
|
||||
RequiredBlocks: config.RequiredBlocks,
|
||||
SnapV2: config.SnapV2,
|
||||
NodeID: eth.p2pServer.Self().ID(),
|
||||
Database: chainDb,
|
||||
Chain: eth.blockchain,
|
||||
TxPool: eth.txPool,
|
||||
BlobPool: eth.blobTxPool,
|
||||
Network: networkID,
|
||||
Sync: config.SyncMode,
|
||||
BloomCache: uint64(cacheLimit),
|
||||
RequiredBlocks: config.RequiredBlocks,
|
||||
SnapV2: config.SnapV2,
|
||||
FetchProbability: config.BlobPool.FetchProbability,
|
||||
}); err != nil {
|
||||
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) TxPool() *txpool.TxPool { return s.txPool }
|
||||
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) Engine() consensus.Engine { return s.engine }
|
||||
func (s *Ethereum) ChainDb() ethdb.Database { return s.chainDb }
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"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
|
||||
// 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 {
|
||||
switch {
|
||||
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")
|
||||
}
|
||||
}
|
||||
if custodyColumns != nil {
|
||||
api.eth.BlobFetcher().UpdateCustody(*custodyColumns)
|
||||
}
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (api *ConsensusAPI) HasBlobs(hashes []common.Hash) []bool {
|
||||
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.
|
||||
func (api *ConsensusAPI) ExchangeCapabilities([]string) []string {
|
||||
func (api *ConsensusAPI) ExchangeCapabilities(caps []string) []string {
|
||||
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++ {
|
||||
name := []rune(valueT.Method(i).Name)
|
||||
if string(name) == "ExchangeCapabilities" {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/consensus/beacon"
|
||||
"github.com/ethereum/go-ethereum/consensus/ethash"
|
||||
"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/crypto"
|
||||
"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
|
||||
// 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 (
|
||||
blobs []kzg4844.Blob
|
||||
blobHashes []common.Hash
|
||||
commitments []kzg4844.Commitment
|
||||
proofs []kzg4844.Proof
|
||||
cells []kzg4844.Cell
|
||||
)
|
||||
for i := 0; i < blobCount; i++ {
|
||||
blobs = append(blobs, *testBlobs[blobOffset+i])
|
||||
commitments = append(commitments, testBlobCommits[blobOffset+i])
|
||||
j := blobOffset + i
|
||||
blobHashes = append(blobHashes, testBlobVHashes[j])
|
||||
commitments = append(commitments, testBlobCommits[j])
|
||||
if version == types.BlobSidecarVersion0 {
|
||||
proofs = append(proofs, testBlobProofs[blobOffset+i])
|
||||
proofs = append(proofs, testBlobProofs[j])
|
||||
} else {
|
||||
cellProofs, _ := kzg4844.ComputeCellProofs(testBlobs[blobOffset+i])
|
||||
proofs = append(proofs, cellProofs...)
|
||||
proofs = append(proofs, testBlobCellProofs[j]...)
|
||||
}
|
||||
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{
|
||||
ChainID: uint256.MustFromBig(chainConfig.ChainID),
|
||||
|
|
@ -1874,12 +1879,20 @@ func makeMultiBlobTx(chainConfig *params.ChainConfig, nonce uint64, blobCount in
|
|||
BlobFeeCap: uint256.NewInt(1000),
|
||||
BlobHashes: blobHashes,
|
||||
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 (
|
||||
// Create a database pre-initialize with a genesis block
|
||||
config = *params.MergedTestChainConfig
|
||||
|
|
@ -1908,17 +1921,23 @@ func newGetBlobEnv(t testing.TB, version byte) (*node.Node, *ConsensusAPI) {
|
|||
}
|
||||
n, ethServ := startEthService(t, gspec, nil)
|
||||
|
||||
// fill blob txs into the pool
|
||||
tx1 := makeMultiBlobTx(&config, 0, 2, 0, key1, version) // blob[0, 2)
|
||||
tx2 := makeMultiBlobTx(&config, 0, 2, 2, key2, version) // blob[2, 4)
|
||||
tx3 := makeMultiBlobTx(&config, 0, 2, 4, key3, version) // blob[4, 6)
|
||||
ethServ.TxPool().Add([]*types.Transaction{tx1, tx2, tx3}, true)
|
||||
// fill blob txs into the pool, each holding only the given custody cells
|
||||
txs := []*blobpool.BlobTxForPool{
|
||||
makeMultiBlobTx(&config, 0, 2, 0, key1, version, custody), // blob[0, 2)
|
||||
makeMultiBlobTx(&config, 0, 2, 2, key2, version, custody), // blob[2, 4)
|
||||
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)
|
||||
return n, api
|
||||
}
|
||||
func TestGetBlobsV2And3(t *testing.T) {
|
||||
n, api := newGetBlobEnv(t, 1)
|
||||
n, api := newGetBlobEnv(t, 1, types.CustodyBitmapAll)
|
||||
defer n.Close()
|
||||
|
||||
suites := []struct {
|
||||
|
|
@ -1954,7 +1973,7 @@ func TestGetBlobsV2And3(t *testing.T) {
|
|||
// Benchmark GetBlobsV2 internals
|
||||
// Note that this is not an RPC-level benchmark, so JSON-RPC overhead is not included.
|
||||
func BenchmarkGetBlobsV2(b *testing.B) {
|
||||
n, api := newGetBlobEnv(b, 1)
|
||||
n, api := newGetBlobEnv(b, 1, types.CustodyBitmapAll)
|
||||
defer n.Close()
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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
904
eth/fetcher/blob_fetcher.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1090
eth/fetcher/blob_fetcher_test.go
Normal file
1090
eth/fetcher/blob_fetcher_test.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -57,4 +57,26 @@ var (
|
|||
// to become "unfrozen", either by eventually replying to the request
|
||||
// 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))
|
||||
|
||||
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))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"github.com/ethereum/go-ethereum/core"
|
||||
"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/eth/protocols/eth"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/metrics"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -185,6 +188,8 @@ type TxFetcher struct {
|
|||
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
|
||||
|
||||
buffer *blobpool.BlobBuffer
|
||||
|
||||
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
|
||||
|
|
@ -194,8 +199,9 @@ type TxFetcher struct {
|
|||
// NewTxFetcher creates a transaction fetcher to retrieve transaction
|
||||
// based on hash announcements.
|
||||
// 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 {
|
||||
return NewTxFetcherForTests(chain, validateMeta, addTxs, fetchTxs, dropPeer, mclock.System{}, time.Now, nil)
|
||||
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), 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
|
||||
|
|
@ -203,7 +209,7 @@ func NewTxFetcher(chain *core.BlockChain, validateMeta func(common.Hash, byte) e
|
|||
// Chain can be nil to disable on-chain checks.
|
||||
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),
|
||||
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{
|
||||
notify: make(chan *txAnnounce),
|
||||
cleanup: make(chan *txDelivery),
|
||||
|
|
@ -224,6 +230,7 @@ func NewTxFetcherForTests(
|
|||
addTxs: addTxs,
|
||||
fetchTxs: fetchTxs,
|
||||
dropPeer: dropPeer,
|
||||
buffer: buffer,
|
||||
clock: clock,
|
||||
realTime: realTime,
|
||||
rand: rand,
|
||||
|
|
@ -231,8 +238,8 @@ func NewTxFetcherForTests(
|
|||
}
|
||||
|
||||
// Notify announces the fetcher of the potential availability of a new batch of
|
||||
// transactions in the network.
|
||||
func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []common.Hash) error {
|
||||
// transactions in the network. It returns array of hashes decided to be fetched.
|
||||
func (f *TxFetcher) Notify(peer string, kinds []byte, sizes []uint32, hashes []common.Hash) ([]common.Hash, error) {
|
||||
// Keep track of all the announced transactions
|
||||
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))
|
||||
unknownMetas = make([]txMetadata, 0, len(hashes))
|
||||
|
||||
blobFetchHashes = make([]common.Hash, 0, len(hashes))
|
||||
|
||||
duplicate int64
|
||||
onchain int64
|
||||
underpriced int64
|
||||
)
|
||||
for i, hash := range hashes {
|
||||
err := f.validateMeta(hash, types[i])
|
||||
err := f.validateMeta(hash, kinds[i])
|
||||
if errors.Is(err, txpool.ErrAlreadyKnown) {
|
||||
if kinds[i] == types.BlobTxType {
|
||||
blobFetchHashes = append(blobFetchHashes, hash)
|
||||
}
|
||||
duplicate++
|
||||
continue
|
||||
}
|
||||
|
|
@ -271,11 +283,14 @@ func (f *TxFetcher) Notify(peer string, types []byte, sizes []uint32, hashes []c
|
|||
}
|
||||
|
||||
unknownHashes = append(unknownHashes, hash)
|
||||
if kinds[i] == types.BlobTxType {
|
||||
blobFetchHashes = append(blobFetchHashes, hash)
|
||||
}
|
||||
|
||||
// Transaction metadata has been available since eth68, and all
|
||||
// legacy eth protocols (prior to eth68) have been deprecated.
|
||||
// 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)
|
||||
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 len(unknownHashes) == 0 {
|
||||
return nil
|
||||
return blobFetchHashes, nil
|
||||
}
|
||||
announce := &txAnnounce{origin: peer, hashes: unknownHashes, metas: unknownMetas}
|
||||
select {
|
||||
case f.notify <- announce:
|
||||
return nil
|
||||
return blobFetchHashes, nil
|
||||
case <-f.quit:
|
||||
return errTerminated
|
||||
return nil, errTerminated
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -304,26 +319,36 @@ func (f *TxFetcher) isKnownUnderpriced(hash common.Hash) bool {
|
|||
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
|
||||
// and the fetcher. This method may be called by both transaction broadcasts and
|
||||
// direct request replies. The differentiation is important so the fetcher can
|
||||
// re-schedule missing transactions as soon as possible.
|
||||
func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool) error {
|
||||
var (
|
||||
inMeter = txReplyInMeter
|
||||
knownMeter = txReplyKnownMeter
|
||||
underpricedMeter = txReplyUnderpricedMeter
|
||||
otherRejectMeter = txReplyOtherRejectMeter
|
||||
violation error
|
||||
)
|
||||
func (f *TxFetcher) Enqueue(peer string, version uint, txs []*types.Transaction, direct bool) error {
|
||||
var violation error
|
||||
|
||||
metrics := deliveryMetrics{
|
||||
inMeter: txReplyInMeter,
|
||||
knownMeter: txReplyKnownMeter,
|
||||
underpricedMeter: txReplyUnderpricedMeter,
|
||||
otherRejectMeter: txReplyOtherRejectMeter,
|
||||
}
|
||||
if !direct {
|
||||
inMeter = txBroadcastInMeter
|
||||
knownMeter = txBroadcastKnownMeter
|
||||
underpricedMeter = txBroadcastUnderpricedMeter
|
||||
otherRejectMeter = txBroadcastOtherRejectMeter
|
||||
metrics = deliveryMetrics{
|
||||
inMeter: txBroadcastInMeter,
|
||||
knownMeter: txBroadcastKnownMeter,
|
||||
underpricedMeter: txBroadcastUnderpricedMeter,
|
||||
otherRejectMeter: txBroadcastOtherRejectMeter,
|
||||
}
|
||||
}
|
||||
// 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
|
||||
// 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) {
|
||||
end = len(txs)
|
||||
}
|
||||
var (
|
||||
duplicate int64
|
||||
underpriced int64
|
||||
otherreject int64
|
||||
)
|
||||
batch := txs[i:end]
|
||||
|
||||
for j, err := range f.addTxs(batch) {
|
||||
// Track the transaction hash if the price is too low for us.
|
||||
// Avoid re-request this transaction when we receive another
|
||||
// announcement.
|
||||
if errors.Is(err, txpool.ErrUnderpriced) || errors.Is(err, txpool.ErrReplaceUnderpriced) || errors.Is(err, txpool.ErrTxGasPriceTooLow) {
|
||||
f.underpriced.Add(batch[j].Hash(), batch[j].Time())
|
||||
var (
|
||||
poolTxs []*types.Transaction
|
||||
blobTxs []*types.Transaction
|
||||
)
|
||||
if version >= eth.ETH72 {
|
||||
for _, tx := range batch {
|
||||
if tx.Type() == types.BlobTxType {
|
||||
blobTxs = append(blobTxs, tx)
|
||||
} else {
|
||||
poolTxs = append(poolTxs, tx)
|
||||
}
|
||||
}
|
||||
// Track a few interesting failure types
|
||||
switch {
|
||||
case err == nil: // Noop, but need to handle to not count these
|
||||
} else {
|
||||
poolTxs = batch
|
||||
}
|
||||
batch = append(poolTxs, blobTxs...)
|
||||
|
||||
case errors.Is(err, txpool.ErrAlreadyKnown):
|
||||
duplicate++
|
||||
// Add regular tx to pool, blob tx to buffer.
|
||||
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):
|
||||
underpriced++
|
||||
|
||||
case errors.Is(err, txpool.ErrKZGVerificationError):
|
||||
hashes := make([]common.Hash, len(batch))
|
||||
for j := range batch {
|
||||
hashes[j] = batch[j].Hash()
|
||||
}
|
||||
for j, err := range errs {
|
||||
if errors.Is(err, txpool.ErrKZGVerificationError) || errors.Is(err, txpool.ErrSidecarFormatError) {
|
||||
// KZG verification failed, terminate transaction processing immediately.
|
||||
// Since KZG verification is computationally expensive, this acts as a
|
||||
// defensive measure against potential DoS attacks.
|
||||
violation = err
|
||||
|
||||
default:
|
||||
otherreject++
|
||||
}
|
||||
added = append(added, batch[j].Hash())
|
||||
metas = append(metas, txMetadata{
|
||||
|
|
@ -381,13 +405,11 @@ func (f *TxFetcher) Enqueue(peer string, txs []*types.Transaction, direct bool)
|
|||
break
|
||||
}
|
||||
}
|
||||
knownMeter.Mark(duplicate)
|
||||
underpricedMeter.Mark(underpriced)
|
||||
otherRejectMeter.Mark(otherreject)
|
||||
|
||||
// If 'other reject' is >25% of the deliveries in any batch, sleep a bit.
|
||||
if otherreject > int64((len(batch)+3)/4) {
|
||||
log.Debug("Peer delivering stale or invalid transactions", "peer", peer, "rejected", otherreject)
|
||||
otherreject := f.handleAddErrors(hashes, errs, metrics)
|
||||
// If 'other reject' is >25% of the deliveries in any batch, sleep a bit
|
||||
// to throttle the misbehaving peer.
|
||||
if otherreject > int64((len(hashes)+3)/4) {
|
||||
log.Debug("Peer delivering stale or invalid transactions", "rejected", otherreject)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
// 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
|
||||
// data structures of the given node.
|
||||
func (f *TxFetcher) Drop(peer string) error {
|
||||
|
|
@ -448,6 +500,14 @@ func (f *TxFetcher) loop() {
|
|||
}
|
||||
|
||||
for {
|
||||
txs, errs := f.buffer.Flush()
|
||||
f.handleAddErrors(txs, errs, deliveryMetrics{
|
||||
inMeter: txReplyInMeter,
|
||||
knownMeter: txReplyKnownMeter,
|
||||
underpricedMeter: txReplyUnderpricedMeter,
|
||||
otherRejectMeter: txReplyOtherRejectMeter,
|
||||
})
|
||||
|
||||
select {
|
||||
case ann := <-f.notify:
|
||||
// Drop part of the new announcements if there are too many accumulated.
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/mclock"
|
||||
"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/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/kzg4844"
|
||||
"github.com/ethereum/go-ethereum/eth/protocols/eth"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
|
@ -60,9 +62,10 @@ type doTxNotify struct {
|
|||
sizes []uint32
|
||||
}
|
||||
type doTxEnqueue struct {
|
||||
peer string
|
||||
txs []*types.Transaction
|
||||
direct bool
|
||||
peer string
|
||||
version uint
|
||||
txs []*types.Transaction
|
||||
direct bool
|
||||
}
|
||||
type doWait struct {
|
||||
time time.Duration
|
||||
|
|
@ -87,6 +90,16 @@ type txFetcherTest struct {
|
|||
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,
|
||||
// and deterministic randomness.
|
||||
func newTestTxFetcher() *TxFetcher {
|
||||
|
|
@ -98,6 +111,7 @@ func newTestTxFetcher() *TxFetcher {
|
|||
},
|
||||
func(string, []common.Hash) error { return nil },
|
||||
nil,
|
||||
newTestBlobBuffer(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1888,7 +1902,7 @@ func testTransactionFetcher(t *testing.T, tt txFetcherTest) {
|
|||
// Process the original or expanded steps
|
||||
switch step := step.(type) {
|
||||
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)
|
||||
}
|
||||
<-wait // Fetcher needs to process this, wait until it's done
|
||||
|
|
@ -1899,7 +1913,7 @@ func testTransactionFetcher(t *testing.T, tt txFetcherTest) {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
<-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) {},
|
||||
newTestBlobBuffer(),
|
||||
mockClock,
|
||||
mockTime,
|
||||
rand.New(rand.NewSource(0)), // Use fixed seed for deterministic behavior
|
||||
|
|
@ -2219,7 +2234,7 @@ func TestTransactionForgotten(t *testing.T) {
|
|||
tx2.SetTime(now)
|
||||
|
||||
// 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)
|
||||
}
|
||||
if !fetcher.isKnownUnderpriced(tx1.Hash()) {
|
||||
|
|
@ -2268,7 +2283,7 @@ func TestTransactionForgotten(t *testing.T) {
|
|||
|
||||
// Re-enqueue tx1 with updated timestamp
|
||||
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)
|
||||
}
|
||||
if !fetcher.isKnownUnderpriced(tx1.Hash()) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,9 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"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/crypto/kzg4844"
|
||||
"github.com/ethereum/go-ethereum/eth/downloader"
|
||||
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
||||
"github.com/ethereum/go-ethereum/eth/fetcher"
|
||||
|
|
@ -75,7 +77,7 @@ type txPool interface {
|
|||
|
||||
// GetRLP retrieves the RLP-encoded transaction from local txpool
|
||||
// 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
|
||||
// given transaction hash.
|
||||
|
|
@ -97,18 +99,31 @@ type txPool interface {
|
|||
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
|
||||
// node network handler.
|
||||
type handlerConfig struct {
|
||||
NodeID enode.ID // P2P node ID used for tx propagation topology
|
||||
Database ethdb.Database // Database for direct sync insertions
|
||||
Chain *core.BlockChain // Blockchain to serve data from
|
||||
TxPool txPool // Transaction pool to propagate from
|
||||
Network uint64 // Network identifier to advertise
|
||||
Sync ethconfig.SyncMode // Whether to snap or full sync
|
||||
BloomCache uint64 // Megabytes to alloc for snap sync bloom
|
||||
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
|
||||
NodeID enode.ID // P2P node ID used for tx propagation topology
|
||||
Database ethdb.Database // Database for direct sync insertions
|
||||
Chain *core.BlockChain // Blockchain to serve data from
|
||||
TxPool txPool // Transaction pool to propagate from
|
||||
BlobPool blobPool // Blob pool for cell-based blob data availability
|
||||
Network uint64 // Network identifier to advertise
|
||||
Sync ethconfig.SyncMode // Whether to snap or full sync
|
||||
BloomCache uint64 // Megabytes to alloc for snap sync bloom
|
||||
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 {
|
||||
|
|
@ -118,11 +133,13 @@ type handler struct {
|
|||
|
||||
database ethdb.Database
|
||||
txpool txPool
|
||||
blobpool blobPool
|
||||
chain *core.BlockChain
|
||||
maxPeers int
|
||||
|
||||
downloader *downloader.Downloader
|
||||
txFetcher *fetcher.TxFetcher
|
||||
blobFetcher *fetcher.BlobFetcher
|
||||
peers *peerSet
|
||||
txBroadcastKey [16]byte
|
||||
|
||||
|
|
@ -148,6 +165,7 @@ func newHandler(config *handlerConfig) (*handler, error) {
|
|||
networkID: config.Network,
|
||||
database: config.Database,
|
||||
txpool: config.TxPool,
|
||||
blobpool: config.BlobPool,
|
||||
chain: config.Chain,
|
||||
peers: newPeerSet(),
|
||||
txBroadcastKey: newBroadcastChoiceKey(),
|
||||
|
|
@ -170,11 +188,19 @@ func newHandler(config *handlerConfig) (*handler, error) {
|
|||
}
|
||||
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 {
|
||||
return h.txpool.Add(txs, false)
|
||||
}
|
||||
validateMeta := func(tx common.Hash, kind byte) error {
|
||||
if h.txpool.Has(tx) {
|
||||
if h.txpool.Has(tx) || blobBuffer.HasTx(tx) {
|
||||
return txpool.ErrAlreadyKnown
|
||||
}
|
||||
if !h.txpool.FilterType(kind) {
|
||||
|
|
@ -182,7 +208,30 @@ func newHandler(config *handlerConfig) (*handler, error) {
|
|||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -397,6 +446,7 @@ func (h *handler) unregisterPeer(id string) {
|
|||
}
|
||||
h.downloader.UnregisterPeer(id)
|
||||
h.txFetcher.Drop(id)
|
||||
h.blobFetcher.Drop(id)
|
||||
|
||||
if err := h.peers.unregisterPeer(id); err != nil {
|
||||
logger.Error("Ethereum peer removal failed", "err", err)
|
||||
|
|
@ -419,6 +469,7 @@ func (h *handler) Start(maxPeers int) {
|
|||
|
||||
// start sync handlers
|
||||
h.txFetcher.Start()
|
||||
h.blobFetcher.Start()
|
||||
|
||||
// start peer handler tracker
|
||||
h.wg.Add(1)
|
||||
|
|
@ -429,6 +480,7 @@ func (h *handler) Stop() {
|
|||
h.txsSub.Unsubscribe() // quits txBroadcastLoop
|
||||
h.blockRange.stop()
|
||||
h.txFetcher.Stop()
|
||||
h.blobFetcher.Stop()
|
||||
h.downloader.Terminate()
|
||||
|
||||
// Quit chainSync and txsync64.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ type ethHandler handler
|
|||
|
||||
func (h *ethHandler) Chain() *core.BlockChain { return h.chain }
|
||||
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.
|
||||
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 {
|
||||
// Consume any broadcasts and announces, forwarding the rest to the downloader
|
||||
switch packet := packet.(type) {
|
||||
case *eth.NewPooledTransactionHashesPacket:
|
||||
return h.txFetcher.Notify(peer.ID(), packet.Types, packet.Sizes, packet.Hashes)
|
||||
case *eth.NewPooledTransactionHashesPacket72:
|
||||
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:
|
||||
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 {
|
||||
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:
|
||||
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 {
|
||||
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:
|
||||
return fmt.Errorf("unexpected eth packet type: %T", packet)
|
||||
|
|
|
|||
|
|
@ -44,13 +44,14 @@ type testEthHandler struct {
|
|||
|
||||
func (h *testEthHandler) Chain() *core.BlockChain { panic("no backing chain") }
|
||||
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) 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) Handle(peer *eth.Peer, packet eth.Packet) error {
|
||||
switch packet := packet.(type) {
|
||||
case *eth.NewPooledTransactionHashesPacket:
|
||||
case *eth.NewPooledTransactionHashesPacket71:
|
||||
h.txAnnounces.Send(packet.Hashes)
|
||||
return nil
|
||||
|
||||
|
|
@ -105,10 +106,12 @@ func testForkIDSplit(t *testing.T, protocol uint) {
|
|||
_, blocksNoFork, _ = core.GenerateChainWithGenesis(gspecNoFork, engine, 2, nil)
|
||||
_, blocksProFork, _ = core.GenerateChainWithGenesis(gspecProFork, engine, 2, nil)
|
||||
|
||||
txPool = newTestTxPool()
|
||||
ethNoFork, _ = newHandler(&handlerConfig{
|
||||
Database: dbNoFork,
|
||||
Chain: chainNoFork,
|
||||
TxPool: newTestTxPool(),
|
||||
TxPool: txPool,
|
||||
BlobPool: txPool,
|
||||
Network: 1,
|
||||
Sync: ethconfig.FullSync,
|
||||
BloomCache: 1,
|
||||
|
|
@ -116,7 +119,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
|
|||
ethProFork, _ = newHandler(&handlerConfig{
|
||||
Database: dbProFork,
|
||||
Chain: chainProFork,
|
||||
TxPool: newTestTxPool(),
|
||||
TxPool: txPool,
|
||||
BlobPool: txPool,
|
||||
Network: 1,
|
||||
Sync: ethconfig.FullSync,
|
||||
BloomCache: 1,
|
||||
|
|
@ -137,8 +141,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
|
|||
defer p2pNoFork.Close()
|
||||
defer p2pProFork.Close()
|
||||
|
||||
peerNoFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil, nil)
|
||||
peerProFork := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, 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, nil)
|
||||
defer peerNoFork.Close()
|
||||
defer peerProFork.Close()
|
||||
|
||||
|
|
@ -168,8 +172,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
|
|||
defer p2pNoFork.Close()
|
||||
defer p2pProFork.Close()
|
||||
|
||||
peerNoFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{1}, "", nil), p2pNoFork, nil, nil)
|
||||
peerProFork = eth.NewPeer(protocol, p2p.NewPeer(enode.ID{2}, "", nil), p2pProFork, 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, nil)
|
||||
defer peerNoFork.Close()
|
||||
defer peerProFork.Close()
|
||||
|
||||
|
|
@ -199,8 +203,8 @@ func testForkIDSplit(t *testing.T, protocol uint) {
|
|||
defer p2pNoFork.Close()
|
||||
defer p2pProFork.Close()
|
||||
|
||||
peerNoFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pNoFork), p2pNoFork, nil, nil)
|
||||
peerProFork = eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pProFork), p2pProFork, 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, nil)
|
||||
defer peerNoFork.Close()
|
||||
defer peerProFork.Close()
|
||||
|
||||
|
|
@ -249,8 +253,8 @@ func testRecvTransactions(t *testing.T, protocol uint) {
|
|||
defer p2pSrc.Close()
|
||||
defer p2pSink.Close()
|
||||
|
||||
src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool, nil)
|
||||
sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, 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, handler.txpool, nil)
|
||||
defer src.Close()
|
||||
defer sink.Close()
|
||||
|
||||
|
|
@ -305,8 +309,8 @@ func testSendTransactions(t *testing.T, protocol uint) {
|
|||
defer p2pSrc.Close()
|
||||
defer p2pSink.Close()
|
||||
|
||||
src := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{1}, "", nil, p2pSrc), p2pSrc, handler.txpool, nil)
|
||||
sink := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{2}, "", nil, p2pSink), p2pSink, 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, handler.blobpool, nil)
|
||||
defer src.Close()
|
||||
defer sink.Close()
|
||||
|
||||
|
|
@ -380,8 +384,8 @@ func testTransactionPropagation(t *testing.T, protocol uint) {
|
|||
defer sourcePipe.Close()
|
||||
defer sinkPipe.Close()
|
||||
|
||||
sourcePeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{byte(i + 1)}, "", nil, sourcePipe), sourcePipe, source.txpool, nil)
|
||||
sinkPeer := eth.NewPeer(protocol, p2p.NewPeerPipe(enode.ID{0}, "", nil, sinkPipe), sinkPipe, sink.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, sink.txpool, nil)
|
||||
defer sourcePeer.Close()
|
||||
defer sinkPeer.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,10 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||
"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/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/kzg4844"
|
||||
"github.com/ethereum/go-ethereum/eth/ethconfig"
|
||||
"github.com/ethereum/go-ethereum/eth/protocols/eth"
|
||||
"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
|
||||
// checks.
|
||||
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
|
||||
lock sync.RWMutex // Protects the transaction pool
|
||||
|
|
@ -63,7 +68,9 @@ type testTxPool struct {
|
|||
// newTestTxPool creates a mock transaction pool.
|
||||
func newTestTxPool() *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()
|
||||
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
|
||||
|
|
@ -81,16 +97,16 @@ func (p *testTxPool) Has(hash common.Hash) bool {
|
|||
func (p *testTxPool) Get(hash common.Hash) *types.Transaction {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
return p.pool[hash]
|
||||
return p.txPool[hash]
|
||||
}
|
||||
|
||||
// Get retrieves the transaction from local txpool with given
|
||||
// tx hash.
|
||||
func (p *testTxPool) GetRLP(hash common.Hash) []byte {
|
||||
func (p *testTxPool) GetRLP(hash common.Hash, _ uint) []byte {
|
||||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
tx := p.pool[hash]
|
||||
tx := p.txPool[hash]
|
||||
if tx != nil {
|
||||
blob, _ := rlp.EncodeToBytes(tx)
|
||||
return blob
|
||||
|
|
@ -104,7 +120,7 @@ func (p *testTxPool) GetMetadata(hash common.Hash) *txpool.TxMetadata {
|
|||
p.lock.Lock()
|
||||
defer p.lock.Unlock()
|
||||
|
||||
tx := p.pool[hash]
|
||||
tx := p.txPool[hash]
|
||||
if tx != nil {
|
||||
return &txpool.TxMetadata{
|
||||
Type: tx.Type(),
|
||||
|
|
@ -121,7 +137,7 @@ func (p *testTxPool) Add(txs []*types.Transaction, sync bool) []error {
|
|||
defer p.lock.Unlock()
|
||||
|
||||
for _, tx := range txs {
|
||||
p.pool[tx.Hash()] = tx
|
||||
p.txPool[tx.Hash()] = tx
|
||||
}
|
||||
p.txFeed.Send(core.NewTxsEvent{Txs: txs})
|
||||
return make([]error, len(txs))
|
||||
|
|
@ -134,7 +150,7 @@ func (p *testTxPool) Pending(filter txpool.PendingFilter) (map[common.Address][]
|
|||
|
||||
var count int
|
||||
batches := make(map[common.Address][]*types.Transaction)
|
||||
for _, tx := range p.pool {
|
||||
for _, tx := range p.txPool {
|
||||
from, _ := types.Sender(types.HomesteadSigner{}, 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 {
|
||||
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.
|
||||
func (p *testTxPool) FilterType(kind byte) bool {
|
||||
|
|
@ -174,14 +271,19 @@ func (p *testTxPool) FilterType(kind byte) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (p *testTxPool) ValidateTxBasics(_ *types.Transaction) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// testHandler is a live implementation of the Ethereum protocol handler, just
|
||||
// preinitialized with some sane testing defaults and the transaction pool mocked
|
||||
// out.
|
||||
type testHandler struct {
|
||||
db ethdb.Database
|
||||
chain *core.BlockChain
|
||||
txpool *testTxPool
|
||||
handler *handler
|
||||
db ethdb.Database
|
||||
chain *core.BlockChain
|
||||
txpool *testTxPool
|
||||
blobpool *testTxPool
|
||||
handler *handler
|
||||
}
|
||||
|
||||
// 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,
|
||||
Chain: chain,
|
||||
TxPool: txpool,
|
||||
BlobPool: txpool,
|
||||
Network: 1,
|
||||
Sync: mode,
|
||||
BloomCache: 1,
|
||||
|
|
@ -217,10 +320,11 @@ func newTestHandlerWithBlocks(blocks int, mode ethconfig.SyncMode) *testHandler
|
|||
handler.Start(1000)
|
||||
|
||||
return &testHandler{
|
||||
db: db,
|
||||
chain: chain,
|
||||
txpool: txpool,
|
||||
handler: handler,
|
||||
db: db,
|
||||
chain: chain,
|
||||
txpool: txpool,
|
||||
blobpool: txpool,
|
||||
handler: handler,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +421,7 @@ func createTestPeers(rand *rand.Rand, n int) []*ethPeer {
|
|||
var id enode.ID
|
||||
rand.Read(id[:])
|
||||
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] = ðPeer{Peer: ep}
|
||||
}
|
||||
return peers
|
||||
|
|
|
|||
|
|
@ -113,29 +113,59 @@ func (p *Peer) announceTransactions() {
|
|||
pending []common.Hash
|
||||
pendingTypes []byte
|
||||
pendingSizes []uint32
|
||||
mask types.CustodyBitmap
|
||||
size common.StorageSize
|
||||
processed = make(map[int]bool)
|
||||
)
|
||||
for count = 0; count < len(queue) && size < maxTxPacketSize; count++ {
|
||||
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])
|
||||
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
|
||||
|
||||
processed[count] = true
|
||||
}
|
||||
}
|
||||
// Shift and trim queue
|
||||
queue = queue[:copy(queue, queue[count:])]
|
||||
// Shift and trim queue using processed map
|
||||
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 len(pending) > 0 {
|
||||
done = make(chan struct{})
|
||||
go func() {
|
||||
if err := p.sendPooledTransactionHashes(pending, pendingTypes, pendingSizes); err != nil {
|
||||
if err := p.sendPooledTransactionHashes(pending, pendingTypes, pendingSizes, mask); err != nil {
|
||||
fail <- err
|
||||
return
|
||||
}
|
||||
close(done)
|
||||
p.Log().Trace("Sent transaction announcements", "count", len(pending))
|
||||
p.Log().Trace("Sent transaction announcements", "count", len(pending), "mask", mask, "tx", pending)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core"
|
||||
"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/metrics"
|
||||
"github.com/ethereum/go-ethereum/p2p"
|
||||
"github.com/ethereum/go-ethereum/p2p/enode"
|
||||
|
|
@ -71,6 +72,9 @@ type Backend interface {
|
|||
// TxPool retrieves the transaction pool object to serve data.
|
||||
TxPool() TxPool
|
||||
|
||||
// BlobPool retrieves the blob pool object to serve cell requests.
|
||||
BlobPool() BlobPool
|
||||
|
||||
// AcceptTxs retrieves whether transaction processing is enabled on the node
|
||||
// or if inbound transactions should simply be dropped.
|
||||
AcceptTxs() bool
|
||||
|
|
@ -90,6 +94,18 @@ type Backend interface {
|
|||
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.
|
||||
type TxPool interface {
|
||||
// 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
|
||||
// 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
|
||||
// given transaction hash.
|
||||
|
|
@ -113,7 +129,7 @@ func MakeProtocols(backend Backend, network uint64, disc enode.Iterator) []p2p.P
|
|||
Version: version,
|
||||
Length: protocolLengths[version],
|
||||
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()
|
||||
|
||||
return backend.RunPeer(peer, func(peer *Peer) error {
|
||||
|
|
@ -216,6 +232,24 @@ var eth71 = map[uint64]msgHandler{
|
|||
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
|
||||
// peer. The remote connection is torn down upon returning any error.
|
||||
func handleMessage(backend Backend, peer *Peer) error {
|
||||
|
|
@ -237,6 +271,8 @@ func handleMessage(backend Backend, peer *Peer) error {
|
|||
handlers = eth70
|
||||
case ETH71:
|
||||
handlers = eth71
|
||||
case ETH72:
|
||||
handlers = eth72
|
||||
default:
|
||||
return fmt.Errorf("unknown eth protocol version: %v", peer.version)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,9 +63,10 @@ func u64(val uint64) *uint64 { return &val }
|
|||
// purpose is to allow testing the request/reply workflows and wire serialization
|
||||
// in the `eth` protocol without actually doing any data processing.
|
||||
type testBackend struct {
|
||||
db ethdb.Database
|
||||
chain *core.BlockChain
|
||||
txpool *txpool.TxPool
|
||||
db ethdb.Database
|
||||
chain *core.BlockChain
|
||||
txpool *txpool.TxPool
|
||||
blobpool *blobpool.BlobPool
|
||||
}
|
||||
|
||||
// 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})
|
||||
|
||||
return &testBackend{
|
||||
db: db,
|
||||
chain: chain,
|
||||
txpool: txpool,
|
||||
db: db,
|
||||
chain: chain,
|
||||
txpool: txpool,
|
||||
blobpool: blobPool,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -157,6 +159,7 @@ func (b *testBackend) close() {
|
|||
|
||||
func (b *testBackend) Chain() *core.BlockChain { return b.chain }
|
||||
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 {
|
||||
// Normally the backend would do peer maintenance and handshakes. All that
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/ethereum/go-ethereum/crypto/kzg4844"
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
"github.com/ethereum/go-ethereum/p2p/tracker"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
|
|
@ -568,7 +569,27 @@ func handleNewPooledTransactionHashes(backend Backend, msg Decoder, peer *Peer)
|
|||
if !backend.AcceptTxs() {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
@ -588,11 +609,11 @@ func handleGetPooledTransactions(backend Backend, msg Decoder, peer *Peer) error
|
|||
if err := msg.Decode(&query); err != nil {
|
||||
return err
|
||||
}
|
||||
hashes, txs := answerGetPooledTransactions(backend, query.GetPooledTransactionsRequest)
|
||||
hashes, txs := answerGetPooledTransactions(backend, query.GetPooledTransactionsRequest, peer.version)
|
||||
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
|
||||
var (
|
||||
bytes int
|
||||
|
|
@ -604,7 +625,7 @@ func answerGetPooledTransactions(backend Backend, query GetPooledTransactionsReq
|
|||
break
|
||||
}
|
||||
// Retrieve the requested transaction, skipping if unknown to us
|
||||
encoded := backend.TxPool().GetRLP(hash)
|
||||
encoded := backend.TxPool().GetRLP(hash, version)
|
||||
if len(encoded) == 0 {
|
||||
continue
|
||||
}
|
||||
|
|
@ -667,6 +688,80 @@ func handleBlockRangeUpdate(backend Backend, msg Decoder, peer *Peer) error {
|
|||
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.
|
||||
func handleGetBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error {
|
||||
var query GetBlockAccessListsPacket
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ func testHandshake(t *testing.T, protocol uint) {
|
|||
defer app.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()
|
||||
|
||||
// Send the junk test with one peer, check the handshake failure
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
mapset "github.com/deckarep/golang-set/v2"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"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/tracker"
|
||||
"github.com/ethereum/go-ethereum/params"
|
||||
|
|
@ -66,7 +67,8 @@ type Peer struct {
|
|||
version uint // Protocol version negotiated
|
||||
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
|
||||
txBroadcast chan []common.Hash // Channel used to queue transaction propagation 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
|
||||
// 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}
|
||||
id := p.ID().String()
|
||||
peer := &Peer{
|
||||
id: p.ID().String(),
|
||||
id: id,
|
||||
Peer: p,
|
||||
rw: rw,
|
||||
version: version,
|
||||
|
|
@ -102,6 +104,7 @@ func NewPeer(version uint, p *p2p.Peer, rw p2p.MsgReadWriter, txpool TxPool, cha
|
|||
reqCancel: make(chan *cancel),
|
||||
resDispatch: make(chan *response),
|
||||
txpool: txpool,
|
||||
blobpool: blobpool,
|
||||
chainConfig: chainConfig,
|
||||
receiptBuffer: make(map[uint64]*receiptRequest),
|
||||
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
|
||||
// directly as the queueing (memory) and transmission (bandwidth) costs should
|
||||
// not be managed directly.
|
||||
func (p *Peer) sendPooledTransactionHashes(hashes []common.Hash, types []byte, sizes []uint32) error {
|
||||
if err := p2p.Send(p.rw, NewPooledTransactionHashesMsg, NewPooledTransactionHashesPacket{Types: types, Sizes: sizes, Hashes: hashes}); err != nil {
|
||||
return err
|
||||
}
|
||||
func (p *Peer) sendPooledTransactionHashes(hashes []common.Hash, types []byte, sizes []uint32, cells types.CustodyBitmap) error {
|
||||
// Mark all the transactions as known, but ensure we don't overflow our limits
|
||||
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
|
||||
|
|
@ -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.
|
||||
func (p *Peer) ReplyReceiptsRLP70(id uint64, receipts rlp.RawList[*ReceiptList], lastBlockIncomplete bool) error {
|
||||
return p2p.Send(p.rw, ReceiptsMsg, &ReceiptsPacket70{
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func newTestPeer(name string, version uint, backend Backend) (*testPeer, <-chan
|
|||
var id enode.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)
|
||||
go func() {
|
||||
defer app.Close()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/core/forkid"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/core/types/bal"
|
||||
"github.com/ethereum/go-ethereum/crypto/kzg4844"
|
||||
"github.com/ethereum/go-ethereum/rlp"
|
||||
)
|
||||
|
||||
|
|
@ -33,6 +34,7 @@ const (
|
|||
ETH69 = 69
|
||||
ETH70 = 70
|
||||
ETH71 = 71
|
||||
ETH72 = 72
|
||||
)
|
||||
|
||||
// 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
|
||||
// is primary).
|
||||
var ProtocolVersions = []uint{ETH71, ETH70, ETH69}
|
||||
var ProtocolVersions = []uint{ETH72, ETH71, ETH70, ETH69}
|
||||
|
||||
// protocolLengths are the number of implemented message corresponding to
|
||||
// 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.
|
||||
const maxMessageSize = 10 * 1024 * 1024
|
||||
|
|
@ -70,6 +72,8 @@ const (
|
|||
BlockRangeUpdateMsg = 0x11
|
||||
GetBlockAccessListsMsg = 0x12
|
||||
BlockAccessListsMsg = 0x13
|
||||
GetCellsMsg = 0x14
|
||||
CellsMsg = 0x15
|
||||
)
|
||||
|
||||
var (
|
||||
|
|
@ -249,13 +253,22 @@ type ReceiptsPacket70 struct {
|
|||
// ReceiptsRLPResponse is used for receipts, when we already have it encoded
|
||||
type ReceiptsRLPResponse []rlp.RawValue
|
||||
|
||||
// NewPooledTransactionHashesPacket represents a transaction announcement packet on eth/68 and newer.
|
||||
type NewPooledTransactionHashesPacket struct {
|
||||
// NewPooledTransactionHashesPacket71 represents a transaction announcement packet on eth/69.
|
||||
type NewPooledTransactionHashesPacket71 struct {
|
||||
Types []byte
|
||||
Sizes []uint32
|
||||
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.
|
||||
type GetPooledTransactionsRequest []common.Hash
|
||||
|
||||
|
|
@ -292,6 +305,31 @@ type BlockRangeUpdatePacket struct {
|
|||
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 GetBlockAccessListsPacket struct {
|
||||
|
|
@ -328,8 +366,11 @@ func (*GetBlockBodiesRequest) Kind() byte { return GetBlockBodiesMsg }
|
|||
func (*BlockBodiesResponse) Name() string { return "BlockBodies" }
|
||||
func (*BlockBodiesResponse) Kind() byte { return BlockBodiesMsg }
|
||||
|
||||
func (*NewPooledTransactionHashesPacket) Name() string { return "NewPooledTransactionHashes" }
|
||||
func (*NewPooledTransactionHashesPacket) Kind() byte { return NewPooledTransactionHashesMsg }
|
||||
func (*NewPooledTransactionHashesPacket71) Name() string { return "NewPooledTransactionHashes" }
|
||||
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) Kind() byte { return GetPooledTransactionsMsg }
|
||||
|
|
@ -354,3 +395,9 @@ func (*GetBlockAccessListsRequest) Kind() byte { return GetBlockAccessListsMsg
|
|||
|
||||
func (*BlockAccessListResponse) Name() string { return "BlockAccessLists" }
|
||||
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 }
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ func testSnapSyncDisabling(t *testing.T, ethVer uint, snapVer uint) {
|
|||
defer emptyPipeEth.Close()
|
||||
defer fullPipeEth.Close()
|
||||
|
||||
emptyPeerEth := eth.NewPeer(ethVer, p2p.NewPeer(enode.ID{1}, "", caps), emptyPipeEth, empty.txpool, nil)
|
||||
fullPeerEth := eth.NewPeer(ethVer, p2p.NewPeer(enode.ID{2}, "", caps), fullPipeEth, full.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, full.blobpool, nil)
|
||||
defer emptyPeerEth.Close()
|
||||
defer fullPeerEth.Close()
|
||||
|
||||
|
|
|
|||
|
|
@ -25,22 +25,28 @@ import (
|
|||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"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/eth/fetcher"
|
||||
"github.com/ethereum/go-ethereum/eth/protocols/eth"
|
||||
)
|
||||
|
||||
var (
|
||||
peers []string
|
||||
txs []*types.Transaction
|
||||
peers []string
|
||||
peerVersions map[string]uint
|
||||
txs []*types.Transaction
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Random is nice, but we need it deterministic
|
||||
rand := rand.New(rand.NewSource(0x3a29))
|
||||
|
||||
supportedVersions := []uint{eth.ETH69, eth.ETH70, eth.ETH72}
|
||||
peers = make([]string, 10)
|
||||
peerVersions = make(map[string]uint, len(peers))
|
||||
for i := 0; i < len(peers); 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
|
||||
for i := 0; i < len(txs); i++ {
|
||||
|
|
@ -85,6 +91,12 @@ func fuzz(input []byte) int {
|
|||
},
|
||||
func(string, []common.Hash) error { return nil },
|
||||
nil,
|
||||
|
||||
blobpool.NewBlobBuffer(blobpool.BlobBufferFunctions{
|
||||
ValidateTx: func(*types.Transaction) error { return nil },
|
||||
AddToPool: func(*blobpool.BlobTxForPool) error { return nil },
|
||||
DropPeer: func(string) {},
|
||||
}),
|
||||
clock,
|
||||
func() time.Time {
|
||||
nanoTime := int64(clock.Now())
|
||||
|
|
@ -139,7 +151,7 @@ func fuzz(input []byte) int {
|
|||
if verbose {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -180,7 +192,7 @@ func fuzz(input []byte) int {
|
|||
if verbose {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue