beacon/engine, eth/catalyst, miner: add GetInclusionListV1

This commit is contained in:
Jihoon Song 2025-06-16 18:40:03 +02:00
parent 1dab670156
commit 7ce64f1d65
5 changed files with 307 additions and 7 deletions

View file

@ -46,3 +46,23 @@ func (inclusionList *InclusionList) UnmarshalJSON(input []byte) error {
return nil
}
func InclusionListToTransactions(inclusionList InclusionList) []*Transaction {
var txs []*Transaction
for _, encTx := range inclusionList {
var tx Transaction
if err := tx.UnmarshalBinary(encTx); err != nil {
continue // Skip invalid transactions
}
txs = append(txs, &tx)
}
return txs
}
func TransactionsToInclusionList(txs []*Transaction) InclusionList {
var enc = make([][]byte, len(txs))
for i, tx := range txs {
enc[i], _ = tx.MarshalBinary()
}
return enc
}

View file

@ -81,6 +81,42 @@ const (
beaconUpdateWarnFrequency = 5 * time.Minute
)
// All methods provided over the engine endpoint.
var caps = []string{
"engine_forkchoiceUpdatedV1",
"engine_forkchoiceUpdatedV2",
"engine_forkchoiceUpdatedV3",
"engine_forkchoiceUpdatedWithWitnessV1",
"engine_forkchoiceUpdatedWithWitnessV2",
"engine_forkchoiceUpdatedWithWitnessV3",
"engine_exchangeTransitionConfigurationV1",
"engine_getPayloadV1",
"engine_getPayloadV2",
"engine_getPayloadV3",
"engine_getPayloadV4",
"engine_getPayloadV5",
"engine_getBlobsV1",
"engine_getBlobsV2",
"engine_getInclusionListV1",
"engine_newPayloadV1",
"engine_newPayloadV2",
"engine_newPayloadV3",
"engine_newPayloadV4",
"engine_newPayloadWithWitnessV1",
"engine_newPayloadWithWitnessV2",
"engine_newPayloadWithWitnessV3",
"engine_newPayloadWithWitnessV4",
"engine_executeStatelessPayloadV1",
"engine_executeStatelessPayloadV2",
"engine_executeStatelessPayloadV3",
"engine_executeStatelessPayloadV4",
"engine_getPayloadBodiesByHashV1",
"engine_getPayloadBodiesByHashV2",
"engine_getPayloadBodiesByRangeV1",
"engine_getPayloadBodiesByRangeV2",
"engine_getClientVersionV1",
}
var (
// Number of blobs requested via getBlobsV2
getBlobsRequestedCounter = metrics.NewRegisteredCounter("engine/getblobs/requested", nil)
@ -100,6 +136,7 @@ type ConsensusAPI struct {
remoteBlocks *headerQueue // Cache of remote payloads received
localBlocks *payloadQueue // Cache of local payloads generated
localInclusionLists *inclusionListQueue // Cache of inclusion list generated
// The forkchoice update and new payload method require us to return the
// latest valid hash in an invalid chain. To support that return, we need
@ -152,6 +189,7 @@ func newConsensusAPIWithoutHeartbeat(eth *eth.Ethereum) *ConsensusAPI {
eth: eth,
remoteBlocks: newHeaderQueue(),
localBlocks: newPayloadQueue(),
localInclusionLists: newInclusionListQueue(),
invalidBlocksHits: make(map[common.Hash]int),
invalidTipsets: make(map[common.Hash]*types.Header),
}
@ -589,6 +627,25 @@ func (api *ConsensusAPI) GetBlobsV2(hashes []common.Hash) ([]*engine.BlobAndProo
return res, nil
}
func (api *ConsensusAPI) GetInclusionListV1(parentHash common.Hash) (types.InclusionList, error) {
if inclusionList := api.localInclusionLists.get(parentHash); inclusionList != nil {
return inclusionList, nil
}
args := &miner.BuildInclusionListArgs{
Parent: parentHash,
}
inclusionList, err := api.eth.Miner().BuildInclusionList(args)
if err != nil {
log.Error("Failed to build inclusion list", "err", err)
return nil, err
}
api.localInclusionLists.put(parentHash, inclusionList)
return inclusionList, nil
}
// Helper for NewPayload* methods.
var invalidStatus = engine.PayloadStatusV1{Status: engine.INVALID}

View file

@ -38,6 +38,10 @@ const maxTrackedPayloads = 10
// limit.
const maxTrackedHeaders = 96
// maxTrackedInclusionLists is the maximum number of inclusion lists the execution
// engine tracks before evicting old ones.
const maxTrackedInclusionLists = 8
// payloadQueueItem represents an id->payload tuple to store until it's retrieved
// or evicted.
type payloadQueueItem struct {
@ -156,3 +160,54 @@ func (q *headerQueue) get(hash common.Hash) *types.Header {
}
return nil
}
// inclusionListQueueItem represents an hash->inclusionList tuple to store until it's retrieved
// or evicted.
type inclusionListQueueItem struct {
parentHash common.Hash
inclusionList types.InclusionList
}
// inclusionListQueue tracks the latest handful of constructed inclusion lists to be retrieved
// by the beacon chain if inclusion list production is requested.
type inclusionListQueue struct {
inclusionLists []*inclusionListQueueItem
lock sync.RWMutex
}
// newinclusionListQueue creates a pre-initialized queue with a fixed number of slots
// all containing empty items.
func newInclusionListQueue() *inclusionListQueue {
return &inclusionListQueue{
inclusionLists: make([]*inclusionListQueueItem, maxTrackedInclusionLists),
}
}
// put inserts a new inclusion list into the queue at the given parent hash that
// the inclusion list is built upon.
func (q *inclusionListQueue) put(parentHash common.Hash, inclusionList types.InclusionList) {
q.lock.Lock()
defer q.lock.Unlock()
copy(q.inclusionLists[1:], q.inclusionLists)
q.inclusionLists[0] = &inclusionListQueueItem{
parentHash,
inclusionList,
}
}
// get retrieves a previously stored inclusion list item or nil if it does not exist.
func (q *inclusionListQueue) get(parentHash common.Hash) types.InclusionList {
q.lock.RLock()
defer q.lock.RUnlock()
for _, item := range q.inclusionLists {
if item == nil {
return nil // no more items
}
if item.parentHash == parentHash {
return item.inclusionList
}
}
return nil
}

View file

@ -0,0 +1,49 @@
package miner
import (
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/params"
)
// BuildInclusionListArgs contains the provided parameters for building inclusion list.
type BuildInclusionListArgs struct {
Parent common.Hash // The hash of the parent block to build the inclusion list upon
}
func (miner *Miner) BuildInclusionList(args *BuildInclusionListArgs) (types.InclusionList, error) {
genParams := &generateParams{
timestamp: uint64(time.Now().Unix()),
forceTime: false,
parentHash: args.Parent,
coinbase: miner.config.PendingFeeRecipient,
random: common.Hash{},
withdrawals: []*types.Withdrawal{},
beaconRoot: nil,
noTxs: false,
}
env, err := miner.prepareWork(genParams, false)
if err != nil {
return nil, err
}
if err := miner.fillTransactions(nil, env); err != nil {
return nil, err
}
inclusionListTxs := make([]*types.Transaction, 0)
inclusionListSize := uint64(0)
for _, tx := range env.txs {
if inclusionListSize+tx.Size() > params.MaxBytesPerInclusionList {
continue
}
inclusionListTxs = append(inclusionListTxs, tx)
inclusionListSize += tx.Size()
}
return types.TransactionsToInclusionList(inclusionListTxs), nil
}

View file

@ -0,0 +1,119 @@
package miner
import (
"crypto/ecdsa"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus"
"github.com/ethereum/go-ethereum/consensus/ethash"
"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/legacypool"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/params"
"github.com/stretchr/testify/require"
)
// testMinerBackend implements miner's Backend interface.
type testMinerBackend struct {
txPool *txpool.TxPool
chain *core.BlockChain
}
func (b *testMinerBackend) BlockChain() *core.BlockChain { return b.chain }
func (b *testMinerBackend) TxPool() *txpool.TxPool { return b.txPool }
func newTestMinerBackend(t *testing.T, genesis *core.Genesis, engine consensus.Engine) *testMinerBackend {
chain, err := core.NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, nil)
if err != nil {
t.Fatalf("core.NewBlockChain failed: %v", err)
}
testTxPoolConfig := legacypool.DefaultConfig
pool := legacypool.New(testTxPoolConfig, chain)
txpool, err := txpool.New(testTxPoolConfig.PriceLimit, chain, []txpool.SubPool{pool})
if err != nil {
t.Fatalf("txpool.New failed: %v", err)
}
return &testMinerBackend{
chain: chain,
txPool: txpool,
}
}
func newTestMiner(eth *testMinerBackend) *Miner {
return New(eth, DefaultConfig, eth.chain.Engine())
}
func newPendingTransactions(fromPrivateKey *ecdsa.PrivateKey, toAddress common.Address) []*types.Transaction {
signer := types.LatestSigner(params.TestChainConfig)
tx1 := types.MustSignNewTx(fromPrivateKey, signer, &types.AccessListTx{
ChainID: params.TestChainConfig.ChainID,
Nonce: 0,
To: &toAddress,
Value: big.NewInt(1000),
Gas: params.TxGas,
GasPrice: big.NewInt(params.InitialBaseFee),
})
pendingTxs = append(pendingTxs, tx1)
tx2 := types.MustSignNewTx(fromPrivateKey, signer, &types.LegacyTx{
Nonce: 1,
To: &toAddress,
Value: big.NewInt(1000),
Gas: params.TxGas,
GasPrice: big.NewInt(params.InitialBaseFee),
})
pendingTxs = append(pendingTxs, tx2)
return pendingTxs
}
func TestBuildInclusionList(t *testing.T) {
var (
// Test accounts
testBankKey, _ = crypto.GenerateKey()
testBankAddress = crypto.PubkeyToAddress(testBankKey.PublicKey)
testBankFunds = big.NewInt(1000000000000000000)
testUserKey, _ = crypto.GenerateKey()
testUserAddress = crypto.PubkeyToAddress(testUserKey.PublicKey)
// Test genesis and consensus engine
testGenesis = &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{testBankAddress: {Balance: testBankFunds}},
}
testEngine = ethash.NewFaker()
)
// Initialize miner and backend.
eth := newTestMinerBackend(t, testGenesis, testEngine)
miner := newTestMiner(eth)
// Add pending transactions to the pool.
pendingTxs := newPendingTransactions(testBankKey, testUserAddress)
eth.txPool.Add(pendingTxs, true)
// Build inclusion list.
args := &BuildInclusionListArgs{
Parent: eth.chain.CurrentBlock().Hash(),
}
inclusionList, err := miner.BuildInclusionList(args)
if err != nil {
t.Fatalf("Failed to build inclusion list %v", err)
}
// Verify inclusion list size.
inclusionListSize := uint64(0)
for _, tx := range inclusionList {
inclusionListSize += uint64(len(tx))
}
require.LessOrEqual(t, inclusionListSize, params.MaxBytesPerInclusionList)
}