eth/protocols/eth: implement eth71 bal response (#34879)
Some checks are pending
/ Linux Build (push) Waiting to run
/ Linux Build (arm) (push) Waiting to run
/ Keeper Build (push) Waiting to run
/ Windows Build (push) Waiting to run
/ Docker Image (push) Waiting to run

This PR implements the serving side of the eth71 BAL exchange messages.
Until commit 4cd7092 also contained the requesting side, but since that
part still needs more work, I'm splitting it out into a separate PR.

The test injects BALs directly into rawdb. This can be removed once BAL 
generation is integrated into the chain maker.

---------

Co-authored-by: Felix Lange <fjl@twurst.com>
This commit is contained in:
Bosul Mun 2026-05-19 20:25:13 +02:00 committed by GitHub
parent 1bdc4a60d9
commit a484a8506d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 209 additions and 2 deletions

View file

@ -296,6 +296,7 @@ func (bc *BlockChain) GetReceiptsRLP(hash common.Hash) rlp.RawValue {
return rawdb.ReadReceiptsRLP(bc.db, hash, number)
}
// GetAccessListRLP retrieves the block access list of a block in RLP encoding.
func (bc *BlockChain) GetAccessListRLP(hash common.Hash) rlp.RawValue {
number, ok := rawdb.ReadHeaderNumber(bc.db, hash)
if !ok {

View file

@ -53,6 +53,9 @@ const (
// containing 200+ transactions nowadays, the practical limit will always
// be softResponseLimit.
maxReceiptsServe = 1024
// maxBALsServe is the maximum number of block access lists to serve.
maxBALsServe = 1024
)
// Handler is a callback to invoke from an outside runner after the boilerplate
@ -197,6 +200,22 @@ var eth70 = map[uint64]msgHandler{
BlockRangeUpdateMsg: handleBlockRangeUpdate,
}
var eth71 = map[uint64]msgHandler{
TransactionsMsg: handleTransactions,
NewPooledTransactionHashesMsg: handleNewPooledTransactionHashes,
GetBlockHeadersMsg: handleGetBlockHeaders,
BlockHeadersMsg: handleBlockHeaders,
GetBlockBodiesMsg: handleGetBlockBodies,
BlockBodiesMsg: handleBlockBodies,
GetReceiptsMsg: handleGetReceipts70,
ReceiptsMsg: handleReceipts70,
GetPooledTransactionsMsg: handleGetPooledTransactions,
PooledTransactionsMsg: handlePooledTransactions,
BlockRangeUpdateMsg: handleBlockRangeUpdate,
GetBlockAccessListsMsg: handleGetBlockAccessLists,
BlockAccessListsMsg: handleBlockAccessLists,
}
// 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 {
@ -216,6 +235,8 @@ func handleMessage(backend Backend, peer *Peer) error {
handlers = eth69
case ETH70:
handlers = eth70
case ETH71:
handlers = eth71
default:
return fmt.Errorf("unknown eth protocol version: %v", peer.version)
}

View file

@ -37,6 +37,7 @@ import (
"github.com/ethereum/go-ethereum/core/txpool/blobpool"
"github.com/ethereum/go-ethereum/core/txpool/legacypool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/types/bal"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/ethdb"
@ -709,6 +710,67 @@ func testGetBlockPartialReceipts(t *testing.T, protocol int) {
}
}
// makeTestBAL creates a BAL with a given address access and balance change,
// and returns its RLP encoding. This is used for injection into the chain DB via
// rawdb.WriteAccessListRLP.
// TODO: Should be deleted when bal is integrated with chain maker.
func makeTestBAL(t *testing.T, addr common.Address) rlp.RawValue {
cb := bal.NewConstructionBlockAccessList()
cb.AccountRead(addr)
cb.BalanceChange(0, addr, uint256.NewInt(1))
var buf bytes.Buffer
if err := cb.EncodeRLP(&buf); err != nil {
t.Fatalf("failed to encode BAL: %v", err)
}
return buf.Bytes()
}
// TestGetBlockAccessLists checks serving part of bal exchange
func TestGetBlockAccessLists(t *testing.T) { testGetBlockAccessLists(t, ETH71) }
func testGetBlockAccessLists(t *testing.T, protocol uint) {
t.Parallel()
backend := newTestBackend(5)
defer backend.close()
peer, _ := newTestPeer("peer", protocol, backend)
defer peer.close()
bal1 := makeTestBAL(t, common.Address{0x11})
bal2 := makeTestBAL(t, common.Address{0x22})
var (
hashes []common.Hash
expect rlp.RawList[RawBlockAccessList]
)
for i := uint64(0); i <= backend.chain.CurrentBlock().Number.Uint64(); i++ {
block := backend.chain.GetBlockByNumber(i)
hashes = append(hashes, block.Hash())
switch i {
case 1:
rawdb.WriteAccessListRLP(backend.db, block.Hash(), i, bal1)
expect.AppendRaw(bal1)
case 3:
rawdb.WriteAccessListRLP(backend.db, block.Hash(), i, bal2)
expect.AppendRaw(bal2)
default:
expect.AppendRaw(rlp.EmptyString)
}
}
p2p.Send(peer.app, GetBlockAccessListsMsg, &GetBlockAccessListsPacket{
RequestId: 123,
GetBlockAccessListsRequest: hashes,
})
if err := p2p.ExpectMsg(peer.app, BlockAccessListsMsg, &BlockAccessListPacket{
RequestId: 123,
List: expect,
}); err != nil {
t.Errorf("BAL response mismatch: %v", err)
}
}
type decoder struct {
msg []byte
}

View file

@ -666,3 +666,68 @@ func handleBlockRangeUpdate(backend Backend, msg Decoder, peer *Peer) error {
peer.lastRange.Store(&update)
return nil
}
// handleGetBlockAccessLists serves a GetBlockAccessLists request.
func handleGetBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error {
var query GetBlockAccessListsPacket
if err := msg.Decode(&query); err != nil {
return err
}
response := serviceGetBlockAccessListsQuery(backend.Chain(), query.GetBlockAccessListsRequest)
return peer.ReplyBlockAccessLists(query.RequestId, response)
}
// serviceGetBlockAccessListsQuery assembles the response to a BAL query.
// Unavailable BALs are returned as empty list entries.
func serviceGetBlockAccessListsQuery(chain *core.BlockChain, query GetBlockAccessListsRequest) rlp.RawList[RawBlockAccessList] {
var (
bytes int
bals rlp.RawList[RawBlockAccessList]
)
for _, hash := range query {
if bytes >= softResponseLimit || bals.Len() >= maxBALsServe {
break
}
data := chain.GetAccessListRLP(hash)
if len(data) == 0 {
// The signal for missing BAL is the empty string, because
// an empty list is also a valid BAL.
bals.AppendRaw(rlp.EmptyString)
continue
}
bals.AppendRaw(data)
bytes += len(data)
}
return bals
}
// handleBlockAccessLists processes an incoming BlockAccessLists response,
// validates it against the request tracker, and dispatches it to the waiting caller.
func handleBlockAccessLists(backend Backend, msg Decoder, peer *Peer) error {
res := new(BlockAccessListPacket)
if err := msg.Decode(res); err != nil {
return err
}
tresp := tracker.Response{ID: res.RequestId, MsgCode: BlockAccessListsMsg, Size: res.List.Len()}
if err := peer.tracker.Fulfil(tresp); err != nil {
return fmt.Errorf("BlockAccessLists: %w", err)
}
bals, err := res.List.Items()
if err != nil {
return fmt.Errorf("BlockAccessLists: %w", err)
}
metadata := func() interface{} {
hashes := make([]common.Hash, len(bals))
for i := range bals {
hashes[i] = crypto.Keccak256Hash(bals[i].Bytes())
}
return hashes
}
return peer.dispatchResponse(&Response{
id: res.RequestId,
code: BlockAccessListsMsg,
Res: (*BlockAccessListResponse)(&bals),
}, metadata)
}

View file

@ -251,6 +251,36 @@ func (p *Peer) ReplyReceiptsRLP70(id uint64, receipts rlp.RawList[*ReceiptList],
})
}
// ReplyBlockAccessLists is the response to GetBlockAccessLists (EIP-8159).
func (p *Peer) ReplyBlockAccessLists(id uint64, list rlp.RawList[RawBlockAccessList]) error {
return p2p.Send(p.rw, BlockAccessListsMsg, &BlockAccessListPacket{
RequestId: id,
List: list,
})
}
// RequestBALs fetches block access lists for the given block hashes (EIP-8159)
func (p *Peer) RequestBALs(hashes []common.Hash, sink chan *Response) (*Request, error) {
p.Log().Debug("Fetching block access lists", "count", len(hashes))
id := rand.Uint64()
req := &Request{
id: id,
sink: sink,
code: GetBlockAccessListsMsg,
want: BlockAccessListsMsg,
numItems: len(hashes),
data: &GetBlockAccessListsPacket{
RequestId: id,
GetBlockAccessListsRequest: hashes,
},
}
if err := p.dispatchRequest(req); err != nil {
return nil, err
}
return req, nil
}
// RequestOneHeader is a wrapper around the header query functions to fetch a
// single header. It is used solely by the fetcher.
func (p *Peer) RequestOneHeader(hash common.Hash, sink chan *Response) (*Request, error) {

View file

@ -24,6 +24,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"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/rlp"
)
@ -31,6 +32,7 @@ import (
const (
ETH69 = 69
ETH70 = 70
ETH71 = 71
)
// ProtocolName is the official short name of the `eth` protocol used during
@ -39,11 +41,11 @@ const ProtocolName = "eth"
// ProtocolVersions are the supported versions of the `eth` protocol (first
// is primary).
var ProtocolVersions = []uint{ETH70, ETH69}
var ProtocolVersions = []uint{ETH71, ETH70, ETH69}
// protocolLengths are the number of implemented message corresponding to
// different protocol versions.
var protocolLengths = map[uint]uint64{ETH69: 18, ETH70: 18}
var protocolLengths = map[uint]uint64{ETH71: 20, ETH69: 18, ETH70: 18}
// maxMessageSize is the maximum cap on the size of a protocol message.
const maxMessageSize = 10 * 1024 * 1024
@ -66,6 +68,8 @@ const (
GetReceiptsMsg = 0x0f
ReceiptsMsg = 0x10
BlockRangeUpdateMsg = 0x11
GetBlockAccessListsMsg = 0x12
BlockAccessListsMsg = 0x13
)
var (
@ -288,6 +292,24 @@ type BlockRangeUpdatePacket struct {
LatestBlockHash common.Hash
}
type GetBlockAccessListsRequest []common.Hash
type GetBlockAccessListsPacket struct {
RequestId uint64
GetBlockAccessListsRequest
}
type RawBlockAccessList struct {
rlp.RawList[bal.AccountAccess]
}
type BlockAccessListResponse []RawBlockAccessList
type BlockAccessListPacket struct {
RequestId uint64
List rlp.RawList[RawBlockAccessList]
}
func (*StatusPacket) Name() string { return "Status" }
func (*StatusPacket) Kind() byte { return StatusMsg }
@ -326,3 +348,9 @@ func (*ReceiptsRLPResponse) Kind() byte { return ReceiptsMsg }
func (*BlockRangeUpdatePacket) Name() string { return "BlockRangeUpdate" }
func (*BlockRangeUpdatePacket) Kind() byte { return BlockRangeUpdateMsg }
func (*GetBlockAccessListsRequest) Name() string { return "GetBlockAccessLists" }
func (*GetBlockAccessListsRequest) Kind() byte { return GetBlockAccessListsMsg }
func (*BlockAccessListResponse) Name() string { return "BlockAccessLists" }
func (*BlockAccessListResponse) Kind() byte { return BlockAccessListsMsg }