diff --git a/core/types/inclusion_list.go b/core/types/inclusion_list.go index 772fd43346..16e37ee617 100644 --- a/core/types/inclusion_list.go +++ b/core/types/inclusion_list.go @@ -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 +} diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 75b263bf6b..04cbab3bb9 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -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) @@ -98,8 +134,9 @@ var ( type ConsensusAPI struct { eth *eth.Ethereum - remoteBlocks *headerQueue // Cache of remote payloads received - localBlocks *payloadQueue // Cache of local payloads generated + 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 @@ -149,11 +186,12 @@ func newConsensusAPIWithoutHeartbeat(eth *eth.Ethereum) *ConsensusAPI { log.Warn("Engine API started but chain not configured for merge yet") } api := &ConsensusAPI{ - eth: eth, - remoteBlocks: newHeaderQueue(), - localBlocks: newPayloadQueue(), - invalidBlocksHits: make(map[common.Hash]int), - invalidTipsets: make(map[common.Hash]*types.Header), + eth: eth, + remoteBlocks: newHeaderQueue(), + localBlocks: newPayloadQueue(), + localInclusionLists: newInclusionListQueue(), + invalidBlocksHits: make(map[common.Hash]int), + invalidTipsets: make(map[common.Hash]*types.Header), } eth.Downloader().SetBadBlockCallback(api.setInvalidAncestor) return api @@ -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} diff --git a/eth/catalyst/queue.go b/eth/catalyst/queue.go index 634dc1b2e6..22fef5cd04 100644 --- a/eth/catalyst/queue.go +++ b/eth/catalyst/queue.go @@ -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 +} diff --git a/miner/inclusion_list_building.go b/miner/inclusion_list_building.go new file mode 100644 index 0000000000..18de2498c0 --- /dev/null +++ b/miner/inclusion_list_building.go @@ -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 +} diff --git a/miner/inclusion_list_building_test.go b/miner/inclusion_list_building_test.go new file mode 100644 index 0000000000..55cb233daa --- /dev/null +++ b/miner/inclusion_list_building_test.go @@ -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) +}