diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 0000000000..6a8e885ee0 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,48 @@ +name: Build Docker +on: + push: + branches: + - main + - testfix + - release-* + tags: + # YYYYMMDD + - "20[0-9][0-9][0-1][0-9][0-3][0-9]*" + schedule: + - cron: "0 0 * * 1" + pull_request: + workflow_dispatch: + +jobs: + build-docker: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Setup Docker BuildKit (buildx) + uses: docker/setup-buildx-action@v2 + + - name: Login to Github Container Repo + uses: docker/login-action@v2 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate op-geth docker metadata + uses: docker/metadata-action@v4 + id: op-geth + with: + images: ghcr.io/anomalyfi/op-integration/op-geth + + - name: Build and push op-geth docker + uses: docker/build-push-action@v4 + with: + context: ./ + file: Dockerfile + platforms: linux/amd64,arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.op-geth.outputs.tags }} + labels: ${{ steps.op-geth.outputs.labels }} \ No newline at end of file diff --git a/beacon/engine/gen_blockparams.go b/beacon/engine/gen_blockparams.go index c343e58906..0a7baf1a46 100644 --- a/beacon/engine/gen_blockparams.go +++ b/beacon/engine/gen_blockparams.go @@ -23,6 +23,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` NoTxPool bool `json:"noTxPool,omitempty" gencodec:"optional"` + NodeKit bool `json:"nodekit,omitempty" gencodec:"optional"` GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` } var enc PayloadAttributes @@ -38,6 +39,7 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { } } enc.NoTxPool = p.NoTxPool + enc.NodeKit = p.NodeKit enc.GasLimit = (*hexutil.Uint64)(p.GasLimit) return json.Marshal(&enc) } @@ -52,6 +54,7 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` NoTxPool *bool `json:"noTxPool,omitempty" gencodec:"optional"` + NodeKit *bool `json:"nodekit,omitempty" gencodec:"optional"` GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` } var dec PayloadAttributes @@ -85,6 +88,9 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { if dec.NoTxPool != nil { p.NoTxPool = *dec.NoTxPool } + if dec.NodeKit != nil { + p.NodeKit = *dec.NodeKit + } if dec.GasLimit != nil { p.GasLimit = (*uint64)(dec.GasLimit) } diff --git a/beacon/engine/gen_ed.go b/beacon/engine/gen_ed.go index 6893d64a16..671bc51386 100644 --- a/beacon/engine/gen_ed.go +++ b/beacon/engine/gen_ed.go @@ -32,6 +32,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { BlockHash common.Hash `json:"blockHash" gencodec:"required"` Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` + Rejected []types.RejectedTransaction `json:"rejected" gencodec:"optional"` BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` } @@ -56,6 +57,7 @@ func (e ExecutableData) MarshalJSON() ([]byte, error) { } } enc.Withdrawals = e.Withdrawals + enc.Rejected = e.Rejected enc.BlobGasUsed = (*hexutil.Uint64)(e.BlobGasUsed) enc.ExcessBlobGas = (*hexutil.Uint64)(e.ExcessBlobGas) return json.Marshal(&enc) @@ -79,6 +81,7 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { BlockHash *common.Hash `json:"blockHash" gencodec:"required"` Transactions []hexutil.Bytes `json:"transactions" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` + Rejected []types.RejectedTransaction `json:"rejected" gencodec:"optional"` BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed"` ExcessBlobGas *hexutil.Uint64 `json:"excessBlobGas"` } @@ -148,6 +151,9 @@ func (e *ExecutableData) UnmarshalJSON(input []byte) error { if dec.Withdrawals != nil { e.Withdrawals = dec.Withdrawals } + if dec.Rejected != nil { + e.Rejected = dec.Rejected + } if dec.BlobGasUsed != nil { e.BlobGasUsed = (*uint64)(dec.BlobGasUsed) } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 6f3aa006b8..61bc935f4f 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -42,6 +42,9 @@ type PayloadAttributes struct { // NoTxPool is a field for rollups: 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"` + // NodeKit indicates whether NodeKit mode is enabled. If so, invalid transactions will be + // silently rejected, instead of causing the whole block to fail. + NodeKit bool `json:"nodekit,omitempty" gencodec:"optional"` // GasLimit is a field for rollups: if set, this sets the exact gas limit the block produced with. GasLimit *uint64 `json:"gasLimit,omitempty" gencodec:"optional"` } @@ -58,23 +61,24 @@ type payloadAttributesMarshaling struct { // ExecutableData is the data necessary to execute an EL payload. type ExecutableData struct { - ParentHash common.Hash `json:"parentHash" gencodec:"required"` - FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` - StateRoot common.Hash `json:"stateRoot" gencodec:"required"` - ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` - LogsBloom []byte `json:"logsBloom" gencodec:"required"` - Random common.Hash `json:"prevRandao" gencodec:"required"` - Number uint64 `json:"blockNumber" gencodec:"required"` - GasLimit uint64 `json:"gasLimit" gencodec:"required"` - GasUsed uint64 `json:"gasUsed" gencodec:"required"` - Timestamp uint64 `json:"timestamp" gencodec:"required"` - ExtraData []byte `json:"extraData" gencodec:"required"` - BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` - BlockHash common.Hash `json:"blockHash" gencodec:"required"` - Transactions [][]byte `json:"transactions" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` - BlobGasUsed *uint64 `json:"blobGasUsed"` - ExcessBlobGas *uint64 `json:"excessBlobGas"` + ParentHash common.Hash `json:"parentHash" gencodec:"required"` + FeeRecipient common.Address `json:"feeRecipient" gencodec:"required"` + StateRoot common.Hash `json:"stateRoot" gencodec:"required"` + ReceiptsRoot common.Hash `json:"receiptsRoot" gencodec:"required"` + LogsBloom []byte `json:"logsBloom" gencodec:"required"` + Random common.Hash `json:"prevRandao" gencodec:"required"` + Number uint64 `json:"blockNumber" gencodec:"required"` + GasLimit uint64 `json:"gasLimit" gencodec:"required"` + GasUsed uint64 `json:"gasUsed" gencodec:"required"` + Timestamp uint64 `json:"timestamp" gencodec:"required"` + ExtraData []byte `json:"extraData" gencodec:"required"` + BaseFeePerGas *big.Int `json:"baseFeePerGas" gencodec:"required"` + BlockHash common.Hash `json:"blockHash" gencodec:"required"` + Transactions [][]byte `json:"transactions" gencodec:"required"` + Withdrawals []*types.Withdrawal `json:"withdrawals"` + Rejected []types.RejectedTransaction `json:"rejected" gencodec:"optional"` + BlobGasUsed *uint64 `json:"blobGasUsed"` + ExcessBlobGas *uint64 `json:"excessBlobGas"` } // JSON type overrides for executableData. @@ -240,7 +244,7 @@ func ExecutableDataToBlock(params ExecutableData, versionedHashes []common.Hash, BlobGasUsed: params.BlobGasUsed, ParentBeaconRoot: beaconRoot, } - block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */).WithWithdrawals(params.Withdrawals) + block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */).WithWithdrawals(params.Withdrawals).WithRejected(params.Rejected) if block.Hash() != params.BlockHash { return nil, fmt.Errorf("blockhash mismatch, want %x, got %x", params.BlockHash, block.Hash()) } @@ -266,6 +270,7 @@ func BlockToExecutableData(block *types.Block, fees *big.Int, sidecars []*types. Random: block.MixDigest(), ExtraData: block.Extra(), Withdrawals: block.Withdrawals(), + Rejected: block.Rejected(), BlobGasUsed: block.BlobGasUsed(), ExcessBlobGas: block.ExcessBlobGas(), } diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index a7d0e58161..618ba49417 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -364,9 +364,9 @@ func (beacon *Beacon) Finalize(chain consensus.ChainHeaderReader, header *types. // FinalizeAndAssemble implements consensus.Engine, setting the final state and // assembling the block. -func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) { +func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal, rejected []types.RejectedTransaction) (*types.Block, error) { if !beacon.IsPoSHeader(header) { - return beacon.ethone.FinalizeAndAssemble(chain, header, state, txs, uncles, receipts, nil) + return beacon.ethone.FinalizeAndAssemble(chain, header, state, txs, uncles, receipts, nil, rejected) } shanghai := chain.Config().IsShanghai(header.Number, header.Time) if shanghai { @@ -386,7 +386,7 @@ func (beacon *Beacon) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea header.Root = state.IntermediateRoot(true) // Assemble and return the final block. - return types.NewBlockWithWithdrawals(header, txs, uncles, receipts, withdrawals, trie.NewStackTrie(nil)), nil + return types.NewBlockWithWithdrawals(header, txs, uncles, receipts, withdrawals, trie.NewStackTrie(nil)).WithRejected(rejected), nil } // Seal generates a new sealing request for the given input block and pushes diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index f708050abd..bf21d47de6 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -573,7 +573,7 @@ func (c *Clique) Finalize(chain consensus.ChainHeaderReader, header *types.Heade // FinalizeAndAssemble implements consensus.Engine, ensuring no uncles are set, // nor block rewards given, and returns the final block. -func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) { +func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal, rejected []types.RejectedTransaction) (*types.Block, error) { if len(withdrawals) > 0 { return nil, errors.New("clique does not support withdrawals") } @@ -584,7 +584,7 @@ func (c *Clique) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header * header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) // Assemble and return the final block for sealing. - return types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)), nil + return types.NewBlock(header, txs, nil, receipts, trie.NewStackTrie(nil)).WithRejected(rejected), nil } // Authorize injects a private key into the consensus engine to mint new blocks diff --git a/consensus/consensus.go b/consensus/consensus.go index 3a2c2d2229..6c0a8adc7e 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -97,7 +97,7 @@ type Engine interface { // Note: The block header and state database might be updated to reflect any // consensus rules that happen at finalization (e.g. block rewards). FinalizeAndAssemble(chain ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, - uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) + uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal, rejected []types.RejectedTransaction) (*types.Block, error) // Seal generates a new sealing request for the given input block and pushes // the result into the given channel. diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index 8eb9863da1..2a6bfe30c7 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -494,7 +494,7 @@ func (ethash *Ethash) Finalize(chain consensus.ChainHeaderReader, header *types. // FinalizeAndAssemble implements consensus.Engine, accumulating the block and // uncle rewards, setting the final state and assembling the block. -func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal) (*types.Block, error) { +func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, txs []*types.Transaction, uncles []*types.Header, receipts []*types.Receipt, withdrawals []*types.Withdrawal, rejected []types.RejectedTransaction) (*types.Block, error) { if len(withdrawals) > 0 { return nil, errors.New("ethash does not support withdrawals") } @@ -505,7 +505,7 @@ func (ethash *Ethash) FinalizeAndAssemble(chain consensus.ChainHeaderReader, hea header.Root = state.IntermediateRoot(chain.Config().IsEIP158(header.Number)) // Header seems complete, assemble into a block and return - return types.NewBlock(header, txs, uncles, receipts, trie.NewStackTrie(nil)), nil + return types.NewBlock(header, txs, uncles, receipts, trie.NewStackTrie(nil)).WithRejected(rejected), nil } // SealHash returns the hash of a block prior to it being sealed. diff --git a/core/chain_makers.go b/core/chain_makers.go index 90b5b1fcee..6355ee7463 100644 --- a/core/chain_makers.go +++ b/core/chain_makers.go @@ -344,7 +344,7 @@ func GenerateChain(config *params.ChainConfig, parent *types.Block, engine conse gen(i, b) } - block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, b.txs, b.uncles, b.receipts, b.withdrawals) + block, err := b.engine.FinalizeAndAssemble(cm, b.header, statedb, b.txs, b.uncles, b.receipts, b.withdrawals, nil) if err != nil { panic(err) } diff --git a/core/types/block.go b/core/types/block.go index 1a357baa3a..a63de32e02 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -196,6 +196,30 @@ type Block struct { transactions Transactions withdrawals Withdrawals + // `rejected` is an OP/NodeKit extension. It collects transactions which were included by the + // OP sequencer in the batch that generated this block, but could not be included in the block + // because the failed basic consensus checks -- invalid signature, nonce too low, etc. + // + // This field is _not_ normative: it is not included in the block hash and is not considered + // part of the L2 chain data. Most block explorers will not show it. This means that the L2 + // chain still has the exact same data structures and format as an L1 EVM chain. The field is + // mainly included here for plumbing: it means that when a batch is executed to produce a block, + // information is not lost when transactions from the batch are rejected. Thus, when the block + // is converted _back_ to a batch by the batcher, the resulting batch which is sent to the L1 + // will include all the transactions from the original batch, including the invalid ones. + // + // Validating OP-nodes which read batches from L1 can then check that the sequencer included all + // the transactions it was required to include, if the sequencer was obligated by the NodeKit + // Sequencer to include certain transactions. Those nodes will then _derive_ the resulting block + // by sending the batch to their own engine, which will once again filter out the invalid + // transactions, rather than trusting the sequencer about which transactions were invalid. + // + // Effectively, we have removed the exclusion of invalid transactions as a responsibility of the + // sequencer and instead pushed this responsibility into the derivation pipeline/fraud proof + // mechanism. As a result, we need this extra field to keep track of the extra information -- + // invalid transactions -- that is a normative part of a batch but not a block. + rejected []RejectedTransaction + // caches hash atomic.Value size atomic.Value @@ -206,12 +230,21 @@ type Block struct { ReceivedFrom interface{} } +type RejectedTransaction struct { + // The raw data of the transaction. This allows us to include even completely malformed data + // blobs that were forced into the sequence by end users as rejected transactions. + Data []byte + // The position in the block at which this tranaction would have appeared had it been valid. + Pos uint64 +} + // "external" block encoding. used for eth protocol, etc. type extblock struct { Header *Header Txs []*Transaction Uncles []*Header - Withdrawals []*Withdrawal `rlp:"optional"` + Withdrawals []*Withdrawal `rlp:"optional"` + Rejected []RejectedTransaction `rlp:"optional"` } // NewBlock creates a new block. The input data is copied, changes to header and to the @@ -314,7 +347,7 @@ func (b *Block) DecodeRLP(s *rlp.Stream) error { if err := s.Decode(&eb); err != nil { return err } - b.header, b.uncles, b.transactions, b.withdrawals = eb.Header, eb.Uncles, eb.Txs, eb.Withdrawals + b.header, b.uncles, b.transactions, b.withdrawals, b.rejected = eb.Header, eb.Uncles, eb.Txs, eb.Withdrawals, eb.Rejected b.size.Store(rlp.ListSize(size)) return nil } @@ -326,6 +359,7 @@ func (b *Block) EncodeRLP(w io.Writer) error { Txs: b.transactions, Uncles: b.uncles, Withdrawals: b.withdrawals, + Rejected: b.rejected, }) } @@ -385,6 +419,10 @@ func (b *Block) BaseFee() *big.Int { func (b *Block) BeaconRoot() *common.Hash { return b.header.ParentBeaconRoot } +func (b *Block) Rejected() []RejectedTransaction { + return b.rejected +} + func (b *Block) ExcessBlobGas() *uint64 { var excessBlobGas *uint64 if b.header.ExcessBlobGas != nil { @@ -482,6 +520,12 @@ func (b *Block) WithWithdrawals(withdrawals []*Withdrawal) *Block { return block } +// WithRejected sets the rejected transactions in a block, does not return a new block. +func (b *Block) WithRejected(rejected []RejectedTransaction) *Block { + b.rejected = rejected + return b +} + // Hash returns the keccak256 hash of b's header. // The hash is computed on the first call and cached thereafter. func (b *Block) Hash() common.Hash { diff --git a/core/types/receipt.go b/core/types/receipt.go index 6c0d863ef6..799ee4c86f 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -570,12 +570,12 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu } } if config.Optimism != nil && len(txs) >= 2 { // need at least an info tx and a non-info tx - if data := txs[0].Data(); len(data) >= 4+32*8 { // function selector + 8 arguments to setL1BlockValues - l1Basefee := new(big.Int).SetBytes(data[4+32*2 : 4+32*3]) // arg index 2 - overhead := new(big.Int).SetBytes(data[4+32*6 : 4+32*7]) // arg index 6 - scalar := new(big.Int).SetBytes(data[4+32*7 : 4+32*8]) // arg index 7 - fscalar := new(big.Float).SetInt(scalar) // legacy: format fee scalar as big Float - fdivisor := new(big.Float).SetUint64(1_000_000) // 10**6, i.e. 6 decimals + if data := txs[0].Data(); len(data) >= 4+32+32*11 { // function selector + dynamic args offset + 11 arguments to setL1BlockValues + l1Basefee := new(big.Int).SetBytes(data[36+32*2 : 36+32*3]) // arg index 2 + overhead := new(big.Int).SetBytes(data[36+32*6 : 36+32*7]) // arg index 6 + scalar := new(big.Int).SetBytes(data[36+32*7 : 36+32*8]) // arg index 7 + fscalar := new(big.Float).SetInt(scalar) // legacy: format fee scalar as big Float + fdivisor := new(big.Float).SetUint64(1_000_000) // 10**6, i.e. 6 decimals feeScalar := new(big.Float).Quo(fscalar, fdivisor) for i := 0; i < len(rs); i++ { if !txs[i].IsDepositTx() { diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 69599d8c6c..a979680e5f 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -383,6 +383,7 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl Withdrawals: payloadAttributes.Withdrawals, BeaconRoot: payloadAttributes.BeaconRoot, NoTxPool: payloadAttributes.NoTxPool, + NodeKit: payloadAttributes.NodeKit, Transactions: transactions, GasLimit: payloadAttributes.GasLimit, } diff --git a/miner/payload_building.go b/miner/payload_building.go index 1510ee5a82..01156858fa 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -43,6 +43,7 @@ type BuildPayloadArgs struct { BeaconRoot *common.Hash // The provided beaconRoot (Cancun) NoTxPool bool // Optimism addition: option to disable tx pool contents from being included + NodeKit bool // Optimism addition: option to enable NodeKit mode Transactions []*types.Transaction // Optimism addition: txs forced into the block via engine API GasLimit *uint64 // Optimism addition: override gas limit of the block to build } @@ -206,6 +207,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { withdrawals: args.Withdrawals, beaconRoot: args.BeaconRoot, noTxs: true, + nodekit: args.NodeKit, txs: args.Transactions, gasLimit: args.GasLimit, } @@ -245,6 +247,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { withdrawals: args.Withdrawals, beaconRoot: args.BeaconRoot, noTxs: false, + nodekit: false, txs: args.Transactions, gasLimit: args.GasLimit, } diff --git a/miner/worker.go b/miner/worker.go index 4a73c1b1b6..49d9a1c8bd 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -936,6 +936,7 @@ 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 + nodekit bool // Flag indicating whether NodeKit mode is enabled txs types.Transactions // Deposit transactions to include at the start of the block gasLimit *uint64 // Optional gas limit override @@ -1074,14 +1075,32 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { misc.EnsureCreate2Deployer(w.chainConfig, work.header.Time, work.state) + rejected := make([]types.RejectedTransaction, 0) for _, tx := range genParams.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)} + if tx.Type() == types.DepositTxType || !genParams.nodekit { + // We must include deposit transactions, and prior to enabling NodeKit mode we must + // include _all_ forced transactions. + 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)} + } + // Other forced transactions that are invalid just get added to a list, which the + // batcher can use to send them to L1 so that validating nodes can check they were in + // fact rejected using their own engine. + log.Warn("failed to force-include tx", "hash", tx.Hash(), "type", tx.Type(), "sender", from, "nonce", tx.Nonce(), "err", err) + bytes, err := tx.MarshalBinary() + if err != nil { + return &newPayloadResult{err: fmt.Errorf("failed to serialize rejected tx %v, err: %w", tx, err)} + } + rejected = append(rejected, types.RejectedTransaction{ + Data: bytes, + Pos: uint64(len(work.receipts)), + }) + } else { + work.tcount++ } - work.tcount++ } // forced transactions done, fill rest of block with transactions @@ -1097,7 +1116,7 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) } } - block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, nil, work.receipts, genParams.withdrawals) + block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, nil, work.receipts, genParams.withdrawals, rejected) if err != nil { return &newPayloadResult{err: err} } @@ -1186,7 +1205,7 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti // https://github.com/ethereum/go-ethereum/issues/24299 env := env.copy() // Withdrawals are set to nil here, because this is only called in PoW. - block, err := w.engine.FinalizeAndAssemble(w.chain, env.header, env.state, env.txs, nil, env.receipts, nil) + block, err := w.engine.FinalizeAndAssemble(w.chain, env.header, env.state, env.txs, nil, env.receipts, nil, nil) if err != nil { return err } diff --git a/miner/worker_test.go b/miner/worker_test.go index d0a5ced8f9..bd9c71d63b 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -461,6 +461,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co withdrawals: nil, beaconRoot: nil, noTxs: false, + nodekit: false, forceTime: true, }) if c.expectErr { @@ -486,6 +487,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co withdrawals: nil, beaconRoot: nil, noTxs: false, + nodekit: false, forceTime: true, }) if c.expectErr {