diff --git a/.circleci/config.yml b/.circleci/config.yml index ad9793ad98..725bbbdbc0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,11 @@ orbs: gcp-cli: circleci/gcp-cli@3.0.1 slack: circleci/slack@4.10.1 +parameters: + go_version: + type: string + default: 1.22.7 # update CI Go version here + commands: gcp-oidc-authenticate: description: "Authenticate with GCP using a CircleCI OIDC token." @@ -134,7 +139,7 @@ jobs: build-geth: docker: - - image: cimg/go:1.21 + - image: cimg/go:<> resource_class: xlarge steps: - checkout @@ -143,7 +148,7 @@ jobs: unit-test: resource_class: xlarge docker: - - image: cimg/go:1.21 + - image: cimg/go:<> steps: - checkout - run: @@ -151,7 +156,7 @@ jobs: lint-geth: resource_class: medium docker: - - image: cimg/go:1.21 + - image: cimg/go:<> steps: - checkout - run: @@ -159,14 +164,14 @@ jobs: tidy-geth: resource_class: small docker: - - image: cimg/go:1.21 + - image: cimg/go:<> steps: - checkout - run: command: go mod tidy && git diff --exit-code check-releases: docker: - - image: cimg/go:1.21 + - image: cimg/go:<> steps: - checkout - run: diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 2020df3991..30c7df3b84 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -159,6 +159,8 @@ var ( utils.RollupSequencerTxConditionalCostRateLimitFlag, utils.RollupHistoricalRPCFlag, utils.RollupHistoricalRPCTimeoutFlag, + utils.RollupInteropRPCFlag, + utils.RollupInteropMempoolFilteringFlag, utils.RollupDisableTxPoolGossipFlag, utils.RollupComputePendingBlock, utils.RollupHaltOnIncompatibleProtocolVersionFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 026bcf7e62..8eda534783 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -939,6 +939,18 @@ var ( Category: flags.RollupCategory, } + RollupInteropRPCFlag = &cli.StringFlag{ + Name: "rollup.interoprpc", + Usage: "RPC endpoint for interop message verification (experimental).", + Category: flags.RollupCategory, + } + + RollupInteropMempoolFilteringFlag = &cli.BoolFlag{ + Name: "rollup.interopmempoolfiltering", + Usage: "If using interop, transactions are checked for interop validity before being added to the mempool (experimental).", + Category: flags.RollupCategory, + } + RollupDisableTxPoolGossipFlag = &cli.BoolFlag{ Name: "rollup.disabletxpoolgossip", Usage: "Disable transaction pool gossip.", @@ -1941,6 +1953,12 @@ 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) + } + if ctx.IsSet(RollupInteropMempoolFilteringFlag.Name) { + cfg.InteropMempoolFiltering = ctx.Bool(RollupInteropMempoolFilteringFlag.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/error.go b/core/error.go index e6ad999bdd..b8b00121eb 100644 --- a/core/error.go +++ b/core/error.go @@ -84,6 +84,10 @@ var ( // current network configuration. ErrTxTypeNotSupported = types.ErrTxTypeNotSupported + // ErrTxFilteredOut indicates an ingress filter has rejected the transaction from + // being included in the pool. + ErrTxFilteredOut = errors.New("transaction filtered out") + // ErrTipAboveFeeCap is a sanity error to ensure no one is able to specify a // transaction with a tip higher than the total fee cap. ErrTipAboveFeeCap = errors.New("max priority fee per gas higher than max fee per gas") 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 f1207c57af..ee3ce99875 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -156,6 +156,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. @@ -440,11 +442,11 @@ func (st *StateTransition) buyGas() error { if st.msg.GasFeeCap != nil { balanceCheck.SetUint64(st.msg.GasLimit) balanceCheck = balanceCheck.Mul(balanceCheck, st.msg.GasFeeCap) + if l1Cost != nil { + balanceCheck.Add(balanceCheck, l1Cost) + } } balanceCheck.Add(balanceCheck, st.msg.Value) - if l1Cost != nil { - balanceCheck.Add(balanceCheck, l1Cost) - } if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { if blobGas := st.blobGasUsed(); blobGas > 0 { @@ -668,6 +670,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 } @@ -811,49 +820,49 @@ func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { if rules.IsEIP4762 && fee.Sign() != 0 { st.evm.AccessEvents.AddAccount(st.evm.Context.Coinbase, true) } - } - // Check that we are post bedrock to enable op-geth to be able to create pseudo pre-bedrock blocks (these are pre-bedrock, but don't follow l2 geth rules) - // Note optimismConfig will not be nil if rules.IsOptimismBedrock is true - if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && rules.IsOptimismBedrock && !st.msg.IsDepositTx { - gasCost := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) + // Check that we are post bedrock to enable op-geth to be able to create pseudo pre-bedrock blocks (these are pre-bedrock, but don't follow l2 geth rules) + // Note optimismConfig will not be nil if rules.IsOptimismBedrock is true + if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && rules.IsOptimismBedrock && !st.msg.IsDepositTx { + gasCost := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) - if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { - gasCost.Add(gasCost, new(big.Int).Mul(new(big.Int).SetUint64(st.blobGasUsed()), st.evm.Context.BlobBaseFee)) - } - - amtU256, overflow := uint256.FromBig(gasCost) - if overflow { - return nil, fmt.Errorf("optimism gas cost overflows U256: %d", gasCost) - } - if shouldCheckGasFormula { - st.baseFee = amtU256.Clone() - } + if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { + gasCost.Add(gasCost, new(big.Int).Mul(new(big.Int).SetUint64(st.blobGasUsed()), st.evm.Context.BlobBaseFee)) + } - amtU256 = st.collectableNativeBalance(amtU256) - st.state.AddBalance(params.OptimismBaseFeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) - if l1Cost := st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time); l1Cost != nil { - amtU256, overflow = uint256.FromBig(l1Cost) + amtU256, overflow := uint256.FromBig(gasCost) if overflow { - return nil, fmt.Errorf("optimism l1 cost overflows U256: %d", l1Cost) + return nil, fmt.Errorf("optimism gas cost overflows U256: %d", gasCost) } - if shouldCheckGasFormula { - st.l1Fee = amtU256.Clone() + st.baseFee = amtU256.Clone() } amtU256 = st.collectableNativeBalance(amtU256) - st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) - } + st.state.AddBalance(params.OptimismBaseFeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) + if l1Cost := st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time); l1Cost != nil { + amtU256, overflow = uint256.FromBig(l1Cost) + if overflow { + return nil, fmt.Errorf("optimism l1 cost overflows U256: %d", l1Cost) + } + if shouldCheckGasFormula { + st.l1Fee = amtU256.Clone() + } - if shouldCheckGasFormula { - if st.l1Fee == nil { - st.l1Fee = new(uint256.Int) + amtU256 = st.collectableNativeBalance(amtU256) + st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) } - if err := st.checkGasFormula(); err != nil { - return nil, err + + if shouldCheckGasFormula { + if st.l1Fee == nil { + st.l1Fee = new(uint256.Int) + } + if err := st.checkGasFormula(); err != nil { + return nil, err + } } } + } return &ExecutionResult{ diff --git a/core/txpool/ingress_filters.go b/core/txpool/ingress_filters.go new file mode 100644 index 0000000000..317585f7a8 --- /dev/null +++ b/core/txpool/ingress_filters.go @@ -0,0 +1,57 @@ +package txpool + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/interoptypes" + "github.com/ethereum/go-ethereum/log" +) + +// IngressFilter is an interface that allows filtering of transactions before they are added to the transaction pool. +// Implementations of this interface can be used to filter transactions based on various criteria. +// FilterTx will return true if the transaction should be allowed, and false if it should be rejected. +type IngressFilter interface { + FilterTx(ctx context.Context, tx *types.Transaction) bool +} + +type interopFilter struct { + logsFn func(tx *types.Transaction) ([]*types.Log, error) + checkFn func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error +} + +func NewInteropFilter( + logsFn func(tx *types.Transaction) ([]*types.Log, error), + checkFn func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error) IngressFilter { + return &interopFilter{ + logsFn: logsFn, + checkFn: checkFn, + } +} + +// FilterTx implements IngressFilter.FilterTx +// it gets logs checks for message safety based on the function provided +func (f *interopFilter) FilterTx(ctx context.Context, tx *types.Transaction) bool { + logs, err := f.logsFn(tx) + if err != nil { + log.Debug("Failed to retrieve logs of tx", "txHash", tx.Hash(), "err", err) + return false // default to deny if logs cannot be retrieved + } + if len(logs) == 0 { + return true // default to allow if there are no logs + } + ems, err := interoptypes.ExecutingMessagesFromLogs(logs) + if err != nil { + log.Debug("Failed to parse executing messages of tx", "txHash", tx.Hash(), "err", err) + return false // default to deny if logs cannot be parsed + } + if len(ems) == 0 { + return true // default to allow if there are no executing messages + } + + ctx, cancel := context.WithTimeout(ctx, time.Second*2) + defer cancel() + // check with the supervisor if the transaction should be allowed given the executing messages + return f.checkFn(ctx, ems, interoptypes.Unsafe) == nil +} diff --git a/core/txpool/ingress_filters_test.go b/core/txpool/ingress_filters_test.go new file mode 100644 index 0000000000..b2fcfd9018 --- /dev/null +++ b/core/txpool/ingress_filters_test.go @@ -0,0 +1,188 @@ +package txpool + +import ( + "context" + "errors" + "math/big" + "net" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types/interoptypes" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +func TestInteropFilter(t *testing.T) { + // some placeholder transaction to test with + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: big.NewInt(1), + Nonce: 1, + To: &common.Address{}, + Value: big.NewInt(1), + Data: []byte{}, + }) + t.Run("Tx has no logs", func(t *testing.T) { + logFn := func(tx *types.Transaction) ([]*types.Log, error) { + return []*types.Log{}, nil + } + checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error { + // make this return error, but it won't be called because logs are empty + return errors.New("error") + } + // when there are no logs to process, the transaction should be allowed + filter := NewInteropFilter(logFn, checkFn) + require.True(t, filter.FilterTx(context.Background(), tx)) + }) + t.Run("Tx errored when getting logs", func(t *testing.T) { + logFn := func(tx *types.Transaction) ([]*types.Log, error) { + return []*types.Log{}, errors.New("error") + } + checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error { + // make this return error, but it won't be called because logs retrieval errored + return errors.New("error") + } + // when log retrieval errors, the transaction should be denied + filter := NewInteropFilter(logFn, checkFn) + require.False(t, filter.FilterTx(context.Background(), tx)) + }) + t.Run("Tx has no executing messages", func(t *testing.T) { + logFn := func(tx *types.Transaction) ([]*types.Log, error) { + l1 := &types.Log{ + Topics: []common.Hash{common.BytesToHash([]byte("topic1"))}, + } + return []*types.Log{l1}, nil + } + checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error { + // make this return error, but it won't be called because logs retrieval doesn't have executing messages + return errors.New("error") + } + // when no executing messages are included, the transaction should be allowed + filter := NewInteropFilter(logFn, checkFn) + require.True(t, filter.FilterTx(context.Background(), tx)) + }) + t.Run("Tx has valid executing message", func(t *testing.T) { + // build a basic executing message + // the executing message must pass basic decode validation, + // but the validity check is done by the checkFn + l1 := &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + }, + Data: []byte{}, + } + // using all 0s for data allows all takeZeros to pass + for i := 0; i < 32*5; i++ { + l1.Data = append(l1.Data, 0) + } + logFn := func(tx *types.Transaction) ([]*types.Log, error) { + return []*types.Log{l1}, nil + } + var spyEMs []interoptypes.Message + checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error { + spyEMs = ems + return nil + } + // when there is one executing message, the transaction should be allowed + // if the checkFn returns nil + filter := NewInteropFilter(logFn, checkFn) + require.True(t, filter.FilterTx(context.Background(), tx)) + // confirm that one executing message was passed to the checkFn + require.Equal(t, 1, len(spyEMs)) + }) + t.Run("Tx has invalid executing message", func(t *testing.T) { + // build a basic executing message + // the executing message must pass basic decode validation, + // but the validity check is done by the checkFn + l1 := &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + }, + Data: []byte{}, + } + // using all 0s for data allows all takeZeros to pass + for i := 0; i < 32*5; i++ { + l1.Data = append(l1.Data, 0) + } + logFn := func(tx *types.Transaction) ([]*types.Log, error) { + return []*types.Log{l1}, nil + } + var spyEMs []interoptypes.Message + checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error { + spyEMs = ems + return errors.New("error") + } + // when there is one executing message, and the checkFn returns an error, + // (ie the supervisor rejects the transaction) the transaction should be denied + filter := NewInteropFilter(logFn, checkFn) + require.False(t, filter.FilterTx(context.Background(), tx)) + // confirm that one executing message was passed to the checkFn + require.Equal(t, 1, len(spyEMs)) + }) +} + +func TestInteropFilterRPCFailures(t *testing.T) { + tests := []struct { + name string + networkErr bool + timeout bool + invalidResp bool + }{ + { + name: "Network Error", + networkErr: true, + }, + { + name: "Timeout", + timeout: true, + }, + { + name: "Invalid Response", + invalidResp: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create mock log function that always returns our test log + logFn := func(tx *types.Transaction) ([]*types.Log, error) { + log := &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(interoptypes.ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + }, + Data: make([]byte, 32*5), + } + return []*types.Log{log}, nil + } + + // Create mock check function that simulates RPC failures + checkFn := func(ctx context.Context, ems []interoptypes.Message, safety interoptypes.SafetyLevel) error { + if tt.networkErr { + return &net.OpError{Op: "dial", Err: errors.New("connection refused")} + } + + if tt.timeout { + return context.DeadlineExceeded + } + + if tt.invalidResp { + return errors.New("invalid response format") + } + + return nil + } + + // Create and test filter + filter := NewInteropFilter(logFn, checkFn) + result := filter.FilterTx(context.Background(), &types.Transaction{}) + require.Equal(t, false, result, "FilterTx result mismatch") + }) + } +} diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 9849fab929..4063a77267 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -568,9 +568,21 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) map[common.Address] } } } + if filter.MaxDATxSize != nil && !pool.locals.contains(addr) { + for i, tx := range txs { + estimate := tx.RollupCostData().EstimatedDASize() + if estimate.Cmp(filter.MaxDATxSize) > 0 { + log.Debug("filtering tx that exceeds max da tx size", + "hash", tx.Hash(), "txda", estimate, "dalimit", filter.MaxDATxSize) + txs = txs[:i] + break + } + } + } if len(txs) > 0 { lazies := make([]*txpool.LazyTransaction, len(txs)) for i := 0; i < len(txs); i++ { + daBytes := txs[i].RollupCostData().EstimatedDASize() lazies[i] = &txpool.LazyTransaction{ Pool: pool, Hash: txs[i].Hash(), @@ -580,6 +592,7 @@ func (pool *LegacyPool) Pending(filter txpool.PendingFilter) map[common.Address] GasTipCap: uint256.MustFromBig(txs[i].GasTipCap()), Gas: txs[i].Gas(), BlobGas: txs[i].BlobGas(), + DABytes: daBytes, } } pending[addr] = lazies diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go index 9881ed1b8f..4e2a0133f3 100644 --- a/core/txpool/subpool.go +++ b/core/txpool/subpool.go @@ -41,6 +41,8 @@ type LazyTransaction struct { Gas uint64 // Amount of gas required by the transaction BlobGas uint64 // Amount of blob gas required by the transaction + + DABytes *big.Int // Amount of data availability bytes this transaction may require if this is a rollup } // Resolve retrieves the full transaction belonging to a lazy handle if it is still @@ -83,6 +85,10 @@ type PendingFilter struct { OnlyPlainTxs bool // Return only plain EVM transactions (peer-join announces, block space filling) OnlyBlobTxs bool // Return only blob transactions (block blob-space filling) + + // OP stack addition: Maximum l1 data size allowed for an included transaction (for throttling + // when batcher is backlogged). Ignored if nil. + MaxDATxSize *big.Int } // SubPool represents a specialized transaction pool that lives on its own (e.g. diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index ff31ffc200..db4ee13ee4 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -17,6 +17,7 @@ package txpool import ( + "context" "errors" "fmt" "math/big" @@ -77,22 +78,32 @@ type TxPool struct { term chan struct{} // Termination channel to detect a closed pool sync chan chan error // Testing / simulator channel to block until internal reset is done + + ingressFilters []IngressFilter // List of filters to apply to incoming transactions + + filterCtx context.Context // Filters may use external resources + filterCancel context.CancelFunc // Filter calls are cancelled on shutdown } // New creates a new transaction pool to gather, sort and filter inbound // transactions from the network. -func New(gasTip uint64, chain BlockChain, subpools []SubPool) (*TxPool, error) { +func New(gasTip uint64, chain BlockChain, subpools []SubPool, poolFilters []IngressFilter) (*TxPool, error) { // Retrieve the current head so that all subpools and this main coordinator // pool will have the same starting state, even if the chain moves forward // during initialization. head := chain.CurrentBlock() + filterCtx, filterCancel := context.WithCancel(context.Background()) + pool := &TxPool{ - subpools: subpools, - reservations: make(map[common.Address]SubPool), - quit: make(chan chan error), - term: make(chan struct{}), - sync: make(chan chan error), + subpools: subpools, + reservations: make(map[common.Address]SubPool), + quit: make(chan chan error), + term: make(chan struct{}), + sync: make(chan chan error), + ingressFilters: poolFilters, + filterCtx: filterCtx, + filterCancel: filterCancel, } for i, subpool := range subpools { if err := subpool.Init(gasTip, head, pool.reserver(i, subpool)); err != nil { @@ -156,6 +167,8 @@ func (p *TxPool) reserver(id int, subpool SubPool) AddressReserver { func (p *TxPool) Close() error { var errs []error + p.filterCancel() // Cancel filter work, these in-flight txs will be not be allowed through before shutdown + // Terminate the reset loop and wait for it to finish errc := make(chan error) p.quit <- errc @@ -319,11 +332,23 @@ func (p *TxPool) Add(txs []*types.Transaction, local bool, sync bool) []error { // so we can piece back the returned errors into the original order. txsets := make([][]*types.Transaction, len(p.subpools)) splits := make([]int, len(txs)) + filtered_out := make([]bool, len(txs)) for i, tx := range txs { // Mark this transaction belonging to no-subpool splits[i] = -1 + // Filter the transaction through the ingress filters + for _, f := range p.ingressFilters { + if !f.FilterTx(p.filterCtx, tx) { + filtered_out[i] = true + } + } + // if the transaction is filtered out, don't add it to any subpool + if filtered_out[i] { + continue + } + // Try to find a subpool that accepts the transaction for j, subpool := range p.subpools { if subpool.Filter(tx) { @@ -341,6 +366,11 @@ func (p *TxPool) Add(txs []*types.Transaction, local bool, sync bool) []error { } errs := make([]error, len(txs)) for i, split := range splits { + // If the transaction was filtered out, mark it as such + if filtered_out[i] { + errs[i] = core.ErrTxFilteredOut + continue + } // If the transaction was rejected by all subpools, mark it unsupported if split == -1 { errs[i] = core.ErrTxTypeNotSupported diff --git a/core/types/gen_transaction_conditional_json.go b/core/types/gen_transaction_conditional_json.go index b168b60206..b202735316 100644 --- a/core/types/gen_transaction_conditional_json.go +++ b/core/types/gen_transaction_conditional_json.go @@ -5,9 +5,8 @@ package types import ( "encoding/json" "math/big" - "bytes" - "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" ) var _ = (*transactionConditionalMarshalling)(nil) @@ -15,36 +14,32 @@ var _ = (*transactionConditionalMarshalling)(nil) // MarshalJSON marshals as JSON. func (t TransactionConditional) MarshalJSON() ([]byte, error) { type TransactionConditional struct { - KnownAccounts KnownAccounts `json:"knownAccounts"` - BlockNumberMin *hexutil.Big `json:"blockNumberMin,omitempty"` - BlockNumberMax *hexutil.Big `json:"blockNumberMax,omitempty"` - TimestampMin *hexutil.Uint64 `json:"timestampMin,omitempty"` - TimestampMax *hexutil.Uint64 `json:"timestampMax,omitempty"` + KnownAccounts KnownAccounts `json:"knownAccounts"` + BlockNumberMin *math.HexOrDecimal256 `json:"blockNumberMin,omitempty"` + BlockNumberMax *math.HexOrDecimal256 `json:"blockNumberMax,omitempty"` + TimestampMin *math.HexOrDecimal64 `json:"timestampMin,omitempty"` + TimestampMax *math.HexOrDecimal64 `json:"timestampMax,omitempty"` } var enc TransactionConditional enc.KnownAccounts = t.KnownAccounts - enc.BlockNumberMin = (*hexutil.Big)(t.BlockNumberMin) - enc.BlockNumberMax = (*hexutil.Big)(t.BlockNumberMax) - enc.TimestampMin = (*hexutil.Uint64)(t.TimestampMin) - enc.TimestampMax = (*hexutil.Uint64)(t.TimestampMax) + enc.BlockNumberMin = (*math.HexOrDecimal256)(t.BlockNumberMin) + enc.BlockNumberMax = (*math.HexOrDecimal256)(t.BlockNumberMax) + enc.TimestampMin = (*math.HexOrDecimal64)(t.TimestampMin) + enc.TimestampMax = (*math.HexOrDecimal64)(t.TimestampMax) return json.Marshal(&enc) } // UnmarshalJSON unmarshals from JSON. func (t *TransactionConditional) UnmarshalJSON(input []byte) error { type TransactionConditional struct { - KnownAccounts *KnownAccounts `json:"knownAccounts"` - BlockNumberMin *hexutil.Big `json:"blockNumberMin,omitempty"` - BlockNumberMax *hexutil.Big `json:"blockNumberMax,omitempty"` - TimestampMin *hexutil.Uint64 `json:"timestampMin,omitempty"` - TimestampMax *hexutil.Uint64 `json:"timestampMax,omitempty"` + KnownAccounts *KnownAccounts `json:"knownAccounts"` + BlockNumberMin *math.HexOrDecimal256 `json:"blockNumberMin,omitempty"` + BlockNumberMax *math.HexOrDecimal256 `json:"blockNumberMax,omitempty"` + TimestampMin *math.HexOrDecimal64 `json:"timestampMin,omitempty"` + TimestampMax *math.HexOrDecimal64 `json:"timestampMax,omitempty"` } var dec TransactionConditional - // --- Not Generated. Disallow unknown fields - decoder := json.NewDecoder(bytes.NewReader(input)) - decoder.DisallowUnknownFields() // Force errors - // --- - if err := decoder.Decode(&dec); err != nil { + if err := json.Unmarshal(input, &dec); err != nil { return err } if dec.KnownAccounts != nil { diff --git a/core/types/interoptypes/interop.go b/core/types/interoptypes/interop.go new file mode 100644 index 0000000000..742ecefdcd --- /dev/null +++ b/core/types/interoptypes/interop.go @@ -0,0 +1,169 @@ +package interoptypes + +import ( + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "math" + + "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 - 4); err != nil { + return fmt.Errorf("invalid log index padding: %w", err) + } + m.Identifier.LogIndex = binary.BigEndian.Uint32(take(4)) + 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 uint32 + 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) + if dec.LogIndex > math.MaxUint32 { + return fmt.Errorf("log index too large: %d", dec.LogIndex) + } + id.LogIndex = uint32(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) +} + +// Valid returns if the safety level is a well-formatted safety level. +func (lvl SafetyLevel) wellFormatted() bool { + switch lvl { + case Finalized, Safe, LocalSafe, CrossUnsafe, Unsafe, Invalid: + 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.wellFormatted() { + return fmt.Errorf("unrecognized safety level: %q", text) + } + *lvl = x + return nil +} + +const ( + Finalized SafetyLevel = "finalized" + Safe SafetyLevel = "safe" + LocalSafe SafetyLevel = "local-safe" + CrossUnsafe SafetyLevel = "cross-unsafe" + Unsafe SafetyLevel = "unsafe" + Invalid SafetyLevel = "invalid" +) diff --git a/core/types/interoptypes/interop_test.go b/core/types/interoptypes/interop_test.go new file mode 100644 index 0000000000..5bb303fecf --- /dev/null +++ b/core/types/interoptypes/interop_test.go @@ -0,0 +1,178 @@ +package interoptypes + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +func FuzzMessage_DecodeEvent(f *testing.F) { + f.Fuzz(func(t *testing.T, validEvTopic bool, numTopics uint8, data []byte) { + if len(data) < 32 { + return + } + if len(data) > 100_000 { + return + } + if validEvTopic { // valid even signature topic implies a topic to be there + numTopics += 1 + } + if numTopics > 4 { // There can be no more than 4 topics per log event + return + } + if int(numTopics)*32 > len(data) { + return + } + var topics []common.Hash + if validEvTopic { + topics = append(topics, ExecutingMessageEventTopic) + } + for i := 0; i < int(numTopics); i++ { + var topic common.Hash + copy(topic[:], data[:]) + data = data[32:] + } + require.NotPanics(t, func() { + var m Message + _ = m.DecodeEvent(topics, data) + }) + }) +} + +func TestSafetyLevel(t *testing.T) { + require.True(t, Invalid.wellFormatted()) + require.True(t, Unsafe.wellFormatted()) + require.True(t, CrossUnsafe.wellFormatted()) + require.True(t, LocalSafe.wellFormatted()) + require.True(t, Safe.wellFormatted()) + require.True(t, Finalized.wellFormatted()) + require.False(t, SafetyLevel("hello").wellFormatted()) + require.False(t, SafetyLevel("").wellFormatted()) +} + +func TestInteropMessageFormatEdgeCases(t *testing.T) { + tests := []struct { + name string + log *types.Log + expectedError string + }{ + { + name: "Empty Topics", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{}, + Data: make([]byte, 32*5), + }, + expectedError: "unexpected number of event topics: 0", + }, + { + name: "Wrong Event Topic", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash([]byte("wrong topic")), + common.BytesToHash([]byte("payloadHash")), + }, + Data: make([]byte, 32*5), + }, + expectedError: "unexpected event topic", + }, + { + name: "Missing PayloadHash Topic", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(ExecutingMessageEventTopic[:]), + }, + Data: make([]byte, 32*5), + }, + expectedError: "unexpected number of event topics: 1", + }, + { + name: "Too Many Topics", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + common.BytesToHash([]byte("extra")), + }, + Data: make([]byte, 32*5), + }, + expectedError: "unexpected number of event topics: 3", + }, + { + name: "Data Too Short", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + }, + Data: make([]byte, 32*4), // One word too short + }, + expectedError: "unexpected identifier data length: 128", + }, + { + name: "Data Too Long", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + }, + Data: make([]byte, 32*6), // One word too long + }, + expectedError: "unexpected identifier data length: 192", + }, + { + name: "Invalid Address Padding", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + }, + Data: func() []byte { + data := make([]byte, 32*5) + data[0] = 1 // Add non-zero byte in address padding + return data + }(), + }, + expectedError: "invalid address padding", + }, + { + name: "Invalid Block Number Padding", + log: &types.Log{ + Address: params.InteropCrossL2InboxAddress, + Topics: []common.Hash{ + common.BytesToHash(ExecutingMessageEventTopic[:]), + common.BytesToHash([]byte("payloadHash")), + }, + Data: func() []byte { + data := make([]byte, 32*5) + data[32+23] = 1 // Add non-zero byte in block number padding + return data + }(), + }, + expectedError: "invalid block number padding", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var msg Message + err := msg.DecodeEvent(tt.log.Topics, tt.log.Data) + if tt.expectedError != "" { + require.Error(t, err) + require.ErrorContains(t, err, tt.expectedError) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/core/types/rollup_cost.go b/core/types/rollup_cost.go index 96c5f30bcf..0a8d659437 100644 --- a/core/types/rollup_cost.go +++ b/core/types/rollup_cost.go @@ -370,14 +370,7 @@ func NewL1CostFuncFjord(l1BaseFee, l1BlobBaseFee, baseFeeScalar, blobFeeScalar * calldataCostPerByte := new(big.Int).Mul(scaledL1BaseFee, sixteen) blobCostPerByte := new(big.Int).Mul(blobFeeScalar, l1BlobBaseFee) l1FeeScaled := new(big.Int).Add(calldataCostPerByte, blobCostPerByte) - - fastLzSize := new(big.Int).SetUint64(costData.FastLzSize) - estimatedSize := new(big.Int).Add(L1CostIntercept, new(big.Int).Mul(L1CostFastlzCoef, fastLzSize)) - - if estimatedSize.Cmp(MinTransactionSizeScaled) < 0 { - estimatedSize.Set(MinTransactionSizeScaled) - } - + estimatedSize := costData.estimatedDASizeScaled() l1CostScaled := new(big.Int).Mul(estimatedSize, l1FeeScaled) l1Cost := new(big.Int).Div(l1CostScaled, fjordDivisor) l1Cost = new(big.Int).Add(l1Cost, new(big.Int).Mul(big.NewInt(int64(costData.Blobs)), big.NewInt(params.BlobDAFee))) @@ -389,6 +382,25 @@ func NewL1CostFuncFjord(l1BaseFee, l1BlobBaseFee, baseFeeScalar, blobFeeScalar * } } +// estimatedDASizeScaled estimates the number of bytes the transaction will occupy in the DA batch using the Fjord +// linear regression model, and returns this value scaled up by 1e6. +func (cd RollupCostData) estimatedDASizeScaled() *big.Int { + fastLzSize := new(big.Int).SetUint64(cd.FastLzSize) + estimatedSize := new(big.Int).Add(L1CostIntercept, new(big.Int).Mul(L1CostFastlzCoef, fastLzSize)) + + if estimatedSize.Cmp(MinTransactionSizeScaled) < 0 { + estimatedSize.Set(MinTransactionSizeScaled) + } + return estimatedSize +} + +// EstimatedDASize estimates the number of bytes the transaction will occupy in its DA batch using the Fjord linear +// regression model. +func (cd RollupCostData) EstimatedDASize() *big.Int { + b := cd.estimatedDASizeScaled() + return b.Div(b, big.NewInt(1e6)) +} + func extractEcotoneFeeParams(l1FeeParams []byte) (l1BaseFeeScalar, l1BlobBaseFeeScalar *big.Int) { offset := scalarSectionStart l1BaseFeeScalar = new(big.Int).SetBytes(l1FeeParams[offset : offset+4]) diff --git a/core/types/transaction_conditional.go b/core/types/transaction_conditional.go index 90a77c7d81..479b6f9fc5 100644 --- a/core/types/transaction_conditional.go +++ b/core/types/transaction_conditional.go @@ -6,7 +6,7 @@ import ( "math/big" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" ) // KnownAccounts represents a set of KnownAccounts @@ -42,7 +42,7 @@ func (ka *KnownAccount) UnmarshalJSON(data []byte) error { } // MarshalJSON will serialize the KnownAccount into JSON bytes. -func (ka *KnownAccount) MarshalJSON() ([]byte, error) { +func (ka KnownAccount) MarshalJSON() ([]byte, error) { if ka.StorageRoot != nil { return json.Marshal(ka.StorageRoot) } @@ -86,10 +86,10 @@ type TransactionConditional struct { // field type overrides for gencodec type transactionConditionalMarshalling struct { - BlockNumberMax *hexutil.Big - BlockNumberMin *hexutil.Big - TimestampMin *hexutil.Uint64 - TimestampMax *hexutil.Uint64 + BlockNumberMax *math.HexOrDecimal256 + BlockNumberMin *math.HexOrDecimal256 + TimestampMin *math.HexOrDecimal64 + TimestampMax *math.HexOrDecimal64 } // Validate will perform sanity checks on the preconditions. This does not check the aggregate cost of the preconditions. diff --git a/core/types/transaction_conditional_test.go b/core/types/transaction_conditional_test.go index f7e3bb7ef1..6abfe946d0 100644 --- a/core/types/transaction_conditional_test.go +++ b/core/types/transaction_conditional_test.go @@ -204,13 +204,21 @@ func TestTransactionConditionalSerDeser(t *testing.T) { }, { name: "BlockNumberMax", - input: `{"blockNumberMin":"0x1", "blockNumberMax":"0x2"}`, + input: `{"blockNumberMax":"0x2"}`, mustFail: false, expected: TransactionConditional{ - BlockNumberMin: big.NewInt(1), BlockNumberMax: big.NewInt(2), }, }, + { + name: "BlockNumber (decimal)", + input: `{"blockNumberMin": 0, "blockNumberMax": 1}`, + mustFail: false, + expected: TransactionConditional{ + BlockNumberMin: big.NewInt(0), + BlockNumberMax: big.NewInt(1), + }, + }, { name: "TimestampMin", input: `{"timestampMin":"0xffff"}`, @@ -228,10 +236,13 @@ func TestTransactionConditionalSerDeser(t *testing.T) { }, }, { - name: "UnknownField", - input: `{"foobarbaz": 1234}`, - mustFail: true, - expected: TransactionConditional{KnownAccounts: nil}, + name: "Timestamp (decimal)", + input: `{"timestampMin": 0, "timestampMax": 1}`, + mustFail: false, + expected: TransactionConditional{ + TimestampMin: uint64Ptr(0), + TimestampMax: uint64Ptr(1), + }, }, } diff --git a/eth/api_miner.go b/eth/api_miner.go index 8c96f4c54a..31f175ffe9 100644 --- a/eth/api_miner.go +++ b/eth/api_miner.go @@ -56,3 +56,10 @@ func (api *MinerAPI) SetGasLimit(gasLimit hexutil.Uint64) bool { api.e.Miner().SetGasCeil(uint64(gasLimit)) return true } + +// SetMaxDASize sets the maximum data availability size of any tx allowed in a block, and the total max l1 data size of +// the block. 0 means no maximum. +func (api *MinerAPI) SetMaxDASize(maxTxSize hexutil.Big, maxBlockSize hexutil.Big) bool { + api.e.Miner().SetMaxDASize(maxTxSize.ToInt(), maxBlockSize.ToInt()) + return true +} diff --git a/eth/backend.go b/eth/backend.go index ba1102eef5..29d8cc600c 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -42,6 +42,7 @@ import ( "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 +80,8 @@ type Ethereum struct { seqRPCService *rpc.Client historicalRPCService *rpc.Client + interopRPC *interop.InteropClient + // DB interfaces chainDb ethdb.Database // Block chain database @@ -270,7 +273,13 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { blobPool := blobpool.New(config.BlobPool, eth.blockchain) txPools = append(txPools, blobPool) } - eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, txPools) + // if interop is enabled, establish an Interop Filter connected to this Ethereum instance's + // simulated logs and message safety check functions + poolFilters := []txpool.IngressFilter{} + if config.InteropMessageRPC != "" && config.InteropMempoolFiltering { + poolFilters = append(poolFilters, txpool.NewInteropFilter(eth.SimLogs, eth.CheckMessages)) + } + eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, txPools, poolFilters) if err != nil { return nil, err } @@ -320,6 +329,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 +496,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() diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index f3415b2284..29eca7bb28 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -181,6 +181,9 @@ type Config struct { RollupDisableTxPoolGossip bool RollupDisableTxPoolAdmission bool RollupHaltOnIncompatibleProtocolVersion string + + InteropMessageRPC string `toml:",omitempty"` + InteropMempoolFiltering bool `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..72f0e1dc73 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -67,6 +67,8 @@ func (c Config) MarshalTOML() (interface{}, error) { RollupDisableTxPoolGossip bool RollupDisableTxPoolAdmission bool RollupHaltOnIncompatibleProtocolVersion string + InteropMessageRPC string `toml:",omitempty"` + InteropMempoolFiltering bool `toml:",omitempty"` } var enc Config enc.Genesis = c.Genesis @@ -119,6 +121,8 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.RollupDisableTxPoolGossip = c.RollupDisableTxPoolGossip enc.RollupDisableTxPoolAdmission = c.RollupDisableTxPoolAdmission enc.RollupHaltOnIncompatibleProtocolVersion = c.RollupHaltOnIncompatibleProtocolVersion + enc.InteropMessageRPC = c.InteropMessageRPC + enc.InteropMempoolFiltering = c.InteropMempoolFiltering return &enc, nil } @@ -175,6 +179,8 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { RollupDisableTxPoolGossip *bool RollupDisableTxPoolAdmission *bool RollupHaltOnIncompatibleProtocolVersion *string + InteropMessageRPC *string `toml:",omitempty"` + InteropMempoolFiltering *bool `toml:",omitempty"` } var dec Config if err := unmarshal(&dec); err != nil { @@ -330,5 +336,11 @@ 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 + } + if dec.InteropMempoolFiltering != nil { + c.InteropMempoolFiltering = *dec.InteropMempoolFiltering + } return nil } diff --git a/eth/interop.go b/eth/interop.go new file mode 100644 index 0000000000..f044f20120 --- /dev/null +++ b/eth/interop.go @@ -0,0 +1,57 @@ +package eth + +import ( + "context" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/core" + "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/internal/ethapi" +) + +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) +} + +// SimLogs simulates the logs that would be generated by a transaction if it were executed on the current state. +// This is used by the interop filter to determine if a transaction should be allowed. +// if errors are encountered, no logs are returned. +func (s *Ethereum) SimLogs(tx *types.Transaction) ([]*types.Log, error) { + chainConfig := s.APIBackend.ChainConfig() + if !chainConfig.IsOptimism() { + return nil, errors.New("expected OP-Stack chain config, SimLogs is an OP-Stack feature") + } + header := s.BlockChain().CurrentBlock() + if chainConfig.InteropTime == nil { + return nil, errors.New("expected Interop fork to be configured, SimLogs is unavailable pre-interop") + } + state, err := s.BlockChain().StateAt(header.Root) + if err != nil { + return nil, fmt.Errorf("state %s (block %d) is unavailable for log simulation: %w", header.Root, header.Number.Uint64(), err) + } + var vmConf vm.Config + signer := types.MakeSigner(chainConfig, header.Number, header.Time) + message, err := core.TransactionToMessage(tx, signer, header.BaseFee) + if err != nil { + return nil, fmt.Errorf("cannot convert tx to message for log simulation: %w", err) + } + chainCtx := ethapi.NewChainContext(context.Background(), s.APIBackend) + blockCtx := core.NewEVMBlockContext(header, chainCtx, &header.Coinbase, chainConfig, state) + txCtx := core.NewEVMTxContext(message) + vmenv := vm.NewEVM(blockCtx, txCtx, state, chainConfig, vmConf) + state.SetTxContext(tx.Hash(), 0) + result, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(header.GasLimit)) + if err != nil { + return nil, fmt.Errorf("failed to execute tx: %w", err) + } + if result.Failed() { // failed txs do not have log events + return nil, nil + } + return state.GetLogs(tx.Hash(), header.Number.Uint64(), header.Hash()), 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/eth/protocols/eth/handler_test.go b/eth/protocols/eth/handler_test.go index fc82b42947..0acd50ed5b 100644 --- a/eth/protocols/eth/handler_test.go +++ b/eth/protocols/eth/handler_test.go @@ -117,7 +117,7 @@ func newTestBackendWithGenerator(blocks int, shanghai bool, generator func(int, txconfig.Journal = "" // Don't litter the disk with test journals pool := legacypool.New(txconfig, chain) - txpool, _ := txpool.New(txconfig.PriceLimit, chain, []txpool.SubPool{pool}) + txpool, _ := txpool.New(txconfig.PriceLimit, chain, []txpool.SubPool{pool}, nil) return &testBackend{ db: db, diff --git a/fork.yaml b/fork.yaml index ce8b56c7d9..cf31c24316 100644 --- a/fork.yaml +++ b/fork.yaml @@ -78,13 +78,15 @@ def: - title: "Core Error definitions" globs: - "core/error.go" - - title: "Gaslimit" + - title: "Gaslimit and EIP-1559 Params" description: | The gaslimit is free to be set by the Engine API caller, instead of enforcing adjustments of the - gaslimit in increments of 1/1024 of the previous gaslimit. - The gaslimit is changed (and limited) through the `SystemConfig` contract. + gaslimit in increments of 1/1024 of the previous gaslimit. The elasticity-multiplier and + base-fee-max-change-denominator EIP-1559 parameters can also be set by the Engine API caller through the + ExtraData field. The gaslimit and EIP-1559 parameters are changed (and limited) through the + `SystemConfig` contract. globs: - - "consensus/misc/eip1559/eip1559.go" + - "consensus/misc/eip1559/*" - title: "Consensus tweaks" description: | The Engine API is activated at the Merge transition, with a Total Terminal Difficulty (TTD). @@ -103,9 +105,11 @@ def: - "consensus/beacon/oplegacy.go" - title: "Engine API modifications" description: | - The Engine API is extended to insert transactions into the block and optionally exclude the tx-pool, - to reproduce the exact block of the sequencer from just the inputs, as derived from L1 by the rollup-node. - See [L2 execution engine specs](https://github.com/ethereum-optimism/optimism/blob/develop/specs/exec-engine.md). + The Engine API is extended to insert transactions into the block and optionally exclude the tx-pool, to + reproduce the exact block of the sequencer from just the inputs, as derived from L1 by the rollup-node. See + [L2 execution engine specs](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/exec-engine.md). + It is also extended to support dynamic EIP-1559 parameters. See + [Holocene execution engine specs](https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md). globs: - "beacon/engine/types.go" - "beacon/engine/gen_blockparams.go" @@ -113,7 +117,9 @@ def: - title: "Block-building modifications" description: | 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 and gaslimit parameters of the Engine API. + 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" @@ -225,6 +231,14 @@ def: globs: - "core/state/workers.go" - "trie/hasher.go" + - title: "Interop message checking" + description: | + The interop upgrade introduces cross-chain message. + Transactions are checked for cross-chain message safety before and during inclusion into a block. + This also includes tx-pool ingress filtering. + globs: + - "eth/interop.go" + - "core/txpool/ingress_filters.go" - title: "User API enhancements" description: "Encode the Deposit Tx properties, the L1 costs, and daisy-chain RPC-calls for pre-Bedrock historical data" sub: diff --git a/go.mod b/go.mod index e9cd235ca2..3c152bd5bd 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/ethereum/go-ethereum -go 1.22 +go 1.22.0 + +toolchain go1.22.7 require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 @@ -21,7 +23,7 @@ require ( github.com/deckarep/golang-set/v2 v2.6.0 github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 github.com/dop251/goja v0.0.0-20230806174421-c933cf95e127 - github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240828144951-4e6edcb7d36c + github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241119111730-bee358f6d6e6 github.com/ethereum/c-kzg-4844 v1.0.0 github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 github.com/fatih/color v1.16.0 @@ -65,11 +67,11 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.25.7 go.uber.org/automaxprocs v1.5.2 - golang.org/x/crypto v0.26.0 + golang.org/x/crypto v0.28.0 golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa golang.org/x/sync v0.8.0 - golang.org/x/sys v0.24.0 - golang.org/x/text v0.17.0 + golang.org/x/sys v0.26.0 + golang.org/x/text v0.19.0 golang.org/x/time v0.5.0 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d google.golang.org/protobuf v1.34.2 @@ -137,13 +139,13 @@ require ( github.com/prometheus/common v0.32.1 // indirect github.com/prometheus/procfs v0.7.3 // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - golang.org/x/mod v0.20.0 // indirect + golang.org/x/mod v0.21.0 // indirect golang.org/x/net v0.25.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect diff --git a/go.sum b/go.sum index 379276a751..a63fc834cd 100644 --- a/go.sum +++ b/go.sum @@ -171,8 +171,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240828144951-4e6edcb7d36c h1:wI6W6CimquWKoD6eZ0GhULXmiZynynzLBCPnsFKt+Y0= -github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20240828144951-4e6edcb7d36c/go.mod h1:XaVXL9jg8BcyOeugECgIUGa9Y3DjYJj71RHmb5qon6M= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241119111730-bee358f6d6e6 h1:+AIYWDX7FeWRLnBVqPiwireTacLLGGww1slGyv+YN0o= +github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241119111730-bee358f6d6e6/go.mod h1:9feO8jcL5OZ1tvRjEfNAHz4Aggvd6373l+ZxmZZAyZs= github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= @@ -480,8 +480,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -542,8 +542,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -577,8 +577,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -698,8 +698,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -712,8 +712,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index 4a53e2c829..a6faa056dc 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -671,6 +671,12 @@ web3._extend({ params: 1, inputFormatter: [web3._extend.utils.fromDecimal] }), + new web3._extend.Method({ + name: 'setMaxDASize', + call: 'miner_setMaxDASize', + params: 2, + inputFormatter: [web3._extend.utils.fromDecimal, web3._extend.utils.fromDecimal] + }), ], properties: [] }); diff --git a/miner/miner.go b/miner/miner.go index 0e5d877bec..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 @@ -58,7 +63,9 @@ type Config struct { RollupComputePendingBlock bool // Compute the pending block from tx-pool, instead of copying the latest-block RollupTransactionConditionalRateLimit int // Total number of conditional cost units allowed in a second - EffectiveGasCeil uint64 // if non-zero, a gas ceiling to apply independent of the header's gaslimit value + EffectiveGasCeil uint64 // if non-zero, a gas ceiling to apply independent of the header's gaslimit value + MaxDATxSize *big.Int // if non-nil, don't include any txs with data availability size larger than this in any built block + MaxDABlockSize *big.Int // if non-nil, then don't build a block requiring more than this amount of total data availability } // DefaultConfig contains default settings for miner. @@ -86,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, @@ -98,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, } } @@ -152,6 +166,22 @@ func (miner *Miner) SetGasTip(tip *big.Int) error { return nil } +// SetMaxDASize sets the maximum data availability size currently allowed for inclusion. 0 means no maximum. +func (miner *Miner) SetMaxDASize(maxTxSize, maxBlockSize *big.Int) { + miner.confMu.Lock() + if maxTxSize == nil || maxTxSize.BitLen() == 0 { + miner.config.MaxDATxSize = nil + } else { + miner.config.MaxDATxSize = new(big.Int).Set(maxTxSize) + } + if maxBlockSize == nil || maxBlockSize.BitLen() == 0 { + miner.config.MaxDABlockSize = nil + } else { + miner.config.MaxDABlockSize = new(big.Int).Set(maxBlockSize) + } + miner.confMu.Unlock() +} + // BuildPayload builds the payload according to the provided parameters. func (miner *Miner) BuildPayload(args *BuildPayloadArgs, witness bool) (*Payload, error) { return miner.buildPayload(args, witness) @@ -190,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/miner_test.go b/miner/miner_test.go index 46a14e97f3..d365ee18ac 100644 --- a/miner/miner_test.go +++ b/miner/miner_test.go @@ -164,7 +164,7 @@ func createMiner(t *testing.T) *Miner { blockchain := &testBlockChain{bc.Genesis().Root(), chainConfig, statedb, 10000000, new(event.Feed)} pool := legacypool.New(testTxPoolConfig, blockchain) - txpool, _ := txpool.New(testTxPoolConfig.PriceLimit, blockchain, []txpool.SubPool{pool}) + txpool, _ := txpool.New(testTxPoolConfig.PriceLimit, blockchain, []txpool.SubPool{pool}, nil) // Create Miner backend := NewMockBackend(bc, txpool) 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/payload_building_test.go b/miner/payload_building_test.go index bf76a2040e..ee5e2e6944 100644 --- a/miner/payload_building_test.go +++ b/miner/payload_building_test.go @@ -18,6 +18,7 @@ package miner import ( "bytes" + "crypto/rand" "math/big" "reflect" "testing" @@ -65,10 +66,14 @@ var ( testConfig = Config{ PendingFeeRecipient: testBankAddress, Recommit: time.Second, - GasCeil: params.GenesisGasLimit, + GasCeil: 50_000_000, } ) +const ( + numDAFilterTxs = 256 +) + func init() { testTxPoolConfig = legacypool.DefaultConfig testTxPoolConfig.Journal = "" @@ -135,7 +140,7 @@ func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine t.Fatalf("core.NewBlockChain failed: %v", err) } pool := legacypool.New(testTxPoolConfig, chain) - txpool, _ := txpool.New(testTxPoolConfig.PriceLimit, chain, []txpool.SubPool{pool}) + txpool, _ := txpool.New(testTxPoolConfig.PriceLimit, chain, []txpool.SubPool{pool}, nil) return &testWorkerBackend{ db: db, @@ -150,7 +155,7 @@ func (b *testWorkerBackend) TxPool() *txpool.TxPool { return b.txPool } func newTestWorker(t *testing.T, chainConfig *params.ChainConfig, engine consensus.Engine, db ethdb.Database, blocks int) (*Miner, *testWorkerBackend) { backend := newTestWorkerBackend(t, chainConfig, engine, db, blocks) - backend.txPool.Add(pendingTxs, true, true) + backend.txPool.Add(pendingTxs, false, true) w := New(backend, testConfig, engine) return w, backend } @@ -173,6 +178,25 @@ func TestBuildPayload(t *testing.T) { t.Run("with-zero-params", func(t *testing.T) { testBuildPayload(t, true, false, zeroParams) }) } +func TestDAFilters(t *testing.T) { + // Each test case inserts one pending small (DA cost 100) transaction followed by + // numDAFilterTxs transactions that have random calldata (min DA size >> 100) + totalTxs := numDAFilterTxs + 1 + + // Very low max should filter all transactions. + t.Run("with-tx-filter-max-filters-all", func(t *testing.T) { testDAFilters(t, big.NewInt(1), nil, 0) }) + t.Run("with-block-filter-max-filters-all", func(t *testing.T) { testDAFilters(t, nil, big.NewInt(1), 0) }) + // Very high max should filter nothing. + t.Run("with-tx-filter-max-too-high", func(t *testing.T) { testDAFilters(t, big.NewInt(1000000), nil, totalTxs) }) + t.Run("with-block-filter-max-too-high", func(t *testing.T) { testDAFilters(t, nil, big.NewInt(1000000), totalTxs) }) + // The first transaction has size 100, all other DA test txs are bigger due to random Data, so should get filtered. + t.Run("with-tx-filter-all-but-first", func(t *testing.T) { testDAFilters(t, big.NewInt(100), nil, 1) }) + t.Run("with-block-filter-all-but-first", func(t *testing.T) { testDAFilters(t, nil, big.NewInt(100), 1) }) + // Zero/nil values for these parameters means we should never filter + t.Run("with-zero-tx-filters", func(t *testing.T) { testDAFilters(t, big.NewInt(0), big.NewInt(0), totalTxs) }) + t.Run("with-nil-tx-filters", func(t *testing.T) { testDAFilters(t, nil, nil, totalTxs) }) +} + func holoceneConfig() *params.ChainConfig { config := *params.TestChainConfig config.LondonBlock = big.NewInt(0) @@ -204,6 +228,7 @@ func newPayloadArgs(parentHash common.Hash, params1559 []byte) *BuildPayloadArgs func testBuildPayload(t *testing.T, noTxPool, interrupt bool, params1559 []byte) { t.Parallel() db := rawdb.NewMemoryDatabase() + config := params.TestChainConfig if len(params1559) != 0 { config = holoceneConfig() @@ -211,11 +236,12 @@ func testBuildPayload(t *testing.T, noTxPool, interrupt bool, params1559 []byte) w, b := newTestWorker(t, config, ethash.NewFaker(), db, 0) const numInterruptTxs = 256 + if interrupt { // when doing interrupt testing, create a large pool so interruption will // definitely be visible. txs := genTxs(1, numInterruptTxs) - b.txPool.Add(txs, true, false) + b.txPool.Add(txs, false, false) } args := newPayloadArgs(b.chain.CurrentBlock().Hash(), params1559) @@ -294,6 +320,30 @@ func testBuildPayload(t *testing.T, noTxPool, interrupt bool, params1559 []byte) } } +func testDAFilters(t *testing.T, maxDATxSize, maxDABlockSize *big.Int, expectedTxCount int) { + t.Parallel() + db := rawdb.NewMemoryDatabase() + config := holoceneConfig() + w, b := newTestWorker(t, config, ethash.NewFaker(), db, 0) + w.SetMaxDASize(maxDATxSize, maxDABlockSize) + txs := genTxs(1, numDAFilterTxs) + b.txPool.Add(txs, false, false) + + params1559 := []byte{0, 1, 2, 3, 4, 5, 6, 7} + args := newPayloadArgs(b.chain.CurrentBlock().Hash(), params1559) + args.NoTxPool = false + + payload, err := w.buildPayload(args, false) + if err != nil { + t.Fatalf("Failed to build payload %v", err) + } + payload.WaitFull() + result := payload.ResolveFull().ExecutionPayload + if len(result.Transactions) != expectedTxCount { + t.Fatalf("Unexpected transaction set: got %d, expected %d", len(result.Transactions), expectedTxCount) + } +} + func testBuildPayloadWrongConfig(t *testing.T, params1559 []byte) { t.Parallel() db := rawdb.NewMemoryDatabase() @@ -331,14 +381,23 @@ func genTxs(startNonce, count uint64) types.Transactions { txs := make(types.Transactions, 0, count) signer := types.LatestSigner(params.TestChainConfig) for nonce := startNonce; nonce < startNonce+count; nonce++ { - txs = append(txs, types.MustSignNewTx(testBankKey, signer, &types.AccessListTx{ + // generate incompressible data to put in the tx for DA filter testing. each of these + // txs will be bigger than the 100 minimum. + randomBytes := make([]byte, 100) + _, err := rand.Read(randomBytes) + if err != nil { + panic(err) + } + tx := types.MustSignNewTx(testBankKey, signer, &types.AccessListTx{ ChainID: params.TestChainConfig.ChainID, Nonce: nonce, To: &testUserAddress, Value: big.NewInt(1000), - Gas: params.TxGas, + Gas: params.TxGas + uint64(len(randomBytes))*16, GasPrice: big.NewInt(params.InitialBaseFee), - })) + Data: randomBytes, + }) + txs = append(txs, tx) } return txs } diff --git a/miner/worker.go b/miner/worker.go index 5b9b42f69d..b405e7b45f 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,7 +78,9 @@ type environment struct { blobs int witness *stateless.Witness - noTxs bool // this is used as a flag whether it's *effectively* sequencing or deriving + // this is used as a flag whether it's *effectively* sequencing or deriving (es comment) + 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. } // isEffectivelySequencing is true when PayloadAttributes.NoTxPool is false, @@ -119,6 +124,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. @@ -151,7 +158,6 @@ func (miner *Miner) generateWork(params *generateParams, witness bool) *newPaylo 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++ } if !params.noTxs { // use shared interrupt if present @@ -295,11 +301,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{}) @@ -315,7 +322,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 { @@ -348,6 +355,7 @@ func (miner *Miner) makeEnv(parent *types.Header, header *types.Header, coinbase coinbase: coinbase, header: header, witness: state.Witness(), + rpcCtx: rpcCtx, }, nil } @@ -406,13 +414,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) @@ -420,11 +452,48 @@ 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 { env.gasPool = new(core.GasPool).AddGas(gasLimit) } + blockDABytes := new(big.Int) for { // Check interruption signal and abort building if it's fired. if interrupt != nil { @@ -478,6 +547,16 @@ func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *tran txs.Pop() continue } + daBytesAfter := new(big.Int) + if ltx.DABytes != nil && miner.config.MaxDABlockSize != nil { + daBytesAfter.Add(blockDABytes, ltx.DABytes) + if daBytesAfter.Cmp(miner.config.MaxDABlockSize) > 0 { + log.Debug("adding tx would exceed block DA size limit", + "hash", ltx.Hash, "txda", ltx.DABytes, "blockda", blockDABytes, "dalimit", miner.config.MaxDABlockSize) + txs.Pop() + continue + } + } // Transaction seems to fit, pull it up from the pool tx := ltx.Resolve() if tx == nil { @@ -515,8 +594,17 @@ 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 txs.Shift() default: @@ -539,7 +627,8 @@ func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment) // Retrieve the pending transactions pre-filtered by the 1559/4844 dynamic fees filter := txpool.PendingFilter{ - MinTip: uint256.MustFromBig(tip), + MinTip: uint256.MustFromBig(tip), + MaxDATxSize: miner.config.MaxDATxSize, } if env.header.BaseFee != nil { filter.BaseFee = uint256.MustFromBig(env.header.BaseFee) 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") diff --git a/params/superchain.go b/params/superchain.go index ff90525b29..bab6bec17e 100644 --- a/params/superchain.go +++ b/params/superchain.go @@ -11,7 +11,7 @@ import ( "github.com/ethereum/go-ethereum/common" ) -var OPStackSupport = ProtocolVersionV0{Build: [8]byte{}, Major: 8, Minor: 0, Patch: 0, PreRelease: 0}.Encode() +var OPStackSupport = ProtocolVersionV0{Build: [8]byte{}, Major: 9, Minor: 0, Patch: 0, PreRelease: 1}.Encode() func init() { for id, ch := range superchain.OPChains {