From c4d09bc9a71e2b227319d524f805b797ead5d78d Mon Sep 17 00:00:00 2001 From: protolambda Date: Fri, 1 Nov 2024 01:02:42 +0700 Subject: [PATCH] miner: support interop block building Co-authored-by: axelKingsley Co-authored-by: Tyler Smith --- cmd/geth/main.go | 1 + cmd/utils/flags.go | 9 ++ core/state_processor.go | 11 ++ core/state_transition.go | 9 ++ core/types/interoptypes/interop.go | 162 +++++++++++++++++++++++++++++ eth/backend.go | 22 ++++ eth/ethconfig/config.go | 2 + eth/ethconfig/gen_config.go | 6 ++ eth/interop/interop.go | 57 ++++++++++ fork.yaml | 1 + miner/miner.go | 16 +++ miner/payload_building.go | 21 +++- miner/worker.go | 84 ++++++++++++++- params/interop.go | 5 + 14 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 core/types/interoptypes/interop.go create mode 100644 eth/interop/interop.go create mode 100644 params/interop.go diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 2020df3991..45c44e77ae 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -159,6 +159,7 @@ var ( utils.RollupSequencerTxConditionalCostRateLimitFlag, utils.RollupHistoricalRPCFlag, utils.RollupHistoricalRPCTimeoutFlag, + utils.RollupInteropRPCFlag, utils.RollupDisableTxPoolGossipFlag, utils.RollupComputePendingBlock, utils.RollupHaltOnIncompatibleProtocolVersionFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 026bcf7e62..c68ba3d3ab 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -939,6 +939,12 @@ var ( Category: flags.RollupCategory, } + RollupInteropRPCFlag = &cli.StringFlag{ + Name: "rollup.interoprpc", + Usage: "RPC endpoint for interop message verification (experimental).", + Category: flags.RollupCategory, + } + RollupDisableTxPoolGossipFlag = &cli.BoolFlag{ Name: "rollup.disabletxpoolgossip", Usage: "Disable transaction pool gossip.", @@ -1941,6 +1947,9 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { if ctx.IsSet(RollupHistoricalRPCTimeoutFlag.Name) { cfg.RollupHistoricalRPCTimeout = ctx.Duration(RollupHistoricalRPCTimeoutFlag.Name) } + if ctx.IsSet(RollupInteropRPCFlag.Name) { + cfg.InteropMessageRPC = ctx.String(RollupInteropRPCFlag.Name) + } cfg.RollupDisableTxPoolGossip = ctx.Bool(RollupDisableTxPoolGossipFlag.Name) cfg.RollupDisableTxPoolAdmission = cfg.RollupSequencerHTTP != "" && !ctx.Bool(RollupEnableTxPoolAdmissionFlag.Name) cfg.RollupHaltOnIncompatibleProtocolVersion = ctx.String(RollupHaltOnIncompatibleProtocolVersionFlag.Name) diff --git a/core/state_processor.go b/core/state_processor.go index 5b41e7fa5a..a1042cf868 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -210,10 +210,21 @@ func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, b // for the transaction, gas used and an error if the transaction failed, // indicating the block was invalid. func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config) (*types.Receipt, error) { + return ApplyTransactionExtended(config, bc, author, gp, statedb, header, tx, usedGas, cfg, nil) +} + +type ApplyTransactionOpts struct { + PostValidation func(evm *vm.EVM, result *ExecutionResult) error +} + +func ApplyTransactionExtended(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, cfg vm.Config, extraOpts *ApplyTransactionOpts) (*types.Receipt, error) { msg, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee) if err != nil { return nil, err } + if extraOpts != nil { + msg.PostValidation = extraOpts.PostValidation + } // Create a new context to be used in the EVM environment blockContext := NewEVMBlockContext(header, bc, author, config, statedb) txContext := NewEVMTxContext(msg) diff --git a/core/state_transition.go b/core/state_transition.go index ce8d5aea94..2de1cad762 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -154,6 +154,8 @@ type Message struct { IsDepositTx bool // IsDepositTx indicates the message is force-included and can persist a mint. Mint *big.Int // Mint is the amount to mint before EVM processing, or nil if there is no minting. RollupCostData types.RollupCostData // RollupCostData caches data to compute the fee we charge for data availability + + PostValidation func(evm *vm.EVM, result *ExecutionResult) error } // TransactionToMessage converts a transaction into a Message. @@ -447,6 +449,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { } err = nil } + + if st.msg.PostValidation != nil { + if err := st.msg.PostValidation(st.evm, result); err != nil { + return nil, err + } + } + return result, err } diff --git a/core/types/interoptypes/interop.go b/core/types/interoptypes/interop.go new file mode 100644 index 0000000000..8c60ac0977 --- /dev/null +++ b/core/types/interoptypes/interop.go @@ -0,0 +1,162 @@ +package interoptypes + +import ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + + "github.com/holiman/uint256" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/params" +) + +var ExecutingMessageEventTopic = crypto.Keccak256Hash([]byte("ExecutingMessage(bytes32,(address,uint256,uint256,uint256,uint256))")) + +type Message struct { + Identifier Identifier `json:"identifier"` + PayloadHash common.Hash `json:"payloadHash"` +} + +func (m *Message) DecodeEvent(topics []common.Hash, data []byte) error { + if len(topics) != 2 { // event hash, indexed payloadHash + return fmt.Errorf("unexpected number of event topics: %d", len(topics)) + } + if topics[0] != ExecutingMessageEventTopic { + return fmt.Errorf("unexpected event topic %q", topics[0]) + } + if len(data) != 32*5 { + return fmt.Errorf("unexpected identifier data length: %d", len(data)) + } + take := func(length uint) []byte { + taken := data[:length] + data = data[length:] + return taken + } + takeZeroes := func(length uint) error { + for _, v := range take(length) { + if v != 0 { + return errors.New("expected zero") + } + } + return nil + } + if err := takeZeroes(12); err != nil { + return fmt.Errorf("invalid address padding: %w", err) + } + m.Identifier.Origin = common.Address(take(20)) + if err := takeZeroes(32 - 8); err != nil { + return fmt.Errorf("invalid block number padding: %w", err) + } + m.Identifier.BlockNumber = binary.BigEndian.Uint64(take(8)) + if err := takeZeroes(32 - 8); err != nil { + return fmt.Errorf("invalid log index padding: %w", err) + } + m.Identifier.LogIndex = binary.BigEndian.Uint64(take(8)) + if err := takeZeroes(32 - 8); err != nil { + return fmt.Errorf("invalid timestamp padding: %w", err) + } + m.Identifier.Timestamp = binary.BigEndian.Uint64(take(8)) + m.Identifier.ChainID.SetBytes32(take(32)) + m.PayloadHash = topics[1] + return nil +} + +func ExecutingMessagesFromLogs(logs []*types.Log) ([]Message, error) { + var executingMessages []Message + for i, l := range logs { + if l.Address == params.InteropCrossL2InboxAddress { + // ignore events that do not match this + if len(l.Topics) == 0 || l.Topics[0] != ExecutingMessageEventTopic { + continue + } + var msg Message + if err := msg.DecodeEvent(l.Topics, l.Data); err != nil { + return nil, fmt.Errorf("invalid executing message %d, tx-log %d: %w", len(executingMessages), i, err) + } + executingMessages = append(executingMessages, msg) + } + } + return executingMessages, nil +} + +type Identifier struct { + Origin common.Address + BlockNumber uint64 + LogIndex uint64 + Timestamp uint64 + ChainID uint256.Int // flat, not a pointer, to make Identifier safe as map key +} + +type identifierMarshaling struct { + Origin common.Address `json:"origin"` + BlockNumber hexutil.Uint64 `json:"blockNumber"` + LogIndex hexutil.Uint64 `json:"logIndex"` + Timestamp hexutil.Uint64 `json:"timestamp"` + ChainID hexutil.U256 `json:"chainID"` +} + +func (id Identifier) MarshalJSON() ([]byte, error) { + var enc identifierMarshaling + enc.Origin = id.Origin + enc.BlockNumber = hexutil.Uint64(id.BlockNumber) + enc.LogIndex = hexutil.Uint64(id.LogIndex) + enc.Timestamp = hexutil.Uint64(id.Timestamp) + enc.ChainID = (hexutil.U256)(id.ChainID) + return json.Marshal(&enc) +} + +func (id *Identifier) UnmarshalJSON(input []byte) error { + var dec identifierMarshaling + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + id.Origin = dec.Origin + id.BlockNumber = uint64(dec.BlockNumber) + id.LogIndex = uint64(dec.LogIndex) + id.Timestamp = uint64(dec.Timestamp) + id.ChainID = (uint256.Int)(dec.ChainID) + return nil +} + +type SafetyLevel string + +func (lvl SafetyLevel) String() string { + return string(lvl) +} + +func (lvl SafetyLevel) Valid() bool { + switch lvl { + case Finalized, Safe, CrossUnsafe, Unsafe: + return true + default: + return false + } +} + +func (lvl SafetyLevel) MarshalText() ([]byte, error) { + return []byte(lvl), nil +} + +func (lvl *SafetyLevel) UnmarshalText(text []byte) error { + if lvl == nil { + return errors.New("cannot unmarshal into nil SafetyLevel") + } + x := SafetyLevel(text) + if !x.Valid() { + return fmt.Errorf("unrecognized safety level: %q", text) + } + *lvl = x + return nil +} + +const ( + Finalized SafetyLevel = "finalized" + Safe SafetyLevel = "safe" + CrossUnsafe SafetyLevel = "cross-unsafe" + Unsafe SafetyLevel = "unsafe" +) diff --git a/eth/backend.go b/eth/backend.go index 860ecc9149..216e44da06 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -20,6 +20,7 @@ package eth import ( "context" "encoding/json" + "errors" "fmt" "math/big" "runtime" @@ -38,10 +39,12 @@ 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/interoptypes" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/gasprice" + "github.com/ethereum/go-ethereum/eth/interop" "github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/eth/tracers" @@ -79,6 +82,8 @@ type Ethereum struct { seqRPCService *rpc.Client historicalRPCService *rpc.Client + interopRPC *interop.InteropClient + // DB interfaces chainDb ethdb.Database // Block chain database @@ -320,6 +325,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { eth.historicalRPCService = client } + if config.InteropMessageRPC != "" { + eth.interopRPC = interop.NewInteropClient(config.InteropMessageRPC) + } + // Start the RPC service eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID) @@ -483,6 +492,12 @@ func (s *Ethereum) Stop() error { if s.historicalRPCService != nil { s.historicalRPCService.Close() } + if s.interopRPC != nil { + s.interopRPC.Close() + } + if s.miner != nil { + s.miner.Close() + } // Clean shutdown marker as the last thing before closing db s.shutdownTracker.Stop() @@ -548,3 +563,10 @@ func (s *Ethereum) HandleRequiredProtocolVersion(required params.ProtocolVersion } return nil } + +func (s *Ethereum) CheckMessages(ctx context.Context, messages []interoptypes.Message, minSafety interoptypes.SafetyLevel) error { + if s.interopRPC == nil { + return errors.New("cannot check interop messages, no RPC available") + } + return s.interopRPC.CheckMessages(ctx, messages, minSafety) +} diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index f3415b2284..dc5cb07528 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -181,6 +181,8 @@ type Config struct { RollupDisableTxPoolGossip bool RollupDisableTxPoolAdmission bool RollupHaltOnIncompatibleProtocolVersion string + + InteropMessageRPC string `toml:",omitempty"` } // CreateConsensusEngine creates a consensus engine for the given chain config. diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 864f4afdbd..16391019df 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -67,6 +67,7 @@ func (c Config) MarshalTOML() (interface{}, error) { RollupDisableTxPoolGossip bool RollupDisableTxPoolAdmission bool RollupHaltOnIncompatibleProtocolVersion string + InteropMessageRPC string `toml:",omitempty"` } var enc Config enc.Genesis = c.Genesis @@ -119,6 +120,7 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.RollupDisableTxPoolGossip = c.RollupDisableTxPoolGossip enc.RollupDisableTxPoolAdmission = c.RollupDisableTxPoolAdmission enc.RollupHaltOnIncompatibleProtocolVersion = c.RollupHaltOnIncompatibleProtocolVersion + enc.InteropMessageRPC = c.InteropMessageRPC return &enc, nil } @@ -175,6 +177,7 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { RollupDisableTxPoolGossip *bool RollupDisableTxPoolAdmission *bool RollupHaltOnIncompatibleProtocolVersion *string + InteropMessageRPC *string `toml:",omitempty"` } var dec Config if err := unmarshal(&dec); err != nil { @@ -330,5 +333,8 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.RollupHaltOnIncompatibleProtocolVersion != nil { c.RollupHaltOnIncompatibleProtocolVersion = *dec.RollupHaltOnIncompatibleProtocolVersion } + if dec.InteropMessageRPC != nil { + c.InteropMessageRPC = *dec.InteropMessageRPC + } return nil } diff --git a/eth/interop/interop.go b/eth/interop/interop.go new file mode 100644 index 0000000000..ae93189ac4 --- /dev/null +++ b/eth/interop/interop.go @@ -0,0 +1,57 @@ +package interop + +import ( + "context" + "errors" + "sync" + + "github.com/ethereum/go-ethereum/core/types/interoptypes" + "github.com/ethereum/go-ethereum/rpc" +) + +type InteropClient struct { + mu sync.Mutex + client *rpc.Client + endpoint string + closed bool // don't allow lazy-dials after Close +} + +// maybeDial dials the endpoint if it was not already. +func (cl *InteropClient) maybeDial(ctx context.Context) error { + cl.mu.Lock() + defer cl.mu.Unlock() + if cl.closed { + return errors.New("client is closed") + } + if cl.client != nil { + return nil + } + rpcClient, err := rpc.DialContext(ctx, cl.endpoint) + if err != nil { + return err + } + cl.client = rpcClient + return nil +} + +func (cl *InteropClient) Close() { + cl.mu.Lock() + defer cl.mu.Unlock() + if cl.client != nil { + cl.client.Close() + } + cl.closed = true +} + +func NewInteropClient(rpcEndpoint string) *InteropClient { + return &InteropClient{endpoint: rpcEndpoint} +} + +// CheckMessages checks if the given messages meet the given minimum safety level. +func (cl *InteropClient) CheckMessages(ctx context.Context, messages []interoptypes.Message, minSafety interoptypes.SafetyLevel) error { + // we lazy-dial the endpoint, so we can start geth, and build blocks, without supervisor endpoint availability. + if err := cl.maybeDial(ctx); err != nil { // a single dial attempt is made, the next call may retry. + return err + } + return cl.client.CallContext(ctx, nil, "supervisor_checkMessages", messages, minSafety) +} diff --git a/fork.yaml b/fork.yaml index 5ba914d9c7..36233e0c83 100644 --- a/fork.yaml +++ b/fork.yaml @@ -119,6 +119,7 @@ def: The block-building code (in the "miner" package because of Proof-Of-Work legacy of ethereum) implements the changes to support the transaction-inclusion, tx-pool toggle, gaslimit, and EIP-1559 parameters of the Engine API. + This also includes experimental support for interop executing-messages to be verified through an RPC. globs: - "miner/*" - title: "Tx-pool tx cost updates" diff --git a/miner/miner.go b/miner/miner.go index 14a6a7e1fa..7cbfebf564 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/interoptypes" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/params" ) @@ -46,6 +47,10 @@ type BackendWithHistoricalState interface { StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) } +type BackendWithInterop interface { + CheckMessages(ctx context.Context, messages []interoptypes.Message, minSafety interoptypes.SafetyLevel) error +} + // Config is the configuration parameters of mining. type Config struct { Etherbase common.Address `toml:"-"` // Deprecated @@ -88,10 +93,14 @@ type Miner struct { pendingMu sync.Mutex // Lock protects the pending block backend Backend + + lifeCtxCancel context.CancelFunc + lifeCtx context.Context } // New creates a new miner with provided config. func New(eth Backend, config Config, engine consensus.Engine) *Miner { + ctx, cancel := context.WithCancel(context.Background()) return &Miner{ backend: eth, config: &config, @@ -100,6 +109,9 @@ func New(eth Backend, config Config, engine consensus.Engine) *Miner { txpool: eth.TxPool(), chain: eth.BlockChain(), pending: &pending{}, + // To interrupt background tasks that may be attached to external processes + lifeCtxCancel: cancel, + lifeCtx: ctx, } } @@ -208,3 +220,7 @@ func (miner *Miner) getPending() *newPayloadResult { miner.pending.update(header.Hash(), ret) return ret } + +func (miner *Miner) Close() { + miner.lifeCtxCancel() +} diff --git a/miner/payload_building.go b/miner/payload_building.go index 30ecfff720..2dd574c15c 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -17,6 +17,7 @@ package miner import ( + "context" "crypto/sha256" "encoding/binary" "errors" @@ -106,10 +107,14 @@ type Payload struct { err error stopOnce sync.Once interrupt *atomic.Int32 // interrupt signal shared with worker + + rpcCtx context.Context // context to limit RPC-coupled payload checks + rpcCancel context.CancelFunc } // newPayload initializes the payload object. -func newPayload(empty *types.Block, witness *stateless.Witness, id engine.PayloadID) *Payload { +func newPayload(lifeCtx context.Context, empty *types.Block, witness *stateless.Witness, id engine.PayloadID) *Payload { + rpcCtx, rpcCancel := context.WithCancel(lifeCtx) payload := &Payload{ id: id, empty: empty, @@ -117,6 +122,9 @@ func newPayload(empty *types.Block, witness *stateless.Witness, id engine.Payloa stop: make(chan struct{}), interrupt: new(atomic.Int32), + + rpcCtx: rpcCtx, + rpcCancel: rpcCancel, } log.Info("Starting work on payload", "id", payload.id) payload.cond = sync.NewCond(&payload.lock) @@ -253,6 +261,7 @@ func (payload *Payload) resolve(onlyFull bool) *engine.ExecutionPayloadEnvelope // the update anyways. // interruptBuilding is safe to be called concurrently. func (payload *Payload) interruptBuilding() { + payload.rpcCancel() // Set the interrupt if not interrupted already. // It's ok if it has either already been interrupted by payload resolution earlier, // or by the timeout timer set to commitInterruptTimeout. @@ -269,6 +278,7 @@ func (payload *Payload) interruptBuilding() { // transactions with interruptBuilding. // stopBuilding is safe to be called concurrently. func (payload *Payload) stopBuilding() { + payload.rpcCancel() // Concurrent Resolve calls should only stop once. payload.stopOnce.Do(func() { log.Debug("Stop payload building.", "id", payload.id) @@ -295,12 +305,14 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload txs: args.Transactions, gasLimit: args.GasLimit, eip1559Params: args.EIP1559Params, + // No RPC requests allowed. + rpcCtx: nil, } empty := miner.generateWork(emptyParams, witness) if empty.err != nil { return nil, empty.err } - payload := newPayload(empty.block, empty.witness, args.Id()) + payload := newPayload(miner.lifeCtx, empty.block, empty.witness, args.Id()) // 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 @@ -329,9 +341,10 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload return nil, err } - payload := newPayload(nil, nil, args.Id()) + payload := newPayload(miner.lifeCtx, nil, nil, args.Id()) // set shared interrupt fullParams.interrupt = payload.interrupt + fullParams.rpcCtx = payload.rpcCtx // Spin up a routine for updating the payload in background. This strategy // can maximum the revenue for including transactions with highest fee. @@ -375,6 +388,8 @@ func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload var lastDuration time.Duration for { select { + case <-miner.lifeCtx.Done(): + stopReason = "miner-shutdown" case <-timer.C: // We have to prioritize the stop signal because the recommit timer // might have fired while stop also got closed. diff --git a/miner/worker.go b/miner/worker.go index 864c5ae2ff..0c5161c8c6 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/interoptypes" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/log" @@ -57,6 +58,8 @@ var ( txConditionalRejectedCounter = metrics.NewRegisteredCounter("miner/transactionConditional/rejected", nil) txConditionalMinedTimer = metrics.NewRegisteredTimer("miner/transactionConditional/elapsedtime", nil) + + txInteropRejectedCounter = metrics.NewRegisteredCounter("miner/transactionInterop/rejected", nil) ) // environment is the worker's current environment and holds all @@ -75,6 +78,9 @@ type environment struct { blobs int witness *stateless.Witness + + noTxs bool // true if we are reproducing a block, and do not have to check interop txs + rpcCtx context.Context // context to control block-building RPC work. No RPC allowed if nil. } const ( @@ -112,6 +118,8 @@ type generateParams struct { eip1559Params []byte // Optional EIP-1559 parameters interrupt *atomic.Int32 // Optional interruption signal to pass down to worker.generateWork isUpdate bool // Optional flag indicating that this is building a discardable update + + rpcCtx context.Context // context to control block-building RPC work. No RPC allowed if nil. } // generateWork generates a sealing block based on the given parameters. @@ -288,11 +296,12 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir // Could potentially happen if starting to mine in an odd state. // Note genParams.coinbase can be different with header.Coinbase // since clique algorithm can modify the coinbase field in header. - env, err := miner.makeEnv(parent, header, genParams.coinbase, witness) + env, err := miner.makeEnv(parent, header, genParams.coinbase, witness, genParams.rpcCtx) if err != nil { log.Error("Failed to create sealing context", "err", err) return nil, err } + env.noTxs = genParams.noTxs if header.ParentBeaconRoot != nil { context := core.NewEVMBlockContext(header, miner.chain, nil, miner.chainConfig, env.state) vmenv := vm.NewEVM(context, vm.TxContext{}, env.state, miner.chainConfig, vm.Config{}) @@ -307,7 +316,7 @@ func (miner *Miner) prepareWork(genParams *generateParams, witness bool) (*envir } // makeEnv creates a new environment for the sealing block. -func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, witness bool) (*environment, error) { +func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, witness bool, rpcCtx context.Context) (*environment, error) { // Retrieve the parent state to execute on top. state, err := miner.chain.StateAt(parent.Root) if err != nil { @@ -340,6 +349,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase coinbase: coinbase, header: header, witness: state.Witness(), + rpcCtx: rpcCtx, }, nil } @@ -396,13 +406,37 @@ func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transactio return nil } +type LogInspector interface { + GetLogs(hash common.Hash, blockNumber uint64, blockHash common.Hash) []*types.Log +} + // applyTransaction runs the transaction. If execution fails, state and gas pool are reverted. func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (*types.Receipt, error) { var ( snap = env.state.Snapshot() gp = env.gasPool.Gas() ) - receipt, err := core.ApplyTransaction(miner.chainConfig, miner.chain, &env.coinbase, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, vm.Config{}) + var extraOpts *core.ApplyTransactionOpts + // If not just reproducing the block, check the interop executing messages. + if !env.noTxs && miner.chain.Config().IsInterop(env.header.Time) { + // Whenever there are `noTxs` it means we are building a block from pre-determined txs. There are two cases: + // (1) it's derived from L1, and will be verified asynchronously by the op-node. + // (2) it is a deposits-only empty-block by the sequencer, in which case there are no interop-txs to verify (as deposits do not emit any). + + // We have to insert as call-back, since we cannot revert the snapshot + // after the tx is deemed successful and the journal has been cleared already. + extraOpts = &core.ApplyTransactionOpts{ + PostValidation: func(evm *vm.EVM, result *core.ExecutionResult) error { + logInspector, ok := evm.StateDB.(LogInspector) + if !ok { + return fmt.Errorf("cannot get logs from StateDB type %T", evm.StateDB) + } + logs := logInspector.GetLogs(tx.Hash(), env.header.Number.Uint64(), common.Hash{}) + return miner.checkInterop(env.rpcCtx, tx, result.Failed(), logs) + }, + } + } + receipt, err := core.ApplyTransactionExtended(miner.chainConfig, miner.chain, &env.coinbase, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, vm.Config{}, extraOpts) if err != nil { env.state.RevertToSnapshot(snap) env.gasPool.SetGas(gp) @@ -410,6 +444,42 @@ func (miner *Miner) applyTransaction(env *environment, tx *types.Transaction) (* return receipt, err } +func (miner *Miner) checkInterop(ctx context.Context, tx *types.Transaction, failed bool, logs []*types.Log) error { + if tx.Type() == types.DepositTxType { + return nil // deposit-txs are always safe + } + if failed { + return nil // failed txs don't persist any logs + } + if tx.Rejected() { + return errors.New("transaction was previously rejected") + } + b, ok := miner.backend.(BackendWithInterop) + if !ok { + return fmt.Errorf("cannot mine interop txs without interop backend, got backend type %T", miner.backend) + } + if ctx == nil { // check if the miner was set up correctly to interact with an RPC + return errors.New("need RPC context to check executing messages") + } + executingMessages, err := interoptypes.ExecutingMessagesFromLogs(logs) + if err != nil { + return fmt.Errorf("cannot parse interop messages from receipt of %s: %w", tx.Hash(), err) + } + if len(executingMessages) == 0 { + return nil // avoid an RPC check if there are no executing messages to verify. + } + if err := b.CheckMessages(ctx, executingMessages, interoptypes.CrossUnsafe); err != nil { + if ctx.Err() != nil { // don't reject transactions permanently on RPC timeouts etc. + log.Debug("CheckMessages timed out", "err", ctx.Err()) + return err + } + txInteropRejectedCounter.Inc(1) + tx.SetRejected() // Mark the tx as rejected: it will not be welcome in the tx-pool anymore. + return err + } + return nil +} + func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *transactionsByPriceAndNonce, interrupt *atomic.Int32) error { gasLimit := env.header.GasLimit if env.gasPool == nil { @@ -516,6 +586,14 @@ func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *tran log.Warn("Skipping account, transaction with failed conditional", "sender", from, "hash", ltx.Hash, "err", err) txs.Pop() + case env.rpcCtx != nil && env.rpcCtx.Err() != nil && errors.Is(err, env.rpcCtx.Err()): + log.Warn("Transaction processing aborted due to RPC context error", "err", err) + txs.Pop() // RPC timeout. Tx could not be checked, and thus not included, but not rejected yet. + + case err != nil && tx.Rejected(): + log.Warn("Transaction was rejected during block-building", "hash", ltx.Hash, "err", err) + txs.Pop() + case errors.Is(err, nil): // Everything ok, collect the logs and shift in the next transaction from the same account blockDABytes = daBytesAfter diff --git a/params/interop.go b/params/interop.go new file mode 100644 index 0000000000..148ece53fe --- /dev/null +++ b/params/interop.go @@ -0,0 +1,5 @@ +package params + +import "github.com/ethereum/go-ethereum/common" + +var InteropCrossL2InboxAddress = common.HexToAddress("0x4200000000000000000000000000000000000022")