From a484a8506dd5106f14bc28cd0fb9c4ab74d6c69d Mon Sep 17 00:00:00 2001 From: Bosul Mun Date: Tue, 19 May 2026 20:25:13 +0200 Subject: [PATCH] eth/protocols/eth: implement eth71 bal response (#34879) 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 --- core/blockchain_reader.go | 1 + eth/protocols/eth/handler.go | 21 ++++++++++ eth/protocols/eth/handler_test.go | 62 +++++++++++++++++++++++++++++ eth/protocols/eth/handlers.go | 65 +++++++++++++++++++++++++++++++ eth/protocols/eth/peer.go | 30 ++++++++++++++ eth/protocols/eth/protocol.go | 32 ++++++++++++++- 6 files changed, 209 insertions(+), 2 deletions(-) diff --git a/core/blockchain_reader.go b/core/blockchain_reader.go index 18afa9ce9d..a540bbc11d 100644 --- a/core/blockchain_reader.go +++ b/core/blockchain_reader.go @@ -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 { diff --git a/eth/protocols/eth/handler.go b/eth/protocols/eth/handler.go index f7d25bd8ca..154b75130c 100644 --- a/eth/protocols/eth/handler.go +++ b/eth/protocols/eth/handler.go @@ -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) } diff --git a/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index d056d121d9..3f40fdb3b3 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -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 } diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index 3254a0abc2..71942cc9ad 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -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) +} diff --git a/eth/protocols/eth/peer.go b/eth/protocols/eth/peer.go index 754fd02be3..2d7079fa12 100644 --- a/eth/protocols/eth/peer.go +++ b/eth/protocols/eth/peer.go @@ -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) { diff --git a/eth/protocols/eth/protocol.go b/eth/protocols/eth/protocol.go index 0df0776c27..a6c45f83ec 100644 --- a/eth/protocols/eth/protocol.go +++ b/eth/protocols/eth/protocol.go @@ -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 }