diff --git a/beacon/engine/gen_blockparams.go b/beacon/engine/gen_blockparams.go index b1f01b50ff..1d5d0735c4 100644 --- a/beacon/engine/gen_blockparams.go +++ b/beacon/engine/gen_blockparams.go @@ -21,6 +21,10 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + // + Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` + NoTxPool bool `json:"noTxPool,omitempty" gencodec:"optional"` + // } var enc PayloadAttributes enc.Timestamp = hexutil.Uint64(p.Timestamp) @@ -28,6 +32,15 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { enc.SuggestedFeeRecipient = p.SuggestedFeeRecipient enc.Withdrawals = p.Withdrawals enc.BeaconRoot = p.BeaconRoot + // + if p.Transactions != nil { + enc.Transactions = make([]hexutil.Bytes, len(p.Transactions)) + for k, v := range p.Transactions { + enc.Transactions[k] = v + } + } + enc.NoTxPool = p.NoTxPool + // return json.Marshal(&enc) } @@ -39,6 +52,10 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { SuggestedFeeRecipient *common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + // + Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` + NoTxPool *bool `json:"noTxPool,omitempty" gencodec:"optional"` + // } var dec PayloadAttributes if err := json.Unmarshal(input, &dec); err != nil { @@ -62,5 +79,16 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { if dec.BeaconRoot != nil { p.BeaconRoot = dec.BeaconRoot } + // + if dec.Transactions != nil { + p.Transactions = make([][]byte, len(dec.Transactions)) + for k, v := range dec.Transactions { + p.Transactions[k] = v + } + } + if dec.NoTxPool != nil { + p.NoTxPool = *dec.NoTxPool + } + // return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 67f30d4455..92022785f9 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -36,11 +36,21 @@ type PayloadAttributes struct { SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + // + // Transactions is a field for L2s: the transactions list is forced into the block + Transactions [][]byte `json:"transactions,omitempty" gencodec:"optional"` + // NoTxPool is a field for L2s: if true, the no transactions are taken out of the tx-pool, + // only transactions from the above Transactions list will be included. + NoTxPool bool `json:"noTxPool,omitempty" gencodec:"optional"` + // } // JSON type overrides for PayloadAttributes. type payloadAttributesMarshaling struct { Timestamp hexutil.Uint64 + // + Transactions []hexutil.Bytes + // } //go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go diff --git a/cmd/geth/main.go b/cmd/geth/main.go index f6fa47ad2e..e847d2b800 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -144,6 +144,9 @@ var ( utils.GpoPercentileFlag, utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, + // + utils.EnableL2EngineApiFlag, + // configFileFlag, }, utils.NetworkFlags, utils.DatabasePathFlags) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index f5f131951a..3ae4991d75 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -949,6 +949,14 @@ Please note that --` + MetricsHTTPFlag.Name + ` must be set to start the server. Value: metrics.DefaultConfig.InfluxDBOrganization, Category: flags.MetricsCategory, } + + // + EnableL2EngineApiFlag = &cli.BoolFlag{ + Name: "enableL2EngineApi", + Usage: "Enable L2 Engine API", + Category: flags.EthCategory, + } + // ) var ( @@ -1574,6 +1582,12 @@ func setMiner(ctx *cli.Context, cfg *miner.Config) { if ctx.IsSet(MinerNewPayloadTimeout.Name) { cfg.NewPayloadTimeout = ctx.Duration(MinerNewPayloadTimeout.Name) } + // + cfg.EnableL2EngineApi = ctx.Bool(EnableL2EngineApiFlag.Name) + if cfg.EnableL2EngineApi { + log.Info("L2 Engine API enabled") + } + // } func setRequiredBlocks(ctx *cli.Context, cfg *ethconfig.Config) { diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 08cce0558b..0254032113 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -320,7 +320,9 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl // If the specified head matches with our local head, do nothing and keep // generating the payload. It's a special corner case that a few slots are // missing and we are requested to generate the payload in slot. - } else { + // + } else if !api.eth.Miner().IsL2EngineApiEnabled() { // minor Engine API divergence: allow proposers to reorg their own chain + // // If the head block is already in our canonical chain, the beacon client is // probably resyncing. Ignore the update. log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number) @@ -364,6 +366,17 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl // sealed by the beacon client. The payload will be requested later, and we // will replace it arbitrarily many times in between. if payloadAttributes != nil { + // + transactions := make(types.Transactions, 0, len(payloadAttributes.Transactions)) + for i, otx := range payloadAttributes.Transactions { + var tx types.Transaction + if err := tx.UnmarshalBinary(otx); err != nil { + return engine.STATUS_INVALID, fmt.Errorf("transaction %d is not valid: %v", i, err) + } + transactions = append(transactions, &tx) + } + // + args := &miner.BuildPayloadArgs{ Parent: update.HeadBlockHash, Timestamp: payloadAttributes.Timestamp, @@ -371,6 +384,10 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl Random: payloadAttributes.Random, Withdrawals: payloadAttributes.Withdrawals, BeaconRoot: payloadAttributes.BeaconRoot, + // + NoTxPool: payloadAttributes.NoTxPool, + Transactions: transactions, + // } id := args.Id() // If we already are busy generating this work, then we do not need @@ -716,6 +733,12 @@ func (api *ConsensusAPI) invalid(err error, latestValid *types.Header) engine.Pa // // TODO(karalabe): Spin this goroutine down somehow func (api *ConsensusAPI) heartbeat() { + // + if api.eth.Miner().IsL2EngineApiEnabled() { // don't start the api heartbeat, there is no transition + return + } + // + // Sleep a bit on startup since there's obviously no beacon client yet // attached, so no need to print scary warnings to the user. time.Sleep(beaconUpdateStartupTimeout) diff --git a/miner/miner.go b/miner/miner.go index b7273948f5..9f62653956 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -18,6 +18,7 @@ package miner import ( + "context" "fmt" "math/big" "sync" @@ -31,6 +32,7 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -43,6 +45,13 @@ type Backend interface { TxPool() *txpool.TxPool } +// +type BackendWithHistoricalState interface { + StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) +} + +// + // Config is the configuration parameters of mining. type Config struct { Etherbase common.Address `toml:",omitempty"` // Public address for block mining rewards @@ -53,6 +62,10 @@ type Config struct { Recommit time.Duration // The time interval for miner to re-create mining work. NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload + + // + EnableL2EngineApi bool `toml:",omitempty"` + // } // DefaultConfig contains default settings for miner. @@ -244,3 +257,10 @@ func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscript func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) { return miner.worker.buildPayload(args) } + +// +func (miner *Miner) IsL2EngineApiEnabled() bool { + return miner.worker.config.EnableL2EngineApi +} + +// diff --git a/miner/payload_building.go b/miner/payload_building.go index 7d8c4368bf..6f49954da1 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -41,6 +41,10 @@ type BuildPayloadArgs struct { Random common.Hash // The provided randomness value Withdrawals types.Withdrawals // The provided withdrawals BeaconRoot *common.Hash // The provided beaconRoot (Cancun) + // + NoTxPool bool // Specular addition: option to disable tx pool contents from being included + Transactions []*types.Transaction // Specular addition: txs forced into the block via engine API + // } // Id computes an 8-byte identifier by hashing the components of the payload arguments. @@ -55,6 +59,16 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID { if args.BeaconRoot != nil { hasher.Write(args.BeaconRoot[:]) } + // + if args.NoTxPool || len(args.Transactions) > 0 { // extend if extra payload attributes are used + binary.Write(hasher, binary.BigEndian, args.NoTxPool) + binary.Write(hasher, binary.BigEndian, uint64(len(args.Transactions))) + for _, tx := range args.Transactions { + h := tx.Hash() + hasher.Write(h[:]) + } + } + // var out engine.PayloadID copy(out[:], hasher.Sum(nil)[:8]) return out @@ -188,6 +202,9 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { withdrawals: args.Withdrawals, beaconRoot: args.BeaconRoot, noTxs: true, + // + txs: args.Transactions, + // } empty := w.getSealingBlock(emptyParams) if empty.err != nil { @@ -196,6 +213,14 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { // Construct a payload object for return. payload := newPayload(empty.block, args.Id()) + // + if args.NoTxPool { // don't start the background payload updating job if there is no tx pool to pull from + // make sure to make it appear as full, otherwise it will wait indefinitely for payload building to complete. + payload.full = empty.block + payload.fullFees = empty.fees + return payload, nil + } + // // Spin up a routine for updating the payload in background. This strategy // can maximum the revenue for including transactions with highest fee. diff --git a/miner/worker.go b/miner/worker.go index 711149232b..09c2ae84ea 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -17,6 +17,7 @@ package miner import ( + "context" "errors" "fmt" "math/big" @@ -33,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -705,6 +707,18 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co // Retrieve the parent state to execute on top and start a prefetcher for // the miner to speed block sealing up a bit. state, err := w.chain.StateAt(parent.Root) + // + if w.config.EnableL2EngineApi { // Allow the miner to reorg its own chain arbitrarily deep + if historicalBackend, ok := w.eth.(BackendWithHistoricalState); ok { + var release tracers.StateReleaseFunc + parentBlock := w.eth.BlockChain().GetBlockByHash(parent.Hash()) + state, release, err = historicalBackend.StateAtBlock(context.Background(), parentBlock, ^uint64(0), nil, false, false) + state = state.Copy() + release() + } + } + // + if err != nil { return nil, err } @@ -885,6 +899,9 @@ type generateParams struct { withdrawals types.Withdrawals // List of withdrawals to include in block. beaconRoot *common.Hash // The beacon root (cancun field). noTxs bool // Flag whether an empty block without any transaction is expected + // + txs types.Transactions // Transactions to include at the start of the block + // } // prepareWork constructs the sealing task according to the given parameters, @@ -921,9 +938,12 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { Coinbase: genParams.coinbase, } // Set the extra field. - if len(w.extra) != 0 { + // + if len(w.extra) != 0 && !w.config.EnableL2EngineApi { // L2 chains must not set any extra data. header.Extra = w.extra } + // + // Set the randomness field from the beacon chain if it's available. if genParams.random != (common.Hash{}) { header.MixDigest = genParams.random @@ -1008,6 +1028,18 @@ func (w *worker) generateWork(params *generateParams) *newPayloadResult { return &newPayloadResult{err: err} } defer work.discard() + // + for _, tx := range params.txs { + from, _ := types.Sender(work.signer, tx) + work.state.SetTxContext(tx.Hash(), work.tcount) + _, err := w.commitTransaction(work, tx) + if err != nil { + return &newPayloadResult{err: fmt.Errorf("failed to force-include tx: %s type: %d sender: %s nonce: %d, err: %w", tx.Hash(), tx.Type(), from, tx.Nonce(), err)} + } + work.tcount++ + } + // forced transactions done, fill rest of block with transactions + // if !params.noTxs { interrupt := new(atomic.Int32)