Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial FOCIL implementation #30914

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1deb36b
beacon/engine, eth/catalyst, miner: add `GetInclusionListV1`
jihoonsong Nov 25, 2024
256a0b5
eth/catalyst: cache recently generated inclusion lists
jihoonsong Nov 25, 2024
1277d84
miner: add an unit test for building an inclusion list
jihoonsong Nov 26, 2024
f1df790
beacon/engine: add `INVALID_INCLUSION_LIST` to payload status
jihoonsong Nov 26, 2024
2402e1d
beacon/engine, miner: add helpers for the conversion between txs and …
jihoonsong Nov 27, 2024
4221635
eth/catalyst: add `engine_newPayloadV5`
jihoonsong Nov 28, 2024
be77496
eth/catalyst: verify if a block satisfies the inclusion list constraints
jihoonsong Nov 28, 2024
7e6b95c
eth/catalyst: add an unit test for verifying new payload against incl…
jihoonsong Nov 28, 2024
cbcdd34
miner: add `inclusionList` to `Miner.generateParams`
jihoonsong Nov 30, 2024
1bfcf6c
miner: add `inclusionList` to `Miner.environment`
jihoonsong Nov 30, 2024
a306697
miner: include inclusion list transactions when building a payload
jihoonsong Nov 30, 2024
4b236ef
miner: add a public method to notify the inclusion list to payload
jihoonsong Nov 30, 2024
f0a80ca
eth/catalyst: add `peak` method to `payloadQueue` to return `Miner.Pa…
jihoonsong Nov 30, 2024
7370523
beacon/engine, eth/catalyst: add `engine_updatePayloadWithInclusionLi…
jihoonsong Nov 30, 2024
eb6f820
eth/catalyst: add an unit test for updating payload with inclusion list
jihoonsong Nov 30, 2024
9842c15
eth/catalyst: unify inclusion list unit tests
jihoonsong Nov 30, 2024
33efbee
eth/catalyst: rely on CL whether to enforce IL constraints or not
jihoonsong Dec 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions beacon/engine/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ var (
// - newPayloadV1: if the payload was accepted, but not processed (side chain)
ACCEPTED = "ACCEPTED"

// INVALID_INCLUSION_LIST is returned by the engine API in the following calls:
// - newPayloadV5: if the payload failed to satisfy the inclusion list constraints
INVALID_INCLUSION_LIST = "INVALID_INCLUSION_LIST"

GenericServerError = &EngineAPIError{code: -32000, msg: "Server error"}
UnknownPayload = &EngineAPIError{code: -38001, msg: "Unknown payload"}
InvalidForkChoiceState = &EngineAPIError{code: -38002, msg: "Invalid forkchoice state"}
Expand Down
26 changes: 26 additions & 0 deletions beacon/engine/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ type executionPayloadEnvelopeMarshaling struct {
Requests []hexutil.Bytes
}

// Max size of inclusion list in bytes.
const MaxBytesPerInclusionList = uint64(8192)

type InclusionListV1 struct {
Transactions [][]byte `json:"transactions" gencodec:"required"`
}

type UpdateInclusionListResponse struct {
PayloadID *PayloadID `json:"payloadId"`
}

type PayloadStatusV1 struct {
Status string `json:"status"`
Witness *hexutil.Bytes `json:"witness"`
Expand Down Expand Up @@ -344,6 +355,21 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types.
}
}

func InclusionListToTransactions(inclusionList *InclusionListV1) ([]*types.Transaction, error) {
txs, err := decodeTransactions(inclusionList.Transactions)
if err != nil {
return nil, err
}

return txs, nil
}

func TransactionsToInclusionList(txs []*types.Transaction) *InclusionListV1 {
return &InclusionListV1{
Transactions: encodeTransactions(txs),
}
}

// ExecutionPayloadBody is used in the response to GetPayloadBodiesByHash and GetPayloadBodiesByRange
type ExecutionPayloadBody struct {
TransactionData []hexutil.Bytes `json:"transactions"`
Expand Down
211 changes: 190 additions & 21 deletions eth/catalyst/api.go

Large diffs are not rendered by default.

140 changes: 139 additions & 1 deletion eth/catalyst/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ func setupBlocks(t *testing.T, ethservice *eth.Ethereum, n int, parent *types.He
}

envelope := getNewEnvelope(t, api, parent, w, h)
execResp, err := api.newPayload(*envelope.ExecutionPayload, []common.Hash{}, h, envelope.Requests, false)
execResp, err := api.newPayload(*envelope.ExecutionPayload, []common.Hash{}, h, envelope.Requests, nil, false)
if err != nil {
t.Fatalf("can't execute payload: %v", err)
}
Expand Down Expand Up @@ -1820,3 +1820,141 @@ func TestGetClientVersion(t *testing.T) {
t.Fatalf("client info does match expected, got %s", info.String())
}
}

func TestInclusionList(t *testing.T) {
genesis, blocks := generateMergeChain(100, false)

// Set cancun time to last block + 5 seconds
time := blocks[len(blocks)-1].Time() + 5
genesis.Config.ShanghaiTime = &time
genesis.Config.CancunTime = &time

n, ethservice := startEthService(t, genesis, blocks)
defer n.Close()

api := NewConsensusAPI(ethservice)
parent := ethservice.BlockChain().CurrentHeader()
statedb, _ := ethservice.BlockChain().StateAt(parent.Root)

// Prepare transactions.
signer := types.LatestSigner(ethservice.BlockChain().Config())
testUserKey, _ := crypto.GenerateKey()
testUserAddress := crypto.PubkeyToAddress(testUserKey.PublicKey)
validTx1 := types.MustSignNewTx(testKey, signer, &types.LegacyTx{
Nonce: statedb.GetNonce(testAddr),
To: &testUserAddress,
Value: big.NewInt(1000),
Gas: params.TxGas,
GasPrice: big.NewInt(params.InitialBaseFee),
})
validTx2 := types.MustSignNewTx(testKey, signer, &types.AccessListTx{
ChainID: ethservice.BlockChain().Config().ChainID,
Nonce: statedb.GetNonce(testAddr) + 1,
To: &testUserAddress,
Value: big.NewInt(1000),
Gas: params.TxGas,
GasPrice: big.NewInt(params.InitialBaseFee),
})
invalidTx := types.MustSignNewTx(testUserKey, signer, &types.AccessListTx{
ChainID: ethservice.BlockChain().Config().ChainID,
Nonce: statedb.GetNonce(testUserAddress),
To: &testAddr,
Value: big.NewInt(1000),
Gas: params.TxGas,
GasPrice: big.NewInt(params.InitialBaseFee),
}) // This tx is invalid as `testUserAddress` has insufficient funds for `gas` * `price` + `value`.

// Add `validTx1` to the pool.
ethservice.TxPool().Add([]*types.Transaction{validTx1}, true, true)

for i, tt := range []*struct {
name string
inclusionList *engine.InclusionListV1
updateInclusionList bool
expectedTransactions int
expectedStatus string
}{
{
name: "Payload misses one transaction in the inclusion list, which could have been included",
inclusionList: engine.TransactionsToInclusionList([]*types.Transaction{validTx1, validTx2}),
updateInclusionList: false,
expectedTransactions: 1,
expectedStatus: engine.INVALID_INCLUSION_LIST,
},
{
name: "All transactions in the inclusion list are already included in the payload before update",
inclusionList: engine.TransactionsToInclusionList([]*types.Transaction{validTx1}),
updateInclusionList: true,
expectedTransactions: 1,
expectedStatus: engine.VALID,
},
{
name: "All transactions in the inclusion list that are not included in the payload before update",
inclusionList: engine.TransactionsToInclusionList([]*types.Transaction{validTx2}),
updateInclusionList: true,
expectedTransactions: 2, // `validTx1` from the pool and `validTx2` from the inclusion list
expectedStatus: engine.VALID,
},
{
name: "Payload includes all valid transactions in the inclusion list",
inclusionList: engine.TransactionsToInclusionList([]*types.Transaction{validTx1, validTx2, invalidTx}),
updateInclusionList: true,
expectedTransactions: 2,
expectedStatus: engine.VALID,
},
} {
t.Run(tt.name, func(t *testing.T) {
// Build Shanghai block.
blockParams := engine.PayloadAttributes{
Timestamp: parent.Time + 5 + uint64(i),
Random: crypto.Keccak256Hash([]byte{byte(0)}),
SuggestedFeeRecipient: parent.Coinbase,
Withdrawals: make([]*types.Withdrawal, 0),
BeaconRoot: &common.Hash{42},
}
fcState := engine.ForkchoiceStateV1{
HeadBlockHash: parent.Hash(),
SafeBlockHash: common.Hash{},
FinalizedBlockHash: common.Hash{},
}

var (
payload *engine.ExecutionPayloadEnvelope
resp engine.ForkChoiceResponse
err error
)

// Start building the payload.
if resp, err = api.ForkchoiceUpdatedV3(fcState, &blockParams); err != nil {
t.Fatalf("error preparing payload, err=%v", err)
}
if resp.PayloadStatus.Status != engine.VALID {
t.Fatalf("error preparing payload, invalid status=%v", resp.PayloadStatus.Status)
}

if tt.updateInclusionList {
// Update the payload with the inclusion list.
api.UpdatePayloadWithInclusionListV1(*resp.PayloadID, *tt.inclusionList)
}

// Get the payload.
if payload, err = api.getPayload(*resp.PayloadID, true); err != nil {
t.Fatalf("error getting payload, err=%v", err)
}

// Verify if the block contains all valid transactions in the inclusion list.
if len(payload.ExecutionPayload.Transactions) != tt.expectedTransactions {
t.Fatalf("expected %d transactions but got %d", tt.expectedTransactions, len(payload.ExecutionPayload.Transactions))
}

// Verify if the block satisfies the inclusion list constraints.
status, err := api.newPayload(*payload.ExecutionPayload, []common.Hash{}, &common.Hash{42}, nil, tt.inclusionList, false)
if err != nil {
t.Fatalf("error validating payload, err=%v", err)
}
if status.Status != tt.expectedStatus {
t.Fatalf("expected status %v but got %v", tt.expectedStatus, status.Status)
}
})
}
}
71 changes: 71 additions & 0 deletions eth/catalyst/queue.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -91,6 +95,22 @@ func (q *payloadQueue) get(id engine.PayloadID, full bool) *engine.ExecutionPayl
return nil
}

// peak retrieves a previously stored payload itself or nil if it does not exist.
func (q *payloadQueue) peak(id engine.PayloadID) *miner.Payload {
q.lock.RLock()
defer q.lock.RUnlock()

for _, item := range q.payloads {
if item == nil {
return nil // no more items
}
if item.id == id {
return item.payload
}
}
return nil
}

// has checks if a particular payload is already tracked.
func (q *payloadQueue) has(id engine.PayloadID) bool {
q.lock.RLock()
Expand Down Expand Up @@ -156,3 +176,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 *engine.InclusionListV1
}

// 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 *engine.InclusionListV1) {
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) *engine.InclusionListV1 {
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
}
2 changes: 1 addition & 1 deletion eth/catalyst/simulated_beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ func (c *SimulatedBeacon) sealBlock(withdrawals []*types.Withdrawal, timestamp u
}
}
// Mark the payload as canon
_, err = c.engineAPI.newPayload(*payload, blobHashes, &common.Hash{}, envelope.Requests, false)
_, err = c.engineAPI.newPayload(*payload, blobHashes, &common.Hash{}, envelope.Requests, nil, false)
if err != nil {
return err
}
Expand Down
49 changes: 49 additions & 0 deletions miner/inclusion_list_building.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package miner

import (
"time"

"github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

// BuildInclusionListArgs contains the provided parameters for building inclusion list.
type BuildInclusionListArgs struct {
Parent common.Hash // The parent block to build inclusion list on top
}

func (miner *Miner) BuildInclusionList(args *BuildInclusionListArgs) (*engine.InclusionListV1, error) {
params := &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(params, 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() > engine.MaxBytesPerInclusionList {
continue
}

inclusionListTxs = append(inclusionListTxs, tx)
inclusionListSize += tx.Size()
}

return engine.TransactionsToInclusionList(inclusionListTxs), nil
}
Loading