op-geth

diff: ignored:
+7224
-496
+9318
-198

This is an overview of the changes in op-geth, a fork of go-ethereum, part of the OP-stack.

The OP-stack architecture is modular, following the Consensus/Execution split of post-Merge Ethereum L1:

  • op-node implements most rollup-specific functionality as Consensus-Layer, similar to a L1 beacon-node.
  • op-geth implements the Execution-Layer, with minimal changes for a secure Ethereum-equivalent application environment.

Related op-stack specifications:

The Bedrock upgrade introduces a Deposit transaction-type (0x7E) to enable both users and the rollup system itself to change the L2 state based on L1 events and system rules as specified.

diff --git go-ethereum/core/types/deposit_tx.go op-geth/core/types/deposit_tx.go new file mode 100644 index 0000000000000000000000000000000000000000..4131ee7af037275dcacd795baab4e0d55a1acb07 --- /dev/null +++ op-geth/core/types/deposit_tx.go @@ -0,0 +1,103 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package types + +import ( + "bytes" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" +) + +const DepositTxType = 0x7E + +type DepositTx struct { + // SourceHash uniquely identifies the source of the deposit + SourceHash common.Hash + // From is exposed through the types.Signer, not through TxData + From common.Address + // nil means contract creation + To *common.Address `rlp:"nil"` + // Mint is minted on L2, locked on L1, nil if no minting. + Mint *big.Int `rlp:"nil"` + // Value is transferred from L2 balance, executed after Mint (if any) + Value *big.Int + // gas limit + Gas uint64 + // Field indicating if this transaction is exempt from the L2 gas limit. + IsSystemTransaction bool + // Normal Tx data + Data []byte +} + +// copy creates a deep copy of the transaction data and initializes all fields. +func (tx *DepositTx) copy() TxData { + cpy := &DepositTx{ + SourceHash: tx.SourceHash, + From: tx.From, + To: copyAddressPtr(tx.To), + Mint: nil, + Value: new(big.Int), + Gas: tx.Gas, + IsSystemTransaction: tx.IsSystemTransaction, + Data: common.CopyBytes(tx.Data), + } + if tx.Mint != nil { + cpy.Mint = new(big.Int).Set(tx.Mint) + } + if tx.Value != nil { + cpy.Value.Set(tx.Value) + } + return cpy +} + +// accessors for innerTx. +func (tx *DepositTx) txType() byte { return DepositTxType } +func (tx *DepositTx) chainID() *big.Int { return common.Big0 } +func (tx *DepositTx) accessList() AccessList { return nil } +func (tx *DepositTx) data() []byte { return tx.Data } +func (tx *DepositTx) gas() uint64 { return tx.Gas } +func (tx *DepositTx) gasFeeCap() *big.Int { return new(big.Int) } +func (tx *DepositTx) gasTipCap() *big.Int { return new(big.Int) } +func (tx *DepositTx) gasPrice() *big.Int { return new(big.Int) } +func (tx *DepositTx) value() *big.Int { return tx.Value } +func (tx *DepositTx) nonce() uint64 { return 0 } +func (tx *DepositTx) to() *common.Address { return tx.To } +func (tx *DepositTx) isSystemTx() bool { return tx.IsSystemTransaction } + +func (tx *DepositTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { + return dst.Set(new(big.Int)) +} + +func (tx *DepositTx) effectiveNonce() *uint64 { return nil } + +func (tx *DepositTx) rawSignatureValues() (v, r, s *big.Int) { + return common.Big0, common.Big0, common.Big0 +} + +func (tx *DepositTx) setSignatureValues(chainID, v, r, s *big.Int) { + // this is a noop for deposit transactions +} + +func (tx *DepositTx) encode(b *bytes.Buffer) error { + return rlp.Encode(b, tx) +} + +func (tx *DepositTx) decode(input []byte) error { + return rlp.DecodeBytes(input, tx) +}
diff --git go-ethereum/core/types/transaction_marshalling.go op-geth/core/types/transaction_marshalling.go index 4d5b2bcdd4ce375d6c9896cbd321ff7949b071fb..a07d65414c3846e49412a3083371e93b843d2553 100644 --- go-ethereum/core/types/transaction_marshalling.go +++ op-geth/core/types/transaction_marshalling.go @@ -19,11 +19,13 @@ import ( "encoding/json" "errors" + "io" "math/big"   "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/rlp" "github.com/holiman/uint256" )   @@ -47,6 +49,12 @@ V *hexutil.Big `json:"v"` R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` YParity *hexutil.Uint64 `json:"yParity,omitempty"` + + // Deposit transaction fields + SourceHash *common.Hash `json:"sourceHash,omitempty"` + From *common.Address `json:"from,omitempty"` + Mint *hexutil.Big `json:"mint,omitempty"` + IsSystemTx *bool `json:"isSystemTx,omitempty"`   // Blob transaction sidecar encoding: Blobs []kzg4844.Blob `json:"blobs,omitempty"` @@ -153,6 +161,33 @@ enc.Blobs = itx.Sidecar.Blobs enc.Commitments = itx.Sidecar.Commitments enc.Proofs = itx.Sidecar.Proofs } + + case *DepositTx: + enc.Gas = (*hexutil.Uint64)(&itx.Gas) + enc.Value = (*hexutil.Big)(itx.Value) + enc.Input = (*hexutil.Bytes)(&itx.Data) + enc.To = tx.To() + enc.SourceHash = &itx.SourceHash + enc.From = &itx.From + if itx.Mint != nil { + enc.Mint = (*hexutil.Big)(itx.Mint) + } + enc.IsSystemTx = &itx.IsSystemTransaction + // other fields will show up as null. + + case *depositTxWithNonce: + enc.Gas = (*hexutil.Uint64)(&itx.Gas) + enc.Value = (*hexutil.Big)(itx.Value) + enc.Input = (*hexutil.Bytes)(&itx.Data) + enc.To = tx.To() + enc.SourceHash = &itx.SourceHash + enc.From = &itx.From + if itx.Mint != nil { + enc.Mint = (*hexutil.Big)(itx.Mint) + } + enc.IsSystemTx = &itx.IsSystemTransaction + enc.Nonce = (*hexutil.Uint64)(&itx.EffectiveNonce) + // other fields will show up as null. } return json.Marshal(&enc) } @@ -409,6 +444,54 @@ return err } }   + case DepositTxType: + if dec.AccessList != nil || dec.MaxFeePerGas != nil || + dec.MaxPriorityFeePerGas != nil { + return errors.New("unexpected field(s) in deposit transaction") + } + if dec.GasPrice != nil && dec.GasPrice.ToInt().Cmp(common.Big0) != 0 { + return errors.New("deposit transaction GasPrice must be 0") + } + if (dec.V != nil && dec.V.ToInt().Cmp(common.Big0) != 0) || + (dec.R != nil && dec.R.ToInt().Cmp(common.Big0) != 0) || + (dec.S != nil && dec.S.ToInt().Cmp(common.Big0) != 0) { + return errors.New("deposit transaction signature must be 0 or unset") + } + var itx DepositTx + inner = &itx + if dec.To != nil { + itx.To = dec.To + } + if dec.Gas == nil { + return errors.New("missing required field 'gas' for txdata") + } + itx.Gas = uint64(*dec.Gas) + if dec.Value == nil { + return errors.New("missing required field 'value' in transaction") + } + itx.Value = (*big.Int)(dec.Value) + // mint may be omitted or nil if there is nothing to mint. + itx.Mint = (*big.Int)(dec.Mint) + if dec.Input == nil { + return errors.New("missing required field 'input' in transaction") + } + itx.Data = *dec.Input + if dec.From == nil { + return errors.New("missing required field 'from' in transaction") + } + itx.From = *dec.From + if dec.SourceHash == nil { + return errors.New("missing required field 'sourceHash' in transaction") + } + itx.SourceHash = *dec.SourceHash + // IsSystemTx may be omitted. Defaults to false. + if dec.IsSystemTx != nil { + itx.IsSystemTransaction = *dec.IsSystemTx + } + + if dec.Nonce != nil { + inner = &depositTxWithNonce{DepositTx: itx, EffectiveNonce: uint64(*dec.Nonce)} + } default: return ErrTxTypeNotSupported } @@ -419,3 +502,15 @@ // TODO: check hash here? return nil } + +type depositTxWithNonce struct { + DepositTx + EffectiveNonce uint64 +} + +// EncodeRLP ensures that RLP encoding this transaction excludes the nonce. Otherwise, the tx Hash would change +func (tx *depositTxWithNonce) EncodeRLP(w io.Writer) error { + return rlp.Encode(w, tx.DepositTx) +} + +func (tx *depositTxWithNonce) effectiveNonce() *uint64 { return &tx.EffectiveNonce }
diff --git go-ethereum/core/types/transaction_signing.go op-geth/core/types/transaction_signing.go index 73011e238b581e6ab0324d33de362b290181d802..1238c9a6c38d7a5485f4ccf2b8c9b95ea7e5b48a 100644 --- go-ethereum/core/types/transaction_signing.go +++ op-geth/core/types/transaction_signing.go @@ -40,7 +40,7 @@ // MakeSigner returns a Signer based on the given chain config and block number. func MakeSigner(config *params.ChainConfig, blockNumber *big.Int, blockTime uint64) Signer { var signer Signer switch { - case config.IsCancun(blockNumber, blockTime): + case config.IsCancun(blockNumber, blockTime) && !config.IsOptimism(): signer = NewCancunSigner(config.ChainID) case config.IsLondon(blockNumber): signer = NewLondonSigner(config.ChainID) @@ -67,7 +67,7 @@ func LatestSigner(config *params.ChainConfig) Signer { var signer Signer if config.ChainID != nil { switch { - case config.CancunTime != nil: + case config.CancunTime != nil && !config.IsOptimism(): signer = NewCancunSigner(config.ChainID) case config.LondonBlock != nil: signer = NewLondonSigner(config.ChainID) @@ -255,6 +255,14 @@ return londonSigner{eip2930Signer{NewEIP155Signer(chainId)}} }   func (s londonSigner) Sender(tx *Transaction) (common.Address, error) { + if tx.Type() == DepositTxType { + switch tx.inner.(type) { + case *DepositTx: + return tx.inner.(*DepositTx).From, nil + case *depositTxWithNonce: + return tx.inner.(*depositTxWithNonce).From, nil + } + } if tx.Type() != DynamicFeeTxType { return s.eip2930Signer.Sender(tx) } @@ -274,6 +282,9 @@ return ok && x.chainId.Cmp(s.chainId) == 0 }   func (s londonSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { + if tx.Type() == DepositTxType { + return nil, nil, nil, fmt.Errorf("deposits do not have a signature") + } txdata, ok := tx.inner.(*DynamicFeeTx) if !ok { return s.eip2930Signer.SignatureValues(tx, sig) @@ -291,6 +302,9 @@ // Hash returns the hash to be signed by the sender. // It does not uniquely identify the transaction. func (s londonSigner) Hash(tx *Transaction) common.Hash { + if tx.Type() == DepositTxType { + panic("deposits cannot be signed and do not have a signing hash") + } if tx.Type() != DynamicFeeTxType { return s.eip2930Signer.Hash(tx) }

The Transaction type now exposes the deposit-transaction and L1-cost properties required for the rollup.

diff --git go-ethereum/core/types/transaction.go op-geth/core/types/transaction.go index 6c8759ee69b63f182dacf8672023872c5d96dc6b..3c94261061476ac453aaf252e49e0b66d8635cd9 100644 --- go-ethereum/core/types/transaction.go +++ op-geth/core/types/transaction.go @@ -28,6 +28,7 @@ "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" )   @@ -60,6 +61,15 @@ // caches hash atomic.Pointer[common.Hash] size atomic.Uint64 from atomic.Pointer[sigCache] + + // cache of details to compute the data availability fee + rollupCostData atomic.Value + + // optional preconditions for inclusion + conditional atomic.Pointer[TransactionConditional] + + // an indicator if this transaction is rejected during block building + rejected atomic.Bool }   // NewTx creates a new transaction. @@ -86,6 +96,7 @@ gasFeeCap() *big.Int value() *big.Int nonce() uint64 to() *common.Address + isSystemTx() bool   rawSignatureValues() (v, r, s *big.Int) setSignatureValues(chainID, v, r, s *big.Int) @@ -206,6 +217,8 @@ case DynamicFeeTxType: inner = new(DynamicFeeTx) case BlobTxType: inner = new(BlobTx) + case DepositTxType: + inner = new(DepositTx) default: return nil, ErrTxTypeNotSupported } @@ -303,12 +316,62 @@ // Nonce returns the sender account nonce of the transaction. func (tx *Transaction) Nonce() uint64 { return tx.inner.nonce() }   +// EffectiveNonce returns the nonce that was actually used as part of transaction execution +// Returns nil if the effective nonce is not known +func (tx *Transaction) EffectiveNonce() *uint64 { + type txWithEffectiveNonce interface { + effectiveNonce() *uint64 + } + + if itx, ok := tx.inner.(txWithEffectiveNonce); ok { + return itx.effectiveNonce() + } + nonce := tx.inner.nonce() + return &nonce +} + // To returns the recipient address of the transaction. // For contract-creation transactions, To returns nil. func (tx *Transaction) To() *common.Address { return copyAddressPtr(tx.inner.to()) }   +// SourceHash returns the hash that uniquely identifies the source of the deposit tx, +// e.g. a user deposit event, or a L1 info deposit included in a specific L2 block height. +// Non-deposit transactions return a zeroed hash. +func (tx *Transaction) SourceHash() common.Hash { + switch tx.inner.(type) { + case *DepositTx: + return tx.inner.(*DepositTx).SourceHash + case *depositTxWithNonce: + return tx.inner.(*depositTxWithNonce).SourceHash + } + return common.Hash{} +} + +// Mint returns the ETH to mint in the deposit tx. +// This returns nil if there is nothing to mint, or if this is not a deposit tx. +func (tx *Transaction) Mint() *big.Int { + switch tx.inner.(type) { + case *DepositTx: + return tx.inner.(*DepositTx).Mint + case *depositTxWithNonce: + return tx.inner.(*depositTxWithNonce).Mint + } + return nil +} + +// IsDepositTx returns true if the transaction is a deposit tx type. +func (tx *Transaction) IsDepositTx() bool { + return tx.Type() == DepositTxType +} + +// IsSystemTx returns true for deposits that are system transactions. These transactions +// are executed in an unmetered environment & do not contribute to the block gas limit. +func (tx *Transaction) IsSystemTx() bool { + return tx.inner.isSystemTx() +} + // Cost returns (gas * gasPrice) + (blobGas * blobGasPrice) + value. func (tx *Transaction) Cost() *big.Int { total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) @@ -319,6 +382,43 @@ total.Add(total, tx.Value()) return total }   +// RollupCostData caches the information needed to efficiently compute the data availability fee +func (tx *Transaction) RollupCostData() RollupCostData { + if tx.Type() == DepositTxType { + return RollupCostData{} + } + if v := tx.rollupCostData.Load(); v != nil { + return v.(RollupCostData) + } + data, err := tx.MarshalBinary() + if err != nil { // Silent error, invalid txs will not be marshalled/unmarshalled for batch submission anyway. + log.Error("failed to encode tx for L1 cost computation", "err", err) + } + out := NewRollupCostData(data) + tx.rollupCostData.Store(out) + return out +} + +// Conditional returns the conditional attached to the transaction +func (tx *Transaction) Conditional() *TransactionConditional { + return tx.conditional.Load() +} + +// SetConditional attaches a conditional to the transaction +func (tx *Transaction) SetConditional(cond *TransactionConditional) { + tx.conditional.Store(cond) +} + +// Rejected will mark this transaction as rejected. +func (tx *Transaction) SetRejected() { + tx.rejected.Store(true) +} + +// Rejected returns the rejected status of this tx +func (tx *Transaction) Rejected() bool { + return tx.rejected.Load() +} + // RawSignatureValues returns the V, R, S signature values of the transaction. // The return values should not be modified by the caller. // The return values may be nil or zero, if the transaction is unsigned. @@ -350,6 +450,9 @@ // EffectiveGasTip returns the effective miner gasTipCap for the given base fee. // Note: if the effective gasTipCap is negative, this method returns both error // the actual negative value, _and_ ErrGasFeeCapTooLow func (tx *Transaction) EffectiveGasTip(baseFee *big.Int) (*big.Int, error) { + if tx.Type() == DepositTxType { + return new(big.Int), nil + } if baseFee == nil { return tx.GasTipCap(), nil } @@ -416,6 +519,18 @@ } return nil }   +// SetBlobTxSidecar sets the sidecar of a transaction. +// The sidecar should match the blob-tx versioned hashes, or the transaction will be invalid. +// This allows tools to easily re-attach blob sidecars to signed transactions that omit the sidecar. +func (tx *Transaction) SetBlobTxSidecar(sidecar *BlobTxSidecar) error { + blobtx, ok := tx.inner.(*BlobTx) + if !ok { + return fmt.Errorf("not a blob tx, type = %d", tx.Type()) + } + blobtx.Sidecar = sidecar + return nil +} + // BlobGasFeeCapCmp compares the blob fee cap of two transactions. func (tx *Transaction) BlobGasFeeCapCmp(other *Transaction) int { return tx.BlobGasFeeCap().Cmp(other.BlobGasFeeCap()) @@ -466,9 +581,9 @@ } return cpy }   -// SetTime sets the decoding time of a transaction. This is used by tests to set -// arbitrary times and by persistent transaction pools when loading old txs from -// disk. +// SetTime sets the decoding time of a transaction. Used by the sequencer API to +// determine mempool time spent by conditional txs and by tests to set arbitrary +// times and by persistent transaction pools when loading old txs from disk. func (tx *Transaction) SetTime(t time.Time) { tx.time = t }
diff --git go-ethereum/core/types/tx_access_list.go op-geth/core/types/tx_access_list.go index 730a77b752866afb5e6ba3d7ea8f4a5af6456d00..59880b0eabb182cd889a27b58d6c449187f4701a 100644 --- go-ethereum/core/types/tx_access_list.go +++ op-geth/core/types/tx_access_list.go @@ -107,6 +107,7 @@ func (tx *AccessListTx) gasFeeCap() *big.Int { return tx.GasPrice } func (tx *AccessListTx) value() *big.Int { return tx.Value } func (tx *AccessListTx) nonce() uint64 { return tx.Nonce } func (tx *AccessListTx) to() *common.Address { return tx.To } +func (tx *AccessListTx) isSystemTx() bool { return false }   func (tx *AccessListTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { return dst.Set(tx.GasPrice)
diff --git go-ethereum/core/types/tx_blob.go op-geth/core/types/tx_blob.go index ce1f287caaf826cef8513e001437c263fd48f490..d85e3c879ab38a05a1d679f4c805d860eefd8e18 100644 --- go-ethereum/core/types/tx_blob.go +++ op-geth/core/types/tx_blob.go @@ -162,6 +162,7 @@ func (tx *BlobTx) value() *big.Int { return tx.Value.ToBig() } func (tx *BlobTx) nonce() uint64 { return tx.Nonce } func (tx *BlobTx) to() *common.Address { tmp := tx.To; return &tmp } func (tx *BlobTx) blobGas() uint64 { return params.BlobTxBlobGasPerBlob * uint64(len(tx.BlobHashes)) } +func (tx *BlobTx) isSystemTx() bool { return false }   func (tx *BlobTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { if baseFee == nil {
diff --git go-ethereum/core/types/tx_dynamic_fee.go op-geth/core/types/tx_dynamic_fee.go index 8b5b514fdec5b58fc8058e582a0a7c355e0ccba8..9c52cde57754d4826e6d673f297238ce02212657 100644 --- go-ethereum/core/types/tx_dynamic_fee.go +++ op-geth/core/types/tx_dynamic_fee.go @@ -96,6 +96,7 @@ func (tx *DynamicFeeTx) gasPrice() *big.Int { return tx.GasFeeCap } func (tx *DynamicFeeTx) value() *big.Int { return tx.Value } func (tx *DynamicFeeTx) nonce() uint64 { return tx.Nonce } func (tx *DynamicFeeTx) to() *common.Address { return tx.To } +func (tx *DynamicFeeTx) isSystemTx() bool { return false }   func (tx *DynamicFeeTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { if baseFee == nil {
diff --git go-ethereum/core/types/tx_legacy.go op-geth/core/types/tx_legacy.go index 71025b78fc060692c1d034f84b59b4cdc8e6cf65..bcb65c39349a3a1bef78948af48a30a1e14c71ba 100644 --- go-ethereum/core/types/tx_legacy.go +++ op-geth/core/types/tx_legacy.go @@ -103,6 +103,7 @@ func (tx *LegacyTx) gasFeeCap() *big.Int { return tx.GasPrice } func (tx *LegacyTx) value() *big.Int { return tx.Value } func (tx *LegacyTx) nonce() uint64 { return tx.Nonce } func (tx *LegacyTx) to() *common.Address { return tx.To } +func (tx *LegacyTx) isSystemTx() bool { return false }   func (tx *LegacyTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { return dst.Set(tx.GasPrice)

Apply L1 cost computation, and add EVM configuration for tooling and more: - Disable bytecode size-limits (for large test/script contracts). - Prank (solidity test terminology) the EVM-call message-sender. - Override precompiles, to insert tooling precompiles and optimize precompile proving.

diff --git go-ethereum/core/vm/evm.go op-geth/core/vm/evm.go index 616668d565cc0f46531c6dae79229c080931da8d..1945a3b4dbb68f2a5a5c060f0031fd3778c232dd 100644 --- go-ethereum/core/vm/evm.go +++ op-geth/core/vm/evm.go @@ -42,6 +42,10 @@ )   func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { p, ok := evm.precompiles[addr] + if evm.Config.PrecompileOverrides != nil { + override := evm.Config.PrecompileOverrides(evm.chainRules, p, addr) + return override, override != nil + } return p, ok }   @@ -55,6 +59,8 @@ // Transfer transfers ether from one account to the other Transfer TransferFunc // GetHash returns the hash corresponding to n GetHash GetHashFunc + // L1CostFunc returns the L1 cost of the rollup message, the function may be nil, or return nil + L1CostFunc types.L1CostFunc   // Block information Coinbase common.Address // Provides information for COINBASE @@ -163,6 +169,13 @@ // Interpreter returns the current interpreter func (evm *EVM) Interpreter() *EVMInterpreter { return evm.interpreter +} + +func (evm *EVM) maybeOverrideCaller(caller ContractRef) ContractRef { + if evm.Config.CallerOverride != nil { + return evm.Config.CallerOverride(caller) + } + return caller }   // Call executes the contract associated with the addr with the given input as @@ -170,6 +183,7 @@ // parameters. It also handles any necessary value transfer required and takes // the necessary steps to create accounts and reverses the state in case of an // execution error or failed value transfer. func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { + caller = evm.maybeOverrideCaller(caller) // Capture the tracer start/end events in debug mode if evm.Config.Tracer != nil { evm.captureBegin(evm.depth, CALL, caller.Address(), addr, input, gas, value.ToBig()) @@ -255,6 +269,7 @@ // // CallCode differs from Call in the sense that it executes the given address' // code with the caller as context. func (evm *EVM) CallCode(caller ContractRef, addr common.Address, input []byte, gas uint64, value *uint256.Int) (ret []byte, leftOverGas uint64, err error) { + caller = evm.maybeOverrideCaller(caller) // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { evm.captureBegin(evm.depth, CALLCODE, caller.Address(), addr, input, gas, value.ToBig()) @@ -309,13 +324,14 @@ // // DelegateCall differs from CallCode in the sense that it executes the given address' // code with the caller as context and the caller is set to the caller of the caller. func (evm *EVM) DelegateCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) { + caller = evm.maybeOverrideCaller(caller) // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { // NOTE: caller must, at all times be a contract. It should never happen // that caller is something other than a Contract. - parent := caller.(*Contract) + parent := caller.(interface{ Value() *uint256.Int }) // DELEGATECALL inherits value from parent call - evm.captureBegin(evm.depth, DELEGATECALL, caller.Address(), addr, input, gas, parent.value.ToBig()) + evm.captureBegin(evm.depth, DELEGATECALL, caller.Address(), addr, input, gas, parent.Value().ToBig()) defer func(startGas uint64) { evm.captureEnd(evm.depth, startGas, leftOverGas, ret, err) }(gas) @@ -357,6 +373,7 @@ // as parameters while disallowing any modifications to the state during the call. // Opcodes that attempt to perform such modifications will result in exceptions // instead of performing the modifications. func (evm *EVM) StaticCall(caller ContractRef, addr common.Address, input []byte, gas uint64) (ret []byte, leftOverGas uint64, err error) { + caller = evm.maybeOverrideCaller(caller) // Invoke tracer hooks that signal entering/exiting a call frame if evm.Config.Tracer != nil { evm.captureBegin(evm.depth, STATICCALL, caller.Address(), addr, input, gas, nil) @@ -534,7 +551,7 @@ return ret, err }   // Check whether the max code size has been exceeded, assign err if the case. - if evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize { + if evm.chainRules.IsEIP158 && len(ret) > params.MaxCodeSize && !evm.Config.NoMaxCodeSize { return ret, ErrMaxCodeSizeExceeded }   @@ -560,6 +577,7 @@ }   // Create creates a new contract using code as deployment code. func (evm *EVM) Create(caller ContractRef, code []byte, gas uint64, value *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { + caller = evm.maybeOverrideCaller(caller) contractAddr = crypto.CreateAddress(caller.Address(), evm.StateDB.GetNonce(caller.Address())) return evm.create(caller, &codeAndHash{code: code}, gas, value, contractAddr, CREATE) } @@ -569,6 +587,7 @@ // // The different between Create2 with Create is Create2 uses keccak256(0xff ++ msg.sender ++ salt ++ keccak256(init_code))[12:] // instead of the usual sender-and-nonce-hash as the address where the contract is initialized at. func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment *uint256.Int, salt *uint256.Int) (ret []byte, contractAddr common.Address, leftOverGas uint64, err error) { + caller = evm.maybeOverrideCaller(caller) codeAndHash := &codeAndHash{code: code} contractAddr = crypto.CreateAddress2(caller.Address(), salt.Bytes32(), codeAndHash.Hash().Bytes()) return evm.create(caller, codeAndHash, gas, endowment, contractAddr, CREATE2)
diff --git go-ethereum/core/vm/gas_table.go op-geth/core/vm/gas_table.go index d294324b08c2859377482b9c523e05ae50a1a272..2c030a855343c337b7b3c26c0a95f912a38e220a 100644 --- go-ethereum/core/vm/gas_table.go +++ op-geth/core/vm/gas_table.go @@ -314,7 +314,7 @@ size, overflow := stack.Back(2).Uint64WithOverflow() if overflow { return 0, ErrGasUintOverflow } - if size > params.MaxInitCodeSize { + if size > params.MaxInitCodeSize && !evm.Config.NoMaxCodeSize { return 0, fmt.Errorf("%w: size %d", ErrMaxInitCodeSizeExceeded, size) } // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow @@ -333,7 +333,7 @@ size, overflow := stack.Back(2).Uint64WithOverflow() if overflow { return 0, ErrGasUintOverflow } - if size > params.MaxInitCodeSize { + if size > params.MaxInitCodeSize && !evm.Config.NoMaxCodeSize { return 0, fmt.Errorf("%w: size %d", ErrMaxInitCodeSizeExceeded, size) } // Since size <= params.MaxInitCodeSize, these multiplication cannot overflow
diff --git go-ethereum/core/vm/interpreter.go op-geth/core/vm/interpreter.go index 793f398367a7ff038b8c8d53b4ab5804f970d2ac..fc7fc21b4ba87d33e5f95ef8d355bf86fc4bb198 100644 --- go-ethereum/core/vm/interpreter.go +++ op-geth/core/vm/interpreter.go @@ -24,8 +24,13 @@ "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" ) + +// PrecompileOverrides is a function that can be used to override the default precompiled contracts +// Nil is returned when there is no precompile. The original is returned if the existing precompile should run. +type PrecompileOverrides func(rules params.Rules, original PrecompiledContract, addr common.Address) PrecompiledContract   // Config are the configuration options for the Interpreter type Config struct { @@ -35,6 +40,10 @@ EnablePreimageRecording bool // Enables recording of SHA3/keccak preimages ExtraEips []int // Additional EIPS that are to be enabled   StatelessSelfValidation bool // Generate execution witnesses and self-check against them (testing purpose) + + PrecompileOverrides PrecompileOverrides // Precompiles can be swapped / changed / wrapped as needed + NoMaxCodeSize bool // Ignore Max code size and max init code size limits + CallerOverride func(v ContractRef) ContractRef // Swap the caller as needed, for VM prank functionality. }   // ScopeContext contains the things that are per-call, such as stack and memory,

Transactions must pay an additional L1 cost based on the amount of rollup-data-gas they consume, estimated based on gas-price-oracle information and encoded tx size.”

diff --git go-ethereum/core/evm.go op-geth/core/evm.go index 5d3c454d7c472839edd8b84c7a20a8a9a3f1fd02..e8016fbb782145ac28fdf78e02e59f0e2b9784be 100644 --- go-ethereum/core/evm.go +++ op-geth/core/evm.go @@ -25,6 +25,7 @@ "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" )   @@ -39,7 +40,7 @@ GetHeader(common.Hash, uint64) *types.Header }   // NewEVMBlockContext creates a new context for use in the EVM. -func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address) vm.BlockContext { +func NewEVMBlockContext(header *types.Header, chain ChainContext, author *common.Address, config *params.ChainConfig, statedb types.StateGetter) vm.BlockContext { var ( beneficiary common.Address baseFee *big.Int @@ -74,6 +75,7 @@ BaseFee: baseFee, BlobBaseFee: blobBaseFee, GasLimit: header.GasLimit, Random: random, + L1CostFunc: types.NewL1CostFunc(config, statedb), } }
diff --git go-ethereum/core/state_prefetcher.go op-geth/core/state_prefetcher.go index 31405fa078bb2819660a3f71f93aaf241f2599fc..3a8fab85e3c9a01ff3df89b2f51f670d89d148db 100644 --- go-ethereum/core/state_prefetcher.go +++ op-geth/core/state_prefetcher.go @@ -48,7 +48,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, cfg vm.Config, interrupt *atomic.Bool) { var ( header = block.Header() gaspool = new(GasPool).AddGas(block.GasLimit()) - blockContext = NewEVMBlockContext(header, p.chain, nil) + blockContext = NewEVMBlockContext(header, p.chain, nil, p.config, statedb) evm = vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) signer = types.MakeSigner(p.config, header.Number, header.Time) )
diff --git go-ethereum/core/state_processor.go op-geth/core/state_processor.go index c74a33c378f46c4d77961003366c48fe1aa1bf82..a1042cf868eac6a0e09ae481547ee435c3e84ead 100644 --- go-ethereum/core/state_processor.go +++ op-geth/core/state_processor.go @@ -68,12 +68,13 @@ // Mutate the block and state according to any hard-fork specs if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { misc.ApplyDAOHardFork(statedb) } + misc.EnsureCreate2Deployer(p.config, block.Time(), statedb) var ( context vm.BlockContext signer = types.MakeSigner(p.config, header.Number, header.Time) err error ) - context = NewEVMBlockContext(header, p.chain, nil) + context = NewEVMBlockContext(header, p.chain, nil, p.config, statedb) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, p.config, cfg) if beaconRoot := block.BeaconRoot(); beaconRoot != nil { ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) @@ -132,6 +133,11 @@ // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) evm.Reset(txContext, statedb)   + nonce := tx.Nonce() + if msg.IsDepositTx && config.IsOptimismRegolith(evm.Context.Time) { + nonce = statedb.GetNonce(msg.From) + } + // Apply the transaction to the current state (included in the env). result, err := ApplyMessage(evm, msg, gp) if err != nil { @@ -147,11 +153,11 @@ root = statedb.IntermediateRoot(config.IsEIP158(blockNumber)).Bytes() } *usedGas += result.UsedGas   - return MakeReceipt(evm, result, statedb, blockNumber, blockHash, tx, *usedGas, root), nil + return MakeReceipt(evm, result, statedb, blockNumber, blockHash, tx, *usedGas, root, config, nonce), nil }   // MakeReceipt generates the receipt object for a transaction given its execution result. -func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas uint64, root []byte) *types.Receipt { +func MakeReceipt(evm *vm.EVM, result *ExecutionResult, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas uint64, root []byte, config *params.ChainConfig, nonce uint64) *types.Receipt { // Create a new receipt for the transaction, storing the intermediate root and gas used // by the tx. receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: usedGas} @@ -163,6 +169,17 @@ } receipt.TxHash = tx.Hash() receipt.GasUsed = result.UsedGas   + if tx.IsDepositTx() && config.IsOptimismRegolith(evm.Context.Time) { + // The actual nonce for deposit transactions is only recorded from Regolith onwards and + // otherwise must be nil. + receipt.DepositNonce = &nonce + // The DepositReceiptVersion for deposit transactions is only recorded from Canyon onwards + // and otherwise must be nil. + if config.IsOptimismCanyon(evm.Context.Time) { + receipt.DepositReceiptVersion = new(uint64) + *receipt.DepositReceiptVersion = types.CanyonDepositReceiptVersion + } + } if tx.Type() == types.BlobTxType { receipt.BlobGasUsed = uint64(len(tx.BlobHashes()) * params.BlobTxBlobGasPerBlob) receipt.BlobGasPrice = evm.Context.BlobBaseFee @@ -170,7 +187,7 @@ }   // If the transaction created a contract, store the creation address in the receipt. if tx.To() == nil { - receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, tx.Nonce()) + receipt.ContractAddress = crypto.CreateAddress(evm.TxContext.Origin, nonce) }   // Merge the tx-local access event into the "block-local" one, in order to collect @@ -193,12 +210,23 @@ // and uses the input parameters for its environment. It returns the receipt // 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) + blockContext := NewEVMBlockContext(header, bc, author, config, statedb) txContext := NewEVMTxContext(msg) vmenv := vm.NewEVM(blockContext, txContext, statedb, config, cfg) return ApplyTransactionWithEVM(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv)
diff --git go-ethereum/core/types/rollup_cost.go op-geth/core/types/rollup_cost.go new file mode 100644 index 0000000000000000000000000000000000000000..f398c43af1e460aa4f24e7c464ddeb7d2ab6e2e5 --- /dev/null +++ op-geth/core/types/rollup_cost.go @@ -0,0 +1,485 @@ +// Copyright 2022 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. + +package types + +import ( + "bytes" + "encoding/binary" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +const ( + // The two 4-byte Ecotone fee scalar values are packed into the same storage slot as the 8-byte + // sequence number and have the following Solidity offsets within the slot. Note that Solidity + // offsets correspond to the last byte of the value in the slot, counting backwards from the + // end of the slot. For example, The 8-byte sequence number has offset 0, and is therefore + // stored as big-endian format in bytes [24:32) of the slot. + BaseFeeScalarSlotOffset = 12 // bytes [16:20) of the slot + BlobBaseFeeScalarSlotOffset = 8 // bytes [20:24) of the slot + + // scalarSectionStart is the beginning of the scalar values segment in the slot + // array. baseFeeScalar is in the first four bytes of the segment, blobBaseFeeScalar the next + // four. + scalarSectionStart = 32 - BaseFeeScalarSlotOffset - 4 +) + +func init() { + if BlobBaseFeeScalarSlotOffset != BaseFeeScalarSlotOffset-4 { + panic("this code assumes the scalars are at adjacent positions in the scalars slot") + } +} + +var ( + // BedrockL1AttributesSelector is the function selector indicating Bedrock style L1 gas + // attributes. + BedrockL1AttributesSelector = []byte{0x01, 0x5d, 0x8e, 0xb9} + // EcotoneL1AttributesSelector is the selector indicating Ecotone style L1 gas attributes. + EcotoneL1AttributesSelector = []byte{0x44, 0x0a, 0x5e, 0x20} + + // L1BlockAddr is the address of the L1Block contract which stores the L1 gas attributes. + L1BlockAddr = common.HexToAddress("0x4200000000000000000000000000000000000015") + + L1BaseFeeSlot = common.BigToHash(big.NewInt(1)) + OverheadSlot = common.BigToHash(big.NewInt(5)) + ScalarSlot = common.BigToHash(big.NewInt(6)) + + // L1BlobBaseFeeSlot was added with the Ecotone upgrade and stores the blobBaseFee L1 gas + // attribute. + L1BlobBaseFeeSlot = common.BigToHash(big.NewInt(7)) + // L1FeeScalarsSlot as of the Ecotone upgrade stores the 32-bit basefeeScalar and + // blobBaseFeeScalar L1 gas attributes at offsets `BaseFeeScalarSlotOffset` and + // `BlobBaseFeeScalarSlotOffset` respectively. + L1FeeScalarsSlot = common.BigToHash(big.NewInt(3)) + + oneMillion = big.NewInt(1_000_000) + ecotoneDivisor = big.NewInt(1_000_000 * 16) + fjordDivisor = big.NewInt(1_000_000_000_000) + sixteen = big.NewInt(16) + + L1CostIntercept = big.NewInt(-42_585_600) + L1CostFastlzCoef = big.NewInt(836_500) + + MinTransactionSize = big.NewInt(100) + MinTransactionSizeScaled = new(big.Int).Mul(MinTransactionSize, big.NewInt(1e6)) + + emptyScalars = make([]byte, 8) +) + +// RollupCostData is a transaction structure that caches data for quickly computing the data +// availability costs for the transaction. +type RollupCostData struct { + Zeroes, Ones uint64 + FastLzSize uint64 +} + +type StateGetter interface { + GetState(common.Address, common.Hash) common.Hash +} + +// L1CostFunc is used in the state transition to determine the data availability fee charged to the +// sender of non-Deposit transactions. It returns nil if no data availability fee is charged. +type L1CostFunc func(rcd RollupCostData, blockTime uint64) *big.Int + +// l1CostFunc is an internal version of L1CostFunc that also returns the gasUsed for use in +// receipts. +type l1CostFunc func(rcd RollupCostData) (fee, gasUsed *big.Int) + +func NewRollupCostData(data []byte) (out RollupCostData) { + for _, b := range data { + if b == 0 { + out.Zeroes++ + } else { + out.Ones++ + } + } + out.FastLzSize = uint64(FlzCompressLen(data)) + return out +} + +// NewL1CostFunc returns a function used for calculating data availability fees, or nil if this is +// not an op-stack chain. +func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc { + if config.Optimism == nil { + return nil + } + forBlock := ^uint64(0) + var cachedFunc l1CostFunc + selectFunc := func(blockTime uint64) l1CostFunc { + if !config.IsOptimismEcotone(blockTime) { + return newL1CostFuncBedrock(config, statedb, blockTime) + } + + // Note: the various state variables below are not initialized from the DB until this + // point to allow deposit transactions from the block to be processed first by state + // transition. This behavior is consensus critical! + l1FeeScalars := statedb.GetState(L1BlockAddr, L1FeeScalarsSlot).Bytes() + l1BlobBaseFee := statedb.GetState(L1BlockAddr, L1BlobBaseFeeSlot).Big() + l1BaseFee := statedb.GetState(L1BlockAddr, L1BaseFeeSlot).Big() + + // Edge case: the very first Ecotone block requires we use the Bedrock cost + // function. We detect this scenario by checking if the Ecotone parameters are + // unset. Note here we rely on assumption that the scalar parameters are adjacent + // in the buffer and l1BaseFeeScalar comes first. We need to check this prior to + // other forks, as the first block of Fjord and Ecotone could be the same block. + firstEcotoneBlock := l1BlobBaseFee.BitLen() == 0 && + bytes.Equal(emptyScalars, l1FeeScalars[scalarSectionStart:scalarSectionStart+8]) + if firstEcotoneBlock { + log.Info("using bedrock l1 cost func for first Ecotone block", "time", blockTime) + return newL1CostFuncBedrock(config, statedb, blockTime) + } + + l1BaseFeeScalar, l1BlobBaseFeeScalar := extractEcotoneFeeParams(l1FeeScalars) + + if config.IsOptimismFjord(blockTime) { + return NewL1CostFuncFjord( + l1BaseFee, + l1BlobBaseFee, + l1BaseFeeScalar, + l1BlobBaseFeeScalar, + ) + } else { + return newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalar, l1BlobBaseFeeScalar) + } + } + + return func(rollupCostData RollupCostData, blockTime uint64) *big.Int { + if rollupCostData == (RollupCostData{}) { + return nil // Do not charge if there is no rollup cost-data (e.g. RPC call or deposit). + } + if forBlock != blockTime { + if forBlock != ^uint64(0) { + // best practice is not to re-use l1 cost funcs across different blocks, but we + // make it work just in case. + log.Info("l1 cost func re-used for different L1 block", "oldTime", forBlock, "newTime", blockTime) + } + forBlock = blockTime + cachedFunc = selectFunc(blockTime) + } + fee, _ := cachedFunc(rollupCostData) + return fee + } +} + +// newL1CostFuncBedrock returns an L1 cost function suitable for Bedrock, Regolith, and the first +// block only of the Ecotone upgrade. +func newL1CostFuncBedrock(config *params.ChainConfig, statedb StateGetter, blockTime uint64) l1CostFunc { + l1BaseFee := statedb.GetState(L1BlockAddr, L1BaseFeeSlot).Big() + overhead := statedb.GetState(L1BlockAddr, OverheadSlot).Big() + scalar := statedb.GetState(L1BlockAddr, ScalarSlot).Big() + isRegolith := config.IsRegolith(blockTime) + return newL1CostFuncBedrockHelper(l1BaseFee, overhead, scalar, isRegolith) +} + +// newL1CostFuncBedrockHelper is lower level version of newL1CostFuncBedrock that expects already +// extracted parameters +func newL1CostFuncBedrockHelper(l1BaseFee, overhead, scalar *big.Int, isRegolith bool) l1CostFunc { + return func(rollupCostData RollupCostData) (fee, gasUsed *big.Int) { + if rollupCostData == (RollupCostData{}) { + return nil, nil // Do not charge if there is no rollup cost-data (e.g. RPC call or deposit) + } + gas := rollupCostData.Zeroes * params.TxDataZeroGas + if isRegolith { + gas += rollupCostData.Ones * params.TxDataNonZeroGasEIP2028 + } else { + gas += (rollupCostData.Ones + 68) * params.TxDataNonZeroGasEIP2028 + } + gasWithOverhead := new(big.Int).SetUint64(gas) + gasWithOverhead.Add(gasWithOverhead, overhead) + l1Cost := l1CostHelper(gasWithOverhead, l1BaseFee, scalar) + return l1Cost, gasWithOverhead + } +} + +// newL1CostFuncEcotone returns an l1 cost function suitable for the Ecotone upgrade except for the +// very first block of the upgrade. +func newL1CostFuncEcotone(l1BaseFee, l1BlobBaseFee, l1BaseFeeScalar, l1BlobBaseFeeScalar *big.Int) l1CostFunc { + return func(costData RollupCostData) (fee, calldataGasUsed *big.Int) { + calldataGasUsed = bedrockCalldataGasUsed(costData) + + // Ecotone L1 cost function: + // + // (calldataGas/16)*(l1BaseFee*16*l1BaseFeeScalar + l1BlobBaseFee*l1BlobBaseFeeScalar)/1e6 + // + // We divide "calldataGas" by 16 to change from units of calldata gas to "estimated # of bytes when + // compressed". Known as "compressedTxSize" in the spec. + // + // Function is actually computed as follows for better precision under integer arithmetic: + // + // calldataGas*(l1BaseFee*16*l1BaseFeeScalar + l1BlobBaseFee*l1BlobBaseFeeScalar)/16e6 + + calldataCostPerByte := new(big.Int).Set(l1BaseFee) + calldataCostPerByte = calldataCostPerByte.Mul(calldataCostPerByte, sixteen) + calldataCostPerByte = calldataCostPerByte.Mul(calldataCostPerByte, l1BaseFeeScalar) + + blobCostPerByte := new(big.Int).Set(l1BlobBaseFee) + blobCostPerByte = blobCostPerByte.Mul(blobCostPerByte, l1BlobBaseFeeScalar) + + fee = new(big.Int).Add(calldataCostPerByte, blobCostPerByte) + fee = fee.Mul(fee, calldataGasUsed) + fee = fee.Div(fee, ecotoneDivisor) + + return fee, calldataGasUsed + } +} + +type gasParams struct { + l1BaseFee *big.Int + l1BlobBaseFee *big.Int + costFunc l1CostFunc + feeScalar *big.Float // pre-ecotone + l1BaseFeeScalar *uint32 // post-ecotone + l1BlobBaseFeeScalar *uint32 // post-ecotone +} + +// intToScaledFloat returns scalar/10e6 as a float +func intToScaledFloat(scalar *big.Int) *big.Float { + fscalar := new(big.Float).SetInt(scalar) + fdivisor := new(big.Float).SetUint64(1_000_000) // 10**6, i.e. 6 decimals + return new(big.Float).Quo(fscalar, fdivisor) +} + +// extractL1GasParams extracts the gas parameters necessary to compute gas costs from L1 block info +func extractL1GasParams(config *params.ChainConfig, time uint64, data []byte) (gasParams, error) { + // edge case: for the very first Ecotone block we still need to use the Bedrock + // function. We detect this edge case by seeing if the function selector is the old one + // If so, fall through to the pre-ecotone format + // Both Ecotone and Fjord use the same function selector + if config.IsEcotone(time) && len(data) >= 4 && !bytes.Equal(data[0:4], BedrockL1AttributesSelector) { + p, err := extractL1GasParamsPostEcotone(data) + if err != nil { + return gasParams{}, err + } + + if config.IsFjord(time) { + p.costFunc = NewL1CostFuncFjord( + p.l1BaseFee, + p.l1BlobBaseFee, + big.NewInt(int64(*p.l1BaseFeeScalar)), + big.NewInt(int64(*p.l1BlobBaseFeeScalar)), + ) + } else { + p.costFunc = newL1CostFuncEcotone( + p.l1BaseFee, + p.l1BlobBaseFee, + big.NewInt(int64(*p.l1BaseFeeScalar)), + big.NewInt(int64(*p.l1BlobBaseFeeScalar)), + ) + } + + return p, nil + } + return extractL1GasParamsPreEcotone(config, time, data) +} + +func extractL1GasParamsPreEcotone(config *params.ChainConfig, time uint64, data []byte) (gasParams, error) { + // data consists of func selector followed by 7 ABI-encoded parameters (32 bytes each) + if len(data) < 4+32*8 { + return gasParams{}, fmt.Errorf("expected at least %d L1 info bytes, got %d", 4+32*8, len(data)) + } + data = data[4:] // trim function selector + l1BaseFee := new(big.Int).SetBytes(data[32*2 : 32*3]) // arg index 2 + overhead := new(big.Int).SetBytes(data[32*6 : 32*7]) // arg index 6 + scalar := new(big.Int).SetBytes(data[32*7 : 32*8]) // arg index 7 + feeScalar := intToScaledFloat(scalar) // legacy: format fee scalar as big Float + costFunc := newL1CostFuncBedrockHelper(l1BaseFee, overhead, scalar, config.IsRegolith(time)) + return gasParams{ + l1BaseFee: l1BaseFee, + costFunc: costFunc, + feeScalar: feeScalar, + }, nil +} + +// extractL1GasParamsPostEcotone extracts the gas parameters necessary to compute gas from L1 attribute +// info calldata after the Ecotone upgrade, but not for the very first Ecotone block. +func extractL1GasParamsPostEcotone(data []byte) (gasParams, error) { + if len(data) != 164 { + return gasParams{}, fmt.Errorf("expected 164 L1 info bytes, got %d", len(data)) + } + // data layout assumed for Ecotone: + // offset type varname + // 0 <selector> + // 4 uint32 _basefeeScalar + // 8 uint32 _blobBaseFeeScalar + // 12 uint64 _sequenceNumber, + // 20 uint64 _timestamp, + // 28 uint64 _l1BlockNumber + // 36 uint256 _basefee, + // 68 uint256 _blobBaseFee, + // 100 bytes32 _hash, + // 132 bytes32 _batcherHash, + l1BaseFee := new(big.Int).SetBytes(data[36:68]) + l1BlobBaseFee := new(big.Int).SetBytes(data[68:100]) + l1BaseFeeScalar := binary.BigEndian.Uint32(data[4:8]) + l1BlobBaseFeeScalar := binary.BigEndian.Uint32(data[8:12]) + return gasParams{ + l1BaseFee: l1BaseFee, + l1BlobBaseFee: l1BlobBaseFee, + l1BaseFeeScalar: &l1BaseFeeScalar, + l1BlobBaseFeeScalar: &l1BlobBaseFeeScalar, + }, nil +} + +// L1Cost computes the the data availability fee for transactions in blocks prior to the Ecotone +// upgrade. It is used by e2e tests so must remain exported. +func L1Cost(rollupDataGas uint64, l1BaseFee, overhead, scalar *big.Int) *big.Int { + l1GasUsed := new(big.Int).SetUint64(rollupDataGas) + l1GasUsed.Add(l1GasUsed, overhead) + return l1CostHelper(l1GasUsed, l1BaseFee, scalar) +} + +func l1CostHelper(gasWithOverhead, l1BaseFee, scalar *big.Int) *big.Int { + fee := new(big.Int).Set(gasWithOverhead) + fee.Mul(fee, l1BaseFee).Mul(fee, scalar).Div(fee, oneMillion) + return fee +} + +// NewL1CostFuncFjord returns an l1 cost function suitable for the Fjord upgrade +func NewL1CostFuncFjord(l1BaseFee, l1BlobBaseFee, baseFeeScalar, blobFeeScalar *big.Int) l1CostFunc { + return func(costData RollupCostData) (fee, calldataGasUsed *big.Int) { + // Fjord L1 cost function: + //l1FeeScaled = baseFeeScalar*l1BaseFee*16 + blobFeeScalar*l1BlobBaseFee + //estimatedSize = max(minTransactionSize, intercept + fastlzCoef*fastlzSize) + //l1Cost = estimatedSize * l1FeeScaled / 1e12 + + scaledL1BaseFee := new(big.Int).Mul(baseFeeScalar, l1BaseFee) + calldataCostPerByte := new(big.Int).Mul(scaledL1BaseFee, sixteen) + blobCostPerByte := new(big.Int).Mul(blobFeeScalar, l1BlobBaseFee) + l1FeeScaled := new(big.Int).Add(calldataCostPerByte, blobCostPerByte) + estimatedSize := costData.estimatedDASizeScaled() + l1CostScaled := new(big.Int).Mul(estimatedSize, l1FeeScaled) + l1Cost := new(big.Int).Div(l1CostScaled, fjordDivisor) + + calldataGasUsed = new(big.Int).Mul(estimatedSize, new(big.Int).SetUint64(params.TxDataNonZeroGasEIP2028)) + calldataGasUsed.Div(calldataGasUsed, big.NewInt(1e6)) + + return l1Cost, calldataGasUsed + } +} + +// 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]) + l1BlobBaseFeeScalar = new(big.Int).SetBytes(l1FeeParams[offset+4 : offset+8]) + return +} + +func bedrockCalldataGasUsed(costData RollupCostData) (calldataGasUsed *big.Int) { + calldataGas := (costData.Zeroes * params.TxDataZeroGas) + (costData.Ones * params.TxDataNonZeroGasEIP2028) + return new(big.Int).SetUint64(calldataGas) +} + +// FlzCompressLen returns the length of the data after compression through FastLZ, based on +// https://github.com/Vectorized/solady/blob/5315d937d79b335c668896d7533ac603adac5315/js/solady.js +func FlzCompressLen(ib []byte) uint32 { + n := uint32(0) + ht := make([]uint32, 8192) + u24 := func(i uint32) uint32 { + return uint32(ib[i]) | (uint32(ib[i+1]) << 8) | (uint32(ib[i+2]) << 16) + } + cmp := func(p uint32, q uint32, e uint32) uint32 { + l := uint32(0) + for e -= q; l < e; l++ { + if ib[p+l] != ib[q+l] { + e = 0 + } + } + return l + } + literals := func(r uint32) { + n += 0x21 * (r / 0x20) + r %= 0x20 + if r != 0 { + n += r + 1 + } + } + match := func(l uint32) { + l-- + n += 3 * (l / 262) + if l%262 >= 6 { + n += 3 + } else { + n += 2 + } + } + hash := func(v uint32) uint32 { + return ((2654435769 * v) >> 19) & 0x1fff + } + setNextHash := func(ip uint32) uint32 { + ht[hash(u24(ip))] = ip + return ip + 1 + } + a := uint32(0) + ipLimit := uint32(len(ib)) - 13 + if len(ib) < 13 { + ipLimit = 0 + } + for ip := a + 2; ip < ipLimit; { + r := uint32(0) + d := uint32(0) + for { + s := u24(ip) + h := hash(s) + r = ht[h] + ht[h] = ip + d = ip - r + if ip >= ipLimit { + break + } + ip++ + if d <= 0x1fff && s == u24(r) { + break + } + } + if ip >= ipLimit { + break + } + ip-- + if ip > a { + literals(ip - a) + } + l := cmp(r+3, ip+3, ipLimit+9) + match(l) + ip = setNextHash(setNextHash(ip + l)) + a = ip + } + literals(uint32(len(ib)) - a) + return n +}

Deposit transactions have special processing rules: gas is pre-paid on L1, and deposits with EVM-failure are included with rolled back changes (except mint). For regular transactions, at the end of the transition, the 1559 burn and L1 cost are routed to vaults.

diff --git go-ethereum/core/state_transition.go op-geth/core/state_transition.go index d285d03fe2450fa679f35570e68ce4b14a444157..a3b05705c9e4461b05f806c4239d27f934220a0f 100644 --- go-ethereum/core/state_transition.go +++ op-geth/core/state_transition.go @@ -149,6 +149,15 @@ SkipNonceChecks bool   // When SkipFromEOACheck is true, the message sender is not checked to be an EOA. SkipFromEOACheck bool + + IsSystemTx bool // IsSystemTx indicates the message, if also a deposit, does not emit gas usage. + 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 is an optional check of the resulting post-state, if and when the message is + // applied fully to the EVM. This function may return an error to deny inclusion of the message. + PostValidation func(evm *vm.EVM, result *ExecutionResult) error }   // TransactionToMessage converts a transaction into a Message. @@ -167,6 +176,11 @@ SkipNonceChecks: false, SkipFromEOACheck: false, BlobHashes: tx.BlobHashes(), BlobGasFeeCap: tx.BlobGasFeeCap(), + + IsSystemTx: tx.IsSystemTx(), + IsDepositTx: tx.IsDepositTx(), + Mint: tx.Mint(), + RollupCostData: tx.RollupCostData(), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -240,10 +254,20 @@ func (st *StateTransition) buyGas() error { mgval := new(big.Int).SetUint64(st.msg.GasLimit) mgval.Mul(mgval, st.msg.GasPrice) + var l1Cost *big.Int + if st.evm.Context.L1CostFunc != nil && !st.msg.SkipNonceChecks && !st.msg.SkipFromEOACheck { + l1Cost = st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time) + if l1Cost != nil { + mgval = mgval.Add(mgval, l1Cost) + } + } balanceCheck := new(big.Int).Set(mgval) 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)   @@ -282,6 +306,21 @@ return nil }   func (st *StateTransition) preCheck() error { + if st.msg.IsDepositTx { + // No fee fields to check, no nonce to check, and no need to check if EOA (L1 already verified it for us) + // Gas is free, but no refunds! + st.initialGas = st.msg.GasLimit + st.gasRemaining += st.msg.GasLimit // Add gas here in order to be able to execute calls. + // Don't touch the gas pool for system transactions + if st.msg.IsSystemTx { + if st.evm.ChainConfig().IsOptimismRegolith(st.evm.Context.Time) { + return fmt.Errorf("%w: address %v", ErrSystemTxNotSupported, + st.msg.From.Hex()) + } + return nil + } + return st.gp.SubGas(st.msg.GasLimit) // gas used by deposits may not be used by other txs + } // Only check transactions that are not fake msg := st.msg if !msg.SkipNonceChecks { @@ -377,6 +416,52 @@ // // However if any consensus issue encountered, return the error directly with // nil evm execution result. func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { + if mint := st.msg.Mint; mint != nil { + mintU256, overflow := uint256.FromBig(mint) + if overflow { + return nil, fmt.Errorf("mint value exceeds uint256: %d", mintU256) + } + st.state.AddBalance(st.msg.From, mintU256, tracing.BalanceMint) + } + snap := st.state.Snapshot() + + result, err := st.innerTransitionDb() + // Failed deposits must still be included. Unless we cannot produce the block at all due to the gas limit. + // On deposit failure, we rewind any state changes from after the minting, and increment the nonce. + if err != nil && err != ErrGasLimitReached && st.msg.IsDepositTx { + if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnEnter != nil { + st.evm.Config.Tracer.OnEnter(0, byte(vm.STOP), common.Address{}, common.Address{}, nil, 0, nil) + } + + st.state.RevertToSnapshot(snap) + // Even though we revert the state changes, always increment the nonce for the next deposit transaction + st.state.SetNonce(st.msg.From, st.state.GetNonce(st.msg.From)+1) + // Record deposits as using all their gas (matches the gas pool) + // System Transactions are special & are not recorded as using any gas (anywhere) + // Regolith changes this behaviour so the actual gas used is reported. + // In this case the tx is invalid so is recorded as using all gas. + gasUsed := st.msg.GasLimit + if st.msg.IsSystemTx && !st.evm.ChainConfig().IsRegolith(st.evm.Context.Time) { + gasUsed = 0 + } + result = &ExecutionResult{ + UsedGas: gasUsed, + Err: fmt.Errorf("failed deposit: %w", err), + ReturnData: nil, + } + err = nil + } + + if err == nil && st.msg.PostValidation != nil { + if err := st.msg.PostValidation(st.evm, result); err != nil { + return nil, err + } + } + + return result, err +} + +func (st *StateTransition) innerTransitionDb() (*ExecutionResult, error) { // First check this message satisfies all consensus rules before // applying the message. The rules include these clauses // @@ -408,7 +493,11 @@ if st.gasRemaining < gas { return nil, fmt.Errorf("%w: have %d, want %d", ErrIntrinsicGas, st.gasRemaining, gas) } if t := st.evm.Config.Tracer; t != nil && t.OnGasChange != nil { - t.OnGasChange(st.gasRemaining, st.gasRemaining-gas, tracing.GasChangeTxIntrinsicGas) + if st.msg.IsDepositTx { + t.OnGasChange(st.gasRemaining, 0, tracing.GasChangeTxIntrinsicGas) + } else { + t.OnGasChange(st.gasRemaining, st.gasRemaining-gas, tracing.GasChangeTxIntrinsicGas) + } } st.gasRemaining -= gas   @@ -451,6 +540,24 @@ st.state.SetNonce(msg.From, st.state.GetNonce(sender.Address())+1) ret, st.gasRemaining, vmerr = st.evm.Call(sender, st.to(), msg.Data, st.gasRemaining, value) }   + // if deposit: skip refunds, skip tipping coinbase + // Regolith changes this behaviour to report the actual gasUsed instead of always reporting all gas used. + if st.msg.IsDepositTx && !rules.IsOptimismRegolith { + // Record deposits as using all their gas (matches the gas pool) + // System Transactions are special & are not recorded as using any gas (anywhere) + gasUsed := st.msg.GasLimit + if st.msg.IsSystemTx { + gasUsed = 0 + } + return &ExecutionResult{ + UsedGas: gasUsed, + Err: vmerr, + ReturnData: ret, + }, nil + } + // Note for deposit tx there is no ETH refunded for unused gas, but that's taken care of by the fact that gasPrice + // is always 0 for deposit tx. So calling refundGas will ensure the gasUsed accounting is correct without actually + // changing the sender's balance var gasRefund uint64 if !rules.IsLondon { // Before EIP-3529: refunds were capped to gasUsed / 2 @@ -459,6 +566,15 @@ } else { // After EIP-3529: refunds are capped to gasUsed / 5 gasRefund = st.refundGas(params.RefundQuotientEIP3529) } + if st.msg.IsDepositTx && rules.IsOptimismRegolith { + // Skip coinbase payments for deposit tx in Regolith + return &ExecutionResult{ + UsedGas: st.gasUsed(), + RefundedGas: gasRefund, + Err: vmerr, + ReturnData: ret, + }, nil + } effectiveTip := msg.GasPrice if rules.IsLondon { effectiveTip = cmath.BigMin(msg.GasTipCap, new(big.Int).Sub(msg.GasFeeCap, st.evm.Context.BaseFee)) @@ -477,6 +593,24 @@ // add the coinbase to the witness iff the fee is greater than 0 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) + amtU256, overflow := uint256.FromBig(gasCost) + if overflow { + return nil, fmt.Errorf("optimism gas cost overflows U256: %d", gasCost) + } + 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) + } + st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) + } } }
diff --git go-ethereum/core/error.go op-geth/core/error.go index 161538fe4323d2ffeeb69f778cfbe0e119c24157..b8b00121eba7c062f0909e34af4768972cb100b2 100644 --- go-ethereum/core/error.go +++ op-geth/core/error.go @@ -84,6 +84,10 @@ // ErrTxTypeNotSupported is returned if a transaction is not supported in the // 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") @@ -112,4 +116,7 @@ ErrMissingBlobHashes = errors.New("blob transaction missing blob hashes")   // ErrBlobTxCreate is returned if a blob transaction has no explicit to field. ErrBlobTxCreate = errors.New("blob transaction of type create") + + // ErrSystemTxNotSupported is returned for any deposit tx with IsSystemTx=true after the Regolith fork + ErrSystemTxNotSupported = errors.New("system tx not supported") )

The gaslimit is free to be set by the Engine API caller, instead of enforcing adjustments of the gaslimit in increments of 11024 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.

diff --git go-ethereum/consensus/misc/eip1559/eip1559.go op-geth/consensus/misc/eip1559/eip1559.go index 84b82c4c492ec1351da2129b51b99fc50d3c6b28..98456a1653b16e5e13cbc8fff74fdbe21733181f 100644 --- go-ethereum/consensus/misc/eip1559/eip1559.go +++ op-geth/consensus/misc/eip1559/eip1559.go @@ -17,8 +17,10 @@ package eip1559   import ( + "encoding/binary" "errors" "fmt" + gomath "math" "math/big"   "github.com/ethereum/go-ethereum/common" @@ -37,15 +39,17 @@ parentGasLimit := parent.GasLimit if !config.IsLondon(parent.Number) { parentGasLimit = parent.GasLimit * config.ElasticityMultiplier() } - if err := misc.VerifyGaslimit(parentGasLimit, header.GasLimit); err != nil { - return err + if config.Optimism == nil { // gasLimit can adjust instantly in optimism + if err := misc.VerifyGaslimit(parentGasLimit, header.GasLimit); err != nil { + return err + } } // Verify the header is not malformed if header.BaseFee == nil { return errors.New("header is missing baseFee") } // Verify the baseFee is correct based on the parent header. - expectedBaseFee := CalcBaseFee(config, parent) + expectedBaseFee := CalcBaseFee(config, parent, header.Time) if header.BaseFee.Cmp(expectedBaseFee) != 0 { return fmt.Errorf("invalid baseFee: have %s, want %s, parentBaseFee %s, parentGasUsed %d", header.BaseFee, expectedBaseFee, parent.BaseFee, parent.GasUsed) @@ -53,14 +57,99 @@ } return nil }   +// DecodeHolocene1559Params extracts the Holcene 1599 parameters from the encoded form defined here: +// https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#eip-1559-parameters-in-payloadattributesv3 +// +// Returns 0,0 if the format is invalid, though ValidateHolocene1559Params should be used instead of this function for +// validity checking. +func DecodeHolocene1559Params(params []byte) (uint64, uint64) { + if len(params) != 8 { + return 0, 0 + } + denominator := binary.BigEndian.Uint32(params[:4]) + elasticity := binary.BigEndian.Uint32(params[4:]) + return uint64(denominator), uint64(elasticity) +} + +// DecodeHoloceneExtraData decodes the Holocene 1559 parameters from the encoded form defined here: +// https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#eip-1559-parameters-in-block-header +// +// Returns 0,0 if the format is invalid, though ValidateHoloceneExtraData should be used instead of this function for +// validity checking. +func DecodeHoloceneExtraData(extra []byte) (uint64, uint64) { + if len(extra) != 9 { + return 0, 0 + } + return DecodeHolocene1559Params(extra[1:]) +} + +// EncodeHolocene1559Params encodes the eip-1559 parameters into 'PayloadAttributes.EIP1559Params' format. Will panic if +// either value is outside uint32 range. +func EncodeHolocene1559Params(denom, elasticity uint64) []byte { + r := make([]byte, 8) + if denom > gomath.MaxUint32 || elasticity > gomath.MaxUint32 { + panic("eip-1559 parameters out of uint32 range") + } + binary.BigEndian.PutUint32(r[:4], uint32(denom)) + binary.BigEndian.PutUint32(r[4:], uint32(elasticity)) + return r +} + +// EncodeHoloceneExtraData encodes the eip-1559 parameters into the header 'ExtraData' format. Will panic if either +// value is outside uint32 range. +func EncodeHoloceneExtraData(denom, elasticity uint64) []byte { + r := make([]byte, 9) + if denom > gomath.MaxUint32 || elasticity > gomath.MaxUint32 { + panic("eip-1559 parameters out of uint32 range") + } + // leave version byte 0 + binary.BigEndian.PutUint32(r[1:5], uint32(denom)) + binary.BigEndian.PutUint32(r[5:], uint32(elasticity)) + return r +} + +// ValidateHolocene1559Params checks if the encoded parameters are valid according to the Holocene +// upgrade. +func ValidateHolocene1559Params(params []byte) error { + if len(params) != 8 { + return fmt.Errorf("holocene eip-1559 params should be 8 bytes, got %d", len(params)) + } + d, e := DecodeHolocene1559Params(params) + if e != 0 && d == 0 { + return errors.New("holocene params cannot have a 0 denominator unless elasticity is also 0") + } + return nil +} + +// ValidateHoloceneExtraData checks if the header extraData is valid according to the Holocene +// upgrade. +func ValidateHoloceneExtraData(extra []byte) error { + if len(extra) != 9 { + return fmt.Errorf("holocene extraData should be 9 bytes, got %d", len(extra)) + } + if extra[0] != 0 { + return fmt.Errorf("holocene extraData should have 0 version byte, got %d", extra[0]) + } + return ValidateHolocene1559Params(extra[1:]) +} + // CalcBaseFee calculates the basefee of the header. -func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { +// The time belongs to the new block to check which upgrades are active. +func CalcBaseFee(config *params.ChainConfig, parent *types.Header, time uint64) *big.Int { // If the current block is the first EIP-1559 block, return the InitialBaseFee. if !config.IsLondon(parent.Number) { return new(big.Int).SetUint64(params.InitialBaseFee) } - - parentGasTarget := parent.GasLimit / config.ElasticityMultiplier() + elasticity := config.ElasticityMultiplier() + denominator := config.BaseFeeChangeDenominator(time) + if config.IsHolocene(parent.Time) { + denominator, elasticity = DecodeHoloceneExtraData(parent.Extra) + if denominator == 0 { + // this shouldn't happen as the ExtraData should have been validated prior + panic("invalid eip-1559 params in extradata") + } + } + parentGasTarget := parent.GasLimit / elasticity // If the parent gasUsed is the same as the target, the baseFee remains unchanged. if parent.GasUsed == parentGasTarget { return new(big.Int).Set(parent.BaseFee) @@ -77,7 +166,7 @@ // max(1, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) num.SetUint64(parent.GasUsed - parentGasTarget) num.Mul(num, parent.BaseFee) num.Div(num, denom.SetUint64(parentGasTarget)) - num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator())) + num.Div(num, denom.SetUint64(denominator)) baseFeeDelta := math.BigMax(num, common.Big1)   return num.Add(parent.BaseFee, baseFeeDelta) @@ -87,7 +176,7 @@ // max(0, parentBaseFee * gasUsedDelta / parentGasTarget / baseFeeChangeDenominator) num.SetUint64(parentGasTarget - parent.GasUsed) num.Mul(num, parent.BaseFee) num.Div(num, denom.SetUint64(parentGasTarget)) - num.Div(num, denom.SetUint64(config.BaseFeeChangeDenominator())) + num.Div(num, denom.SetUint64(denominator)) baseFee := num.Sub(parent.BaseFee, num)   return math.BigMax(baseFee, common.Big0)
diff --git go-ethereum/consensus/misc/eip1559/eip1559_test.go op-geth/consensus/misc/eip1559/eip1559_test.go index b5afdf0fe5e56ca6ced6031be233c6aafb17dcdd..c48b540b74e62a1146b616f3aa5c4e22b11160a1 100644 --- go-ethereum/consensus/misc/eip1559/eip1559_test.go +++ op-geth/consensus/misc/eip1559/eip1559_test.go @@ -55,6 +55,22 @@ config.LondonBlock = big.NewInt(5) return config }   +func opConfig() *params.ChainConfig { + config := copyConfig(params.TestChainConfig) + config.LondonBlock = big.NewInt(5) + ct := uint64(10) + eip1559DenominatorCanyon := uint64(250) + config.CanyonTime = &ct + ht := uint64(12) + config.HoloceneTime = &ht + config.Optimism = &params.OptimismConfig{ + EIP1559Elasticity: 6, + EIP1559Denominator: 50, + EIP1559DenominatorCanyon: &eip1559DenominatorCanyon, + } + return config +} + // TestBlockGasLimits tests the gasLimit checks for blocks both across // the EIP-1559 boundary and post-1559 blocks func TestBlockGasLimits(t *testing.T) { @@ -124,7 +140,80 @@ GasLimit: test.parentGasLimit, GasUsed: test.parentGasUsed, BaseFee: big.NewInt(test.parentBaseFee), } - if have, want := CalcBaseFee(config(), parent), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 { + if have, want := CalcBaseFee(config(), parent, 0), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 { + t.Errorf("test %d: have %d want %d, ", i, have, want) + } + } +} + +// TestCalcBaseFeeOptimism assumes all blocks are 1559-blocks but tests the Canyon activation +func TestCalcBaseFeeOptimism(t *testing.T) { + tests := []struct { + parentBaseFee int64 + parentGasLimit uint64 + parentGasUsed uint64 + expectedBaseFee int64 + postCanyon bool + }{ + {params.InitialBaseFee, 30_000_000, 5_000_000, params.InitialBaseFee, false}, // usage == target + {params.InitialBaseFee, 30_000_000, 4_000_000, 996000000, false}, // usage below target + {params.InitialBaseFee, 30_000_000, 10_000_000, 1020000000, false}, // usage above target + {params.InitialBaseFee, 30_000_000, 5_000_000, params.InitialBaseFee, true}, // usage == target + {params.InitialBaseFee, 30_000_000, 4_000_000, 999200000, true}, // usage below target + {params.InitialBaseFee, 30_000_000, 10_000_000, 1004000000, true}, // usage above target + } + for i, test := range tests { + parent := &types.Header{ + Number: common.Big32, + GasLimit: test.parentGasLimit, + GasUsed: test.parentGasUsed, + BaseFee: big.NewInt(test.parentBaseFee), + Time: 6, + } + if test.postCanyon { + parent.Time = 8 + } + if have, want := CalcBaseFee(opConfig(), parent, parent.Time+2), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 { + t.Errorf("test %d: have %d want %d, ", i, have, want) + } + if test.postCanyon { + // make sure Holocene activation doesn't change the outcome; since these tests have empty eip1559 params, + // they should be handled using the Canyon config. + parent.Time = 10 + if have, want := CalcBaseFee(opConfig(), parent, parent.Time+2), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 { + t.Errorf("test %d: have %d want %d, ", i, have, want) + } + } + } +} + +// TestCalcBaseFeeOptimismHolocene assumes all blocks are Optimism blocks post-Holocene upgrade +func TestCalcBaseFeeOptimismHolocene(t *testing.T) { + parentBaseFee := int64(10_000_000) + parentGasLimit := uint64(30_000_000) + + tests := []struct { + parentGasUsed uint64 + expectedBaseFee int64 + denom, elasticity uint64 + }{ + {parentGasLimit / 2, parentBaseFee, 10, 2}, // target + {10_000_000, 9_666_667, 10, 2}, // below + {20_000_000, 10_333_333, 10, 2}, // above + {parentGasLimit / 10, parentBaseFee, 2, 10}, // target + {1_000_000, 6_666_667, 2, 10}, // below + {30_000_000, 55_000_000, 2, 10}, // above + } + for i, test := range tests { + parent := &types.Header{ + Number: common.Big32, + GasLimit: parentGasLimit, + GasUsed: test.parentGasUsed, + BaseFee: big.NewInt(parentBaseFee), + Time: 12, + Extra: EncodeHoloceneExtraData(test.denom, test.elasticity), + } + if have, want := CalcBaseFee(opConfig(), parent, parent.Time+2), big.NewInt(test.expectedBaseFee); have.Cmp(want) != 0 { t.Errorf("test %d: have %d want %d, ", i, have, want) } }

The Engine API is activated at the Merge transition, with a Total Terminal Difficulty (TTD). The rollup starts post-merge, and thus sets the TTD to 0. The TTD is always “reached” starting at the bedrock block.

diff --git go-ethereum/consensus/beacon/consensus.go op-geth/consensus/beacon/consensus.go index ae4c9f29a1cd4f00bc471bbe09e16c526cb8fc62..35f6e0cd9a771e1855cedec887472137072dff6d 100644 --- go-ethereum/consensus/beacon/consensus.go +++ op-geth/consensus/beacon/consensus.go @@ -60,6 +60,7 @@ // The beacon here is a half-functional consensus engine with partial functions which // is only used for necessary consensus checks. The legacy consensus engine can be any // engine implements the consensus interface (except the beacon itself). type Beacon struct { + // For migrated OP chains (OP mainnet, OP Goerli), ethone is a dummy legacy pre-Bedrock consensus ethone consensus.Engine // Original consensus engine used in eth1, e.g. ethash or clique }   @@ -107,12 +108,27 @@ } return errs }   +// OP-Stack Bedrock variant of splitHeaders: the total-terminal difficulty is terminated at bedrock transition, but also reset to 0. +// So just use the bedrock fork check to split the headers, to simplify the splitting. +// The returned slices are slices over the input. The input must be sorted. +func (beacon *Beacon) splitBedrockHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) ([]*types.Header, []*types.Header, error) { + for i, h := range headers { + if chain.Config().IsBedrock(h.Number) { + return headers[:i], headers[i:], nil + } + } + return headers, nil, nil +} + // splitHeaders splits the provided header batch into two parts according to // the configured ttd. It requires the parent of header batch along with its // td are stored correctly in chain. If ttd is not configured yet, all headers // will be treated legacy PoW headers. // Note, this function will not verify the header validity but just split them. func (beacon *Beacon) splitHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) ([]*types.Header, []*types.Header, error) { + if chain.Config().Optimism != nil { + return beacon.splitBedrockHeaders(chain, headers) + } // TTD is not defined yet, all headers should be in legacy format. ttd := chain.Config().TerminalTotalDifficulty if ttd == nil { @@ -387,8 +403,21 @@ // Assign the final state root to header. header.Root = state.IntermediateRoot(true)   + if chain.Config().IsOptimismIsthmus(header.Time) { + if body.Withdrawals == nil || len(body.Withdrawals) > 0 { // We verify nil/empty withdrawals in the CL pre-Isthmus + return nil, fmt.Errorf("expected non-nil empty withdrawals operation list in Isthmus, but got: %v", body.Withdrawals) + } + // State-root has just been computed, we can get an accurate storage-root now. + h := state.GetStorageRoot(params.OptimismL2ToL1MessagePasser) + header.WithdrawalsHash = &h + sa := state.AccessEvents() + if sa != nil { + sa.AddAccount(params.OptimismL2ToL1MessagePasser, false) // include in execution witness + } + } + // Assemble the final block. - block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil), chain.Config())   // Create the block witness and attach to block. // This step needs to happen as late as possible to catch all access events. @@ -479,6 +508,10 @@ func (beacon *Beacon) InnerEngine() consensus.Engine { return beacon.ethone }   +func (beacon *Beacon) SwapInner(inner consensus.Engine) { + beacon.ethone = inner +} + // SetThreads updates the mining threads. Delegate the call // to the eth1 engine if it's threaded. func (beacon *Beacon) SetThreads(threads int) { @@ -494,8 +527,16 @@ // IsTTDReached checks if the TotalTerminalDifficulty has been surpassed on the `parentHash` block. // It depends on the parentHash already being stored in the database. // If the parentHash is not stored in the database a UnknownAncestor error is returned. func IsTTDReached(chain consensus.ChainHeaderReader, parentHash common.Hash, parentNumber uint64) (bool, error) { + if cfg := chain.Config(); cfg.Optimism != nil { + // If OP-Stack then bedrock activation number determines when TTD (eth Merge) has been reached. + // Note: some tests/utils will set parentNumber == max_uint64 as "parent" of the genesis block, this is fine. + return cfg.IsBedrock(new(big.Int).SetUint64(parentNumber + 1)), nil + } if chain.Config().TerminalTotalDifficulty == nil { return false, nil + } + if common.Big0.Cmp(chain.Config().TerminalTotalDifficulty) == 0 { // in case TTD is reached at genesis. + return true, nil } td := chain.GetTd(parentHash, parentNumber) if td == nil {

Pre-Bedrock OP-mainnet and OP-Goerli had differently formatted block-headers, loosely compatible with the geth types (since it was based on Clique). However, due to differences like the extra-data length (97+ bytes), these legacy block-headers need special verification. The pre-merge “consensus” fallback is set to this custom but basic verifier, to accept these headers when syncing a pre-bedrock part of the chain, independent of any clique code or configuration (which may be removed from geth at a later point). All the custom verifier has to do is accept the headers, as the headers are already verified by block-hash through the reverse-header-sync.

diff --git go-ethereum/consensus/beacon/oplegacy.go op-geth/consensus/beacon/oplegacy.go new file mode 100644 index 0000000000000000000000000000000000000000..8e94cfa60087a70abf8bc7a65b01043d6d0ac99e --- /dev/null +++ op-geth/consensus/beacon/oplegacy.go @@ -0,0 +1,91 @@ +package beacon + +import ( + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rpc" +) + +type OpLegacy struct{} + +func (o *OpLegacy) Author(header *types.Header) (common.Address, error) { + return header.Coinbase, nil +} + +func (o *OpLegacy) VerifyHeader(chain consensus.ChainHeaderReader, header *types.Header) error { + // Short circuit if the header is known, or its parent not + number := header.Number.Uint64() + if chain.GetHeader(header.Hash(), number) != nil { + return nil + } + parent := chain.GetHeader(header.ParentHash, number-1) + if parent == nil { + return consensus.ErrUnknownAncestor + } + // legacy chain is verified by block-hash reverse sync otherwise + return nil +} + +func (o *OpLegacy) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) { + abort := make(chan struct{}) + results := make(chan error, len(headers)) + + for i := range headers { + // legacy chain is verified by block-hash reverse sync + var parent *types.Header + if i == 0 { + parent = chain.GetHeader(headers[0].ParentHash, headers[0].Number.Uint64()-1) + } else if headers[i-1].Hash() == headers[i].ParentHash { + parent = headers[i-1] + } + var err error + if parent == nil { + err = consensus.ErrUnknownAncestor + } + results <- err + } + return abort, results +} + +func (o *OpLegacy) VerifyUncles(chain consensus.ChainReader, block *types.Block) error { + return nil +} + +func (o *OpLegacy) Prepare(chain consensus.ChainHeaderReader, header *types.Header) error { + return fmt.Errorf("cannot prepare for legacy block header: %s (num %d)", header.Hash(), header.Number) +} + +func (o *OpLegacy) Finalize(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body) { + panic(fmt.Errorf("cannot finalize legacy block header: %s (num %d)", header.Hash(), header.Number)) +} + +func (o *OpLegacy) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *types.Header, state *state.StateDB, body *types.Body, receipts []*types.Receipt) (*types.Block, error) { + return nil, fmt.Errorf("cannot finalize and assemble for legacy block header: %s (num %d)", header.Hash(), header.Number) +} + +func (o *OpLegacy) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { + return fmt.Errorf("cannot seal legacy block header: %s (num %d)", block.Hash(), block.Number()) +} + +func (o *OpLegacy) SealHash(header *types.Header) common.Hash { + panic(fmt.Errorf("cannot compute pow/poa seal-hash for legacy block header: %s (num %d)", header.Hash(), header.Number)) +} + +func (o *OpLegacy) CalcDifficulty(chain consensus.ChainHeaderReader, time uint64, parent *types.Header) *big.Int { + return big.NewInt(0) +} + +func (o *OpLegacy) APIs(chain consensus.ChainHeaderReader) []rpc.API { + return nil +} + +func (o *OpLegacy) Close() error { + return nil +} + +var _ consensus.Engine = (*OpLegacy)(nil)

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. It is also extended to support dynamic EIP-1559 parameters. See Holocene execution engine specs.

diff --git go-ethereum/beacon/engine/gen_blockparams.go op-geth/beacon/engine/gen_blockparams.go index b1f01b50ff8e3b1adf0454ff989356a6f612a337..1a9a45fba586f76cb73c359b8151da4ead09389a 100644 --- go-ethereum/beacon/engine/gen_blockparams.go +++ op-geth/beacon/engine/gen_blockparams.go @@ -21,6 +21,10 @@ Random common.Hash `json:"prevRandao" gencodec:"required"` SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` + NoTxPool bool `json:"noTxPool,omitempty" gencodec:"optional"` + GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` + EIP1559Params hexutil.Bytes `json:"eip1559Params,omitempty" gencodec:"optional"` } var enc PayloadAttributes enc.Timestamp = hexutil.Uint64(p.Timestamp) @@ -28,6 +32,15 @@ enc.Random = p.Random enc.SuggestedFeeRecipient = p.SuggestedFeeRecipient enc.Withdrawals = p.Withdrawals enc.BeaconRoot = p.BeaconRoot + if p.Transactions != nil { + enc.Transactions = make([]hexutil.Bytes, len(p.Transactions)) + for k, v := range p.Transactions { + enc.Transactions[k] = v + } + } + enc.NoTxPool = p.NoTxPool + enc.GasLimit = (*hexutil.Uint64)(p.GasLimit) + enc.EIP1559Params = p.EIP1559Params return json.Marshal(&enc) }   @@ -39,6 +52,10 @@ Random *common.Hash `json:"prevRandao" gencodec:"required"` SuggestedFeeRecipient *common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` + NoTxPool *bool `json:"noTxPool,omitempty" gencodec:"optional"` + GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` + EIP1559Params *hexutil.Bytes `json:"eip1559Params,omitempty" gencodec:"optional"` } var dec PayloadAttributes if err := json.Unmarshal(input, &dec); err != nil { @@ -61,6 +78,21 @@ p.Withdrawals = dec.Withdrawals } if dec.BeaconRoot != nil { p.BeaconRoot = dec.BeaconRoot + } + if dec.Transactions != nil { + p.Transactions = make([][]byte, len(dec.Transactions)) + for k, v := range dec.Transactions { + p.Transactions[k] = v + } + } + if dec.NoTxPool != nil { + p.NoTxPool = *dec.NoTxPool + } + if dec.GasLimit != nil { + p.GasLimit = (*uint64)(dec.GasLimit) + } + if dec.EIP1559Params != nil { + p.EIP1559Params = *dec.EIP1559Params } return nil }
diff --git go-ethereum/beacon/engine/types.go op-geth/beacon/engine/types.go index 74c56f403de2e69d674474769035ac82a9f6e778..3b5e110cfcbc37c5b8b9333cb1d30ac36ca188af 100644 --- go-ethereum/beacon/engine/types.go +++ op-geth/beacon/engine/types.go @@ -48,11 +48,27 @@ Random common.Hash `json:"prevRandao" gencodec:"required"` SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` BeaconRoot *common.Hash `json:"parentBeaconBlockRoot"` + + // Transactions is a field for rollups: the transactions list is forced into the block + Transactions [][]byte `json:"transactions,omitempty" gencodec:"optional"` + // 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"` + // 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"` + // EIP1559Params is a field for rollups implementing the Holocene upgrade, + // and contains encoded EIP-1559 parameters. See: + // https://github.com/ethereum-optimism/specs/blob/main/specs/protocol/holocene/exec-engine.md#eip1559params-encoding + EIP1559Params []byte `json:"eip1559Params,omitempty" gencodec:"optional"` }   // JSON type overrides for PayloadAttributes. type payloadAttributesMarshaling struct { Timestamp hexutil.Uint64 + + Transactions []hexutil.Bytes + GasLimit *hexutil.Uint64 + EIP1559Params hexutil.Bytes }   //go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go @@ -78,6 +94,11 @@ BlobGasUsed *uint64 `json:"blobGasUsed"` ExcessBlobGas *uint64 `json:"excessBlobGas"` Deposits types.Deposits `json:"depositRequests"` ExecutionWitness *types.ExecutionWitness `json:"executionWitness,omitempty"` + + // OP-Stack Isthmus specific field: + // instead of computing the root from a withdrawals list, set it directly. + // The "withdrawals" list attribute must be non-nil but empty. + WithdrawalsRoot *common.Hash `json:"withdrawalsRoot,omitempty"` }   // JSON type overrides for executableData. @@ -110,6 +131,8 @@ BlockValue *big.Int `json:"blockValue" gencodec:"required"` BlobsBundle *BlobsBundleV1 `json:"blobsBundle"` Override bool `json:"shouldOverrideBuilder"` Witness *hexutil.Bytes `json:"witness"` + // OP-Stack: Ecotone specific fields + ParentBeaconBlockRoot *common.Hash `json:"parentBeaconBlockRoot,omitempty"` }   type BlobsBundleV1 struct { @@ -207,8 +230,8 @@ // // and that the blockhash of the constructed block matches the parameters. Nil // Withdrawals value will propagate through the returned block. Empty // Withdrawals value must be passed via non-nil, length 0 value in data. -func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (*types.Block, error) { - block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot) +func ExecutableDataToBlock(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, bType types.BlockType) (*types.Block, error) { + block, err := ExecutableDataToBlockNoHash(data, versionedHashes, beaconRoot, bType) if err != nil { return nil, err } @@ -221,7 +244,7 @@ // ExecutableDataToBlockNoHash is analogous to ExecutableDataToBlock, but is used // for stateless execution, so it skips checking if the executable data hashes to // the requested hash (stateless has to *compute* the root hash, it's not given). -func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash) (*types.Block, error) { +func ExecutableDataToBlockNoHash(data ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, bType types.BlockType) (*types.Block, error) { txs, err := decodeTransactions(data.Transactions) if err != nil { return nil, err @@ -252,7 +275,18 @@ // Only set withdrawalsRoot if it is non-nil. This allows CLs to use // ExecutableData before withdrawals are enabled by marshaling // Withdrawals as the json null value. var withdrawalsRoot *common.Hash - if data.Withdrawals != nil { + if bType.HasOptimismWithdrawalsRoot(data.Timestamp) { + if data.WithdrawalsRoot == nil { + return nil, fmt.Errorf("attribute WithdrawalsRoot is required for Isthmus blocks") + } + if data.Withdrawals == nil || len(data.Withdrawals) > 0 { + return nil, fmt.Errorf("expected non-nil empty withdrawals operation list in Isthmus, but got: %v", data.Withdrawals) + } + } + if data.WithdrawalsRoot != nil { + h := *data.WithdrawalsRoot // copy, avoid any sharing of memory + withdrawalsRoot = &h + } else if data.Withdrawals != nil { h := types.DeriveSha(types.Withdrawals(data.Withdrawals), trie.NewStackTrie(nil)) withdrawalsRoot = &h } @@ -319,6 +353,8 @@ Withdrawals: block.Withdrawals(), BlobGasUsed: block.BlobGasUsed(), ExcessBlobGas: block.ExcessBlobGas(), ExecutionWitness: block.ExecutionWitness(), + // OP-Stack addition: withdrawals list alone does not express the withdrawals storage-root. + WithdrawalsRoot: block.WithdrawalsRoot(), } bundle := BlobsBundleV1{ Commitments: make([]hexutil.Bytes, 0), @@ -333,7 +369,13 @@ bundle.Proofs = append(bundle.Proofs, hexutil.Bytes(sidecar.Proofs[j][:])) } } setRequests(block.Requests(), data) - return &ExecutionPayloadEnvelope{ExecutionPayload: data, BlockValue: fees, BlobsBundle: &bundle, Override: false} + return &ExecutionPayloadEnvelope{ + ExecutionPayload: data, + BlockValue: fees, + BlobsBundle: &bundle, + Override: false, + ParentBeaconBlockRoot: block.BeaconRoot(), + } }   // setRequests differentiates the different request types and
diff --git go-ethereum/eth/catalyst/api.go op-geth/eth/catalyst/api.go index 991cdf93f328ee07f2551b84845096f0aaa4ee89..d776044a33a2b6565ceb4249f896693c46d309bf 100644 --- go-ethereum/eth/catalyst/api.go +++ op-geth/eth/catalyst/api.go @@ -18,6 +18,7 @@ // Package catalyst implements the temporary eth1/eth2 RPC integration. package catalyst   import ( + "bytes" "errors" "fmt" "strconv" @@ -27,6 +28,7 @@ "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/stateless" @@ -394,7 +396,7 @@ } else if api.eth.BlockChain().CurrentBlock().Hash() == update.HeadBlockHash { // If the specified head matches with our local head, do nothing and keep // generating the payload. It's a special corner case that a few slots are // missing and we are requested to generate the payload in slot. - } else { + } else if api.eth.BlockChain().Config().Optimism == nil { // minor Engine API divergence: allow proposers to reorg their own chain // If the head block is already in our canonical chain, the beacon client is // probably resyncing. Ignore the update. log.Info("Ignoring beacon update to old head", "number", block.NumberU64(), "hash", update.HeadBlockHash, "age", common.PrettyAge(time.Unix(int64(block.Time()), 0)), "have", api.eth.BlockChain().CurrentBlock().Number) @@ -434,15 +436,43 @@ } // If payload generation was requested, create a new block to be potentially // sealed by the beacon client. The payload will be requested later, and we // will replace it arbitrarily many times in between. + if payloadAttributes != nil { + var eip1559Params []byte + if api.eth.BlockChain().Config().Optimism != nil { + if payloadAttributes.GasLimit == nil { + return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("gasLimit parameter is required")) + } + if api.eth.BlockChain().Config().IsHolocene(payloadAttributes.Timestamp) { + if err := eip1559.ValidateHolocene1559Params(payloadAttributes.EIP1559Params); err != nil { + return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(err) + } + eip1559Params = bytes.Clone(payloadAttributes.EIP1559Params) + } else if len(payloadAttributes.EIP1559Params) != 0 { + return engine.STATUS_INVALID, + engine.InvalidPayloadAttributes.With(errors.New("eip155Params not supported prior to Holocene upgrade")) + } + } + transactions := make(types.Transactions, 0, len(payloadAttributes.Transactions)) + for i, otx := range payloadAttributes.Transactions { + var tx types.Transaction + if err := tx.UnmarshalBinary(otx); err != nil { + return engine.STATUS_INVALID, fmt.Errorf("transaction %d is not valid: %v", i, err) + } + transactions = append(transactions, &tx) + } args := &miner.BuildPayloadArgs{ - Parent: update.HeadBlockHash, - Timestamp: payloadAttributes.Timestamp, - FeeRecipient: payloadAttributes.SuggestedFeeRecipient, - Random: payloadAttributes.Random, - Withdrawals: payloadAttributes.Withdrawals, - BeaconRoot: payloadAttributes.BeaconRoot, - Version: payloadVersion, + Parent: update.HeadBlockHash, + Timestamp: payloadAttributes.Timestamp, + FeeRecipient: payloadAttributes.SuggestedFeeRecipient, + Random: payloadAttributes.Random, + Withdrawals: payloadAttributes.Withdrawals, + BeaconRoot: payloadAttributes.BeaconRoot, + NoTxPool: payloadAttributes.NoTxPool, + Transactions: transactions, + GasLimit: payloadAttributes.GasLimit, + Version: payloadVersion, + EIP1559Params: eip1559Params, } id := args.Id() // If we already are busy generating this work, then we do not need @@ -811,11 +841,19 @@ // 3. When the db compaction ends, then N calls inserting the same payload are processed // sequentially. // Hence, we use a lock here, to be sure that the previous call has finished before we // check whether we already have the block locally. + + // Payload must have eip-1559 params in ExtraData after Holocene + if api.eth.BlockChain().Config().IsHolocene(params.Timestamp) { + if err := eip1559.ValidateHoloceneExtraData(params.ExtraData); err != nil { + return api.invalid(err, nil), nil + } + } + api.newPayloadLock.Lock() defer api.newPayloadLock.Unlock()   log.Trace("Engine API request received", "method", "NewPayload", "number", params.Number, "hash", params.BlockHash) - block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot) + block, err := engine.ExecutableDataToBlock(params, versionedHashes, beaconRoot, api.eth.BlockChain().Config()) if err != nil { bgu := "nil" if params.BlobGasUsed != nil { @@ -843,6 +881,7 @@ "params.ExcessBlobGas", ebg, "len(params.Transactions)", len(params.Transactions), "len(params.Withdrawals)", len(params.Withdrawals), "len(params.Deposits)", len(params.Deposits), + "params.WithdrawalsRoot", params.WithdrawalsRoot, "beaconRoot", beaconRoot, "error", err) return api.invalid(err, nil), nil @@ -930,7 +969,7 @@ func (api *ConsensusAPI) executeStatelessPayload(params engine.ExecutableData, versionedHashes []common.Hash, beaconRoot *common.Hash, opaqueWitness hexutil.Bytes) (engine.StatelessPayloadStatusV1, error) { log.Trace("Engine API request received", "method", "ExecuteStatelessPayload", "number", params.Number, "hash", params.BlockHash)   - block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot) + block, err := engine.ExecutableDataToBlockNoHash(params, versionedHashes, beaconRoot, api.eth.BlockChain().Config()) if err != nil { bgu := "nil" if params.BlobGasUsed != nil { @@ -958,6 +997,7 @@ "params.ExcessBlobGas", ebg, "len(params.Transactions)", len(params.Transactions), "len(params.Withdrawals)", len(params.Withdrawals), "len(params.Deposits)", len(params.Deposits), + "params.WithdrawalsRoot", params.WithdrawalsRoot, "beaconRoot", beaconRoot, "error", err) errorMsg := err.Error() @@ -1108,6 +1148,9 @@ // user that something might be off with their consensus node. // // TODO(karalabe): Spin this goroutine down somehow func (api *ConsensusAPI) heartbeat() { + if api.eth.BlockChain().Config().Optimism != nil { // don't start the api heartbeat, there is no transition + return + } // Sleep a bit on startup since there's obviously no beacon client yet // attached, so no need to print scary warnings to the user. time.Sleep(beaconUpdateStartupTimeout)

The block-building code (in the “miner” package because of Proof-Of-Work legacy of ethereum) implements the changes to support the transaction-inclusion, tx-pool toggle, gaslimit, and EIP-1559 parameters of the Engine API. This also includes experimental support for interop executing-messages to be verified through an RPC.

diff --git go-ethereum/miner/miner.go op-geth/miner/miner.go index 9892c08ed6e5e0cca83348bb3979df3c1a5e9216..447cf4ccbd2f59fac1c9c1f7d391f2d74c726d01 100644 --- go-ethereum/miner/miner.go +++ op-geth/miner/miner.go @@ -18,6 +18,7 @@ // Package miner implements Ethereum block creation and mining. package miner   import ( + "context" "fmt" "math/big" "sync" @@ -30,6 +31,8 @@ "github.com/ethereum/go-ethereum/core" "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" )   @@ -40,6 +43,14 @@ BlockChain() *core.BlockChain TxPool() *txpool.TxPool }   +type BackendWithHistoricalState interface { + StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, tracers.StateReleaseFunc, error) +} + +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 @@ -48,12 +59,19 @@ ExtraData hexutil.Bytes `toml:",omitempty"` // Block extra data set by the miner GasCeil uint64 // Target gas ceiling for mined blocks. GasPrice *big.Int // Minimum gas price for mining a transaction Recommit time.Duration // The time interval for miner to re-create mining work. + + 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 + MaxDATxSize *big.Int `toml:",omitempty"` // if non-nil, don't include any txs with data availability size larger than this in any built block + MaxDABlockSize *big.Int `toml:",omitempty"` // if non-nil, then don't build a block requiring more than this amount of total data availability }   // DefaultConfig contains default settings for miner. var DefaultConfig = Config{ GasCeil: 30_000_000, - GasPrice: big.NewInt(params.GWei / 1000), + GasPrice: big.NewInt(params.Wei),   // The default recommit time is chosen as two seconds since // consensus-layer usually will wait a half slot of time(6s) @@ -73,17 +91,27 @@ txpool *txpool.TxPool chain *core.BlockChain pending *pending 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, chainConfig: eth.BlockChain().Config(), engine: engine, txpool: eth.TxPool(), chain: eth.BlockChain(), pending: &pending{}, + // To interrupt background tasks that may be attached to external processes + lifeCtxCancel: cancel, + lifeCtx: ctx, } }   @@ -91,6 +119,19 @@ // Pending returns the currently pending block and associated receipts, logs // and statedb. The returned values can be nil in case the pending block is // not initialized. func (miner *Miner) Pending() (*types.Block, types.Receipts, *state.StateDB) { + if miner.chainConfig.Optimism != nil && !miner.config.RollupComputePendingBlock { + // For compatibility when not computing a pending block, we serve the latest block as "pending" + headHeader := miner.chain.CurrentHeader() + headBlock := miner.chain.GetBlock(headHeader.Hash(), headHeader.Number.Uint64()) + headReceipts := miner.chain.GetReceiptsByHash(headHeader.Hash()) + stateDB, err := miner.chain.StateAt(headHeader.Root) + if err != nil { + return nil, nil, nil + } + + return headBlock, headReceipts, stateDB.Copy() + } + pending := miner.getPending() if pending == nil { return nil, nil, nil @@ -125,6 +166,22 @@ miner.confMu.Unlock() 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) @@ -163,3 +220,7 @@ } miner.pending.update(header.Hash(), ret) return ret } + +func (miner *Miner) Close() { + miner.lifeCtxCancel() +}
diff --git go-ethereum/miner/miner_test.go op-geth/miner/miner_test.go index b92febdd1254ab7cc8832c9d141ecaf141c380cf..09ad909ca95f2c21446c39c05aa42d86cf043d87 100644 --- go-ethereum/miner/miner_test.go +++ op-geth/miner/miner_test.go @@ -21,6 +21,7 @@ import ( "math/big" "sync" "testing" + "time"   "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/clique" @@ -74,11 +75,12 @@ func (bc *testBlockChain) CurrentBlock() *types.Header { return &types.Header{ Number: new(big.Int), GasLimit: bc.gasLimit, + BaseFee: big.NewInt(params.InitialBaseFee), } }   func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block { - return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil)) + return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil), types.DefaultBlockConfig) }   func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) { @@ -139,16 +141,18 @@ func createMiner(t *testing.T) *Miner { // Create Ethash config config := Config{ - PendingFeeRecipient: common.HexToAddress("123456789"), + PendingFeeRecipient: common.HexToAddress("123456789"), + RollupTransactionConditionalRateLimit: params.TransactionConditionalMaxCost, } // Create chainConfig chainDB := rawdb.NewMemoryDatabase() triedb := triedb.NewDatabase(chainDB, nil) - genesis := minerTestGenesisBlock(15, 11_500_000, common.HexToAddress("12345")) + genesis := minerTestGenesisBlock(15, 11_500_000, testBankAddress) chainConfig, _, err := core.SetupGenesisBlock(chainDB, triedb, genesis) if err != nil { t.Fatalf("can't create new chain config: %v", err) } + // Create consensus engine engine := clique.New(chainConfig.Clique, chainDB) // Create Ethereum backend @@ -160,10 +164,57 @@ statedb, _ := state.New(bc.Genesis().Root(), bc.StateCache()) 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) miner := New(backend, config, engine) return miner } + +func TestRejectedConditionalTx(t *testing.T) { + miner := createMiner(t) + timestamp := uint64(time.Now().Unix()) + uint64Ptr := func(num uint64) *uint64 { return &num } + + // add a conditional transaction to be rejected + signer := types.LatestSigner(miner.chainConfig) + tx := types.MustSignNewTx(testBankKey, signer, &types.LegacyTx{ + Nonce: 0, + To: &testUserAddress, + Value: big.NewInt(1000), + Gas: params.TxGas, + GasPrice: big.NewInt(params.InitialBaseFee), + }) + tx.SetConditional(&types.TransactionConditional{TimestampMax: uint64Ptr(timestamp - 1)}) + + // 1 pending tx (synchronously, it has to be there before it can be rejected) + miner.txpool.Add(types.Transactions{tx}, true, true) + if !miner.txpool.Has(tx.Hash()) { + t.Fatalf("conditional tx is not in the mempool") + } + + // request block + r := miner.generateWork(&generateParams{ + parentHash: miner.chain.CurrentBlock().Hash(), + timestamp: timestamp, + random: common.HexToHash("0xcafebabe"), + noTxs: false, + forceTime: true, + }, false) + + if len(r.block.Transactions()) != 0 { + t.Fatalf("block should be empty") + } + + // tx is rejected + if !tx.Rejected() { + t.Fatalf("conditional tx is not marked as rejected") + } + + // rejected conditional is evicted from the txpool + miner.txpool.Sync() + if miner.txpool.Has(tx.Hash()) { + t.Fatalf("conditional tx is still in the mempool") + } +}
diff --git go-ethereum/miner/payload_building.go op-geth/miner/payload_building.go index d48ce0faa601f547bb154a54e8778cc71b152da7..2dd574c15c698978ddc0b6f319ddd8eb9ef817a2 100644 --- go-ethereum/miner/payload_building.go +++ op-geth/miner/payload_building.go @@ -17,10 +17,13 @@ package miner   import ( + "context" "crypto/sha256" "encoding/binary" + "errors" "math/big" "sync" + "sync/atomic" "time"   "github.com/ethereum/go-ethereum/beacon/engine" @@ -44,6 +47,11 @@ Random common.Hash // The provided randomness value Withdrawals types.Withdrawals // The provided withdrawals BeaconRoot *common.Hash // The provided beaconRoot (Cancun) Version engine.PayloadVersion // Versioning byte for payload id calculation. + + NoTxPool bool // Optimism addition: option to disable tx pool contents from being included + 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 + EIP1559Params []byte // Optimism addition: encodes Holocene EIP-1559 params }   // Id computes an 8-byte identifier by hashing the components of the payload arguments. @@ -57,6 +65,22 @@ rlp.Encode(hasher, args.Withdrawals) if args.BeaconRoot != nil { hasher.Write(args.BeaconRoot[:]) } + + if args.NoTxPool || len(args.Transactions) > 0 { // extend if extra payload attributes are used + binary.Write(hasher, binary.BigEndian, args.NoTxPool) + binary.Write(hasher, binary.BigEndian, uint64(len(args.Transactions))) + for _, tx := range args.Transactions { + h := tx.Hash() + hasher.Write(h[:]) + } + } + if args.GasLimit != nil { + binary.Write(hasher, binary.BigEndian, *args.GasLimit) + } + if len(args.EIP1559Params) != 0 { + hasher.Write(args.EIP1559Params[:]) + } + var out engine.PayloadID copy(out[:], hasher.Sum(nil)[:8]) out[0] = byte(args.Version) @@ -79,20 +103,35 @@ fullFees *big.Int stop chan struct{} lock sync.Mutex cond *sync.Cond + + 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, emptyWitness: witness, 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) return payload } + +var errInterruptedUpdate = errors.New("interrupted payload update")   // update updates the full-block with latest built version. func (payload *Payload) update(r *newPayloadResult, elapsed time.Duration) { @@ -104,6 +143,19 @@ case <-payload.stop: return // reject stale update default: } + + defer payload.cond.Broadcast() // fire signal for notifying any full block result + + if errors.Is(r.err, errInterruptedUpdate) { + log.Debug("Ignoring interrupted payload update", "id", payload.id) + return + } else if r.err != nil { + log.Warn("Error building payload update", "id", payload.id, "err", r.err) + payload.err = r.err // record latest error + return + } + log.Debug("New payload update", "id", payload.id, "elapsed", common.PrettyDuration(elapsed)) + // Ensure the newly provided full block has a higher transaction fee. // In post-merge stage, there is no uncle reward anymore and transaction // fee(apart from the mev revenue) is the only indicator for comparison. @@ -126,34 +178,12 @@ "root", r.block.Root(), "elapsed", common.PrettyDuration(elapsed), ) } - payload.cond.Broadcast() // fire signal for notifying full block }   // Resolve returns the latest built payload and also terminates the background // thread for updating payload. It's safe to be called multiple times. func (payload *Payload) Resolve() *engine.ExecutionPayloadEnvelope { - payload.lock.Lock() - defer payload.lock.Unlock() - - select { - case <-payload.stop: - default: - close(payload.stop) - } - if payload.full != nil { - envelope := engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars) - if payload.fullWitness != nil { - envelope.Witness = new(hexutil.Bytes) - *envelope.Witness, _ = rlp.EncodeToBytes(payload.fullWitness) // cannot fail - } - return envelope - } - envelope := engine.BlockToExecutableData(payload.empty, big.NewInt(0), nil) - if payload.emptyWitness != nil { - envelope.Witness = new(hexutil.Bytes) - *envelope.Witness, _ = rlp.EncodeToBytes(payload.emptyWitness) // cannot fail - } - return envelope + return payload.resolve(false) }   // ResolveEmpty is basically identical to Resolve, but it expects empty block only. @@ -173,10 +203,24 @@ // ResolveFull is basically identical to Resolve, but it expects full block only. // Don't call Resolve until ResolveFull returns, otherwise it might block forever. func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope { + return payload.resolve(true) +} + +func (payload *Payload) WaitFull() { + payload.lock.Lock() + defer payload.lock.Unlock() + payload.cond.Wait() +} + +func (payload *Payload) resolve(onlyFull bool) *engine.ExecutionPayloadEnvelope { payload.lock.Lock() defer payload.lock.Unlock()   - if payload.full == nil { + // We interrupt any active building block to prevent it from adding more transactions, + // and if it is an update, don't attempt to seal the block. + payload.interruptBuilding() + + if payload.full == nil && (onlyFull || payload.empty == nil) { select { case <-payload.stop: return nil @@ -187,41 +231,120 @@ // forever if Resolve is called in the meantime which // terminates the background construction process. payload.cond.Wait() } - // Terminate the background payload construction - select { - case <-payload.stop: - default: - close(payload.stop) + + // Now we can signal the building routine to stop. + payload.stopBuilding() + + if payload.full != nil { + envelope := engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars) + if payload.fullWitness != nil { + envelope.Witness = new(hexutil.Bytes) + *envelope.Witness, _ = rlp.EncodeToBytes(payload.fullWitness) // cannot fail + } + return envelope + } else if !onlyFull && payload.empty != nil { + envelope := engine.BlockToExecutableData(payload.empty, big.NewInt(0), nil) + if payload.emptyWitness != nil { + envelope.Witness = new(hexutil.Bytes) + *envelope.Witness, _ = rlp.EncodeToBytes(payload.emptyWitness) // cannot fail + } + } else if err := payload.err; err != nil { + log.Error("Error building any payload", "id", payload.id, "err", err) } - envelope := engine.BlockToExecutableData(payload.full, payload.fullFees, payload.sidecars) - if payload.fullWitness != nil { - envelope.Witness = new(hexutil.Bytes) - *envelope.Witness, _ = rlp.EncodeToBytes(payload.fullWitness) // cannot fail + return nil +} + +// interruptBuilding sets an interrupt for a potentially ongoing +// block building process. +// This will prevent it from adding new transactions to the block, and if it is +// building an update, the block will also not be sealed, as we would discard +// 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. + if payload.interrupt.CompareAndSwap(commitInterruptNone, commitInterruptResolve) { + log.Debug("Interrupted payload building.", "id", payload.id) + } else { + log.Debug("Payload building already interrupted.", + "id", payload.id, "interrupt", payload.interrupt.Load()) } - return envelope +} + +// stopBuilding signals to the block updating routine to stop. An ongoing payload +// building job will still complete. It can be interrupted to stop filling new +// 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) + close(payload.stop) + }) }   // buildPayload builds the payload according to the provided parameters. func (miner *Miner) buildPayload(args *BuildPayloadArgs, witness bool) (*Payload, error) { - // Build the initial version with no transaction included. It should be fast - // enough to run. The empty payload can at least make sure there is something - // to deliver for not missing slot. - emptyParams := &generateParams{ - timestamp: args.Timestamp, - forceTime: true, - parentHash: args.Parent, - coinbase: args.FeeRecipient, - random: args.Random, - withdrawals: args.Withdrawals, - beaconRoot: args.BeaconRoot, - noTxs: true, + if args.NoTxPool { // don't start the background payload updating job if there is no tx pool to pull from + // Build the initial version with no transaction included. It should be fast + // enough to run. The empty payload can at least make sure there is something + // to deliver for not missing slot. + // In OP-Stack, the "empty" block is constructed from provided txs only, i.e. no tx-pool usage. + emptyParams := &generateParams{ + timestamp: args.Timestamp, + forceTime: true, + parentHash: args.Parent, + coinbase: args.FeeRecipient, + random: args.Random, + withdrawals: args.Withdrawals, + beaconRoot: args.BeaconRoot, + noTxs: true, + 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(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 + payload.cond.Broadcast() // unblocks Resolve + return payload, nil } - empty := miner.generateWork(emptyParams, witness) - if empty.err != nil { - return nil, empty.err + + fullParams := &generateParams{ + timestamp: args.Timestamp, + forceTime: true, + parentHash: args.Parent, + coinbase: args.FeeRecipient, + random: args.Random, + withdrawals: args.Withdrawals, + beaconRoot: args.BeaconRoot, + noTxs: false, + txs: args.Transactions, + gasLimit: args.GasLimit, + eip1559Params: args.EIP1559Params, } - // Construct a payload object for return. - payload := newPayload(empty.block, empty.witness, args.Id()) + + // Since we skip building the empty block when using the tx pool, we need to explicitly + // validate the BuildPayloadArgs here. + blockTime, err := miner.validateParams(fullParams) + if err != nil { + return nil, err + } + + 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. @@ -231,38 +354,61 @@ // for triggering process immediately. timer := time.NewTimer(0) defer timer.Stop()   - // Setup the timer for terminating the process if SECONDS_PER_SLOT (12s in - // the Mainnet configuration) have passed since the point in time identified - // by the timestamp parameter. - endTimer := time.NewTimer(time.Second * 12) + start := time.Now() + // Setup the timer for terminating the payload building process as determined + // by validateParams. + endTimer := time.NewTimer(blockTime) + defer endTimer.Stop() + + timeout := time.Now().Add(blockTime) + + stopReason := "delivery" + defer func() { + log.Info("Stopping work on payload", + "id", payload.id, + "reason", stopReason, + "elapsed", common.PrettyDuration(time.Since(start))) + }()   - fullParams := &generateParams{ - timestamp: args.Timestamp, - forceTime: true, - parentHash: args.Parent, - coinbase: args.FeeRecipient, - random: args.Random, - withdrawals: args.Withdrawals, - beaconRoot: args.BeaconRoot, - noTxs: false, + updatePayload := func() time.Duration { + start := time.Now() + // getSealingBlock is interrupted by shared interrupt + r := miner.generateWork(fullParams, witness) + dur := time.Since(start) + // update handles error case + payload.update(r, dur) + if r.err == nil { + // after first successful pass, we're updating + fullParams.isUpdate = true + } + timer.Reset(miner.config.Recommit) + return dur }   + var lastDuration time.Duration for { select { + case <-miner.lifeCtx.Done(): + stopReason = "miner-shutdown" case <-timer.C: - start := time.Now() - r := miner.generateWork(fullParams, witness) - if r.err == nil { - payload.update(r, time.Since(start)) - } else { - log.Info("Error while generating work", "id", payload.id, "err", r.err) + // We have to prioritize the stop signal because the recommit timer + // might have fired while stop also got closed. + select { + case <-payload.stop: + return + default: } - timer.Reset(miner.config.Recommit) + // Assuming last payload building duration as lower bound for next one, + // skip new update if we're too close to the timeout anyways. + if lastDuration > 0 && time.Now().Add(lastDuration).After(timeout) { + stopReason = "near-timeout" + return + } + lastDuration = updatePayload() case <-payload.stop: - log.Info("Stopping work on payload", "id", payload.id, "reason", "delivery") return case <-endTimer.C: - log.Info("Stopping work on payload", "id", payload.id, "reason", "timeout") + stopReason = "timeout" return } }
diff --git go-ethereum/miner/payload_building_test.go op-geth/miner/payload_building_test.go index aad87627e6746187b4eebc1a2db556ae7094ee11..ee5e2e69444a4e23647e5874240ae879ce93c499 100644 --- go-ethereum/miner/payload_building_test.go +++ op-geth/miner/payload_building_test.go @@ -17,6 +17,8 @@ package miner   import ( + "bytes" + "crypto/rand" "math/big" "reflect" "testing" @@ -28,6 +30,7 @@ "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/clique" "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/txpool" @@ -53,6 +56,9 @@ testUserKey, _ = crypto.GenerateKey() testUserAddress = crypto.PubkeyToAddress(testUserKey.PublicKey)   + testRecipient = common.HexToAddress("0xdeadbeef") + testTimestamp = uint64(time.Now().Unix()) + // Test transactions pendingTxs []*types.Transaction newTxs []*types.Transaction @@ -60,10 +66,14 @@ testConfig = Config{ PendingFeeRecipient: testBankAddress, Recommit: time.Second, - GasCeil: params.GenesisGasLimit, + GasCeil: 50_000_000, } )   +const ( + numDAFilterTxs = 256 +) + func init() { testTxPoolConfig = legacypool.DefaultConfig testTxPoolConfig.Journal = "" @@ -120,13 +130,17 @@ }) case *ethash.Ethash: default: t.Fatalf("unexpected consensus engine type: %T", engine) + } + if chainConfig.HoloceneTime != nil { + // genesis block extraData needs to be correct format + gspec.ExtraData = []byte{0, 0, 1, 2, 3, 4, 5, 6, 7} } chain, err := core.NewBlockChain(db, &core.CacheConfig{TrieDirtyDisabled: true}, gspec, nil, engine, vm.Config{}, nil) if err != nil { 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, @@ -141,30 +155,109 @@ 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 }   func TestBuildPayload(t *testing.T) { - var ( - db = rawdb.NewMemoryDatabase() - recipient = common.HexToAddress("0xdeadbeef") - ) - w, b := newTestWorker(t, params.TestChainConfig, ethash.NewFaker(), db, 0) + t.Run("no-tx-pool", func(t *testing.T) { testBuildPayload(t, true, false, nil) }) + // no-tx-pool case with interrupt not interesting because no-tx-pool doesn't run + // the builder routine + t.Run("with-tx-pool", func(t *testing.T) { testBuildPayload(t, false, false, nil) }) + t.Run("with-tx-pool-interrupt", func(t *testing.T) { testBuildPayload(t, false, true, nil) }) + params1559 := []byte{0, 1, 2, 3, 4, 5, 6, 7} + t.Run("with-params", func(t *testing.T) { testBuildPayload(t, false, false, params1559) }) + t.Run("with-params-no-tx-pool", func(t *testing.T) { testBuildPayload(t, true, false, params1559) }) + t.Run("with-params-interrupt", func(t *testing.T) { testBuildPayload(t, false, true, params1559) }) + + t.Run("wrong-config-no-params", func(t *testing.T) { testBuildPayloadWrongConfig(t, nil) }) + t.Run("wrong-config-params", func(t *testing.T) { testBuildPayloadWrongConfig(t, params1559) }) + + zeroParams := make([]byte, 8) + 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) + t := uint64(0) + config.CanyonTime = &t + config.HoloceneTime = &t + canyonDenom := uint64(250) + config.Optimism = &params.OptimismConfig{ + EIP1559Elasticity: 6, + EIP1559Denominator: 50, + EIP1559DenominatorCanyon: &canyonDenom, + } + return &config +}   - timestamp := uint64(time.Now().Unix()) - args := &BuildPayloadArgs{ - Parent: b.chain.CurrentBlock().Hash(), - Timestamp: timestamp, - Random: common.Hash{}, - FeeRecipient: recipient, +// newPayloadArgs returns a BuildPaylooadArgs with the given parentHash and eip-1559 params, +// testTimestamp for Timestamp, and testRecipient for recipient. NoTxPool is set to true. +func newPayloadArgs(parentHash common.Hash, params1559 []byte) *BuildPayloadArgs { + return &BuildPayloadArgs{ + Parent: parentHash, + Timestamp: testTimestamp, + Random: common.Hash{}, + FeeRecipient: testRecipient, + NoTxPool: true, + EIP1559Params: params1559, } +} + +func testBuildPayload(t *testing.T, noTxPool, interrupt bool, params1559 []byte) { + t.Parallel() + db := rawdb.NewMemoryDatabase() + + config := params.TestChainConfig + if len(params1559) != 0 { + config = holoceneConfig() + } + 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, false, false) + } + + args := newPayloadArgs(b.chain.CurrentBlock().Hash(), params1559) + args.NoTxPool = noTxPool + + // payload resolution now interrupts block building, so we have to + // wait for the payloading building process to build its first block payload, err := w.buildPayload(args, false) if err != nil { t.Fatalf("Failed to build payload %v", err) } verify := func(outer *engine.ExecutionPayloadEnvelope, txs int) { + t.Helper() + if outer == nil { + t.Fatal("ExecutionPayloadEnvelope is nil") + } payload := outer.ExecutionPayload if payload.ParentHash != b.chain.CurrentBlock().Hash() { t.Fatal("Unexpected parent hash") @@ -172,21 +265,51 @@ } if payload.Random != (common.Hash{}) { t.Fatal("Unexpected random value") } - if payload.Timestamp != timestamp { + if payload.Timestamp != testTimestamp { t.Fatal("Unexpected timestamp") } - if payload.FeeRecipient != recipient { + if payload.FeeRecipient != testRecipient { t.Fatal("Unexpected fee recipient") } - if len(payload.Transactions) != txs { - t.Fatal("Unexpected transaction set") + if !interrupt && len(payload.Transactions) != txs { + t.Fatalf("Unexpect transaction set: got %d, expected %d", len(payload.Transactions), txs) + } else if interrupt && len(payload.Transactions) >= txs { + t.Fatalf("Unexpect transaction set: got %d, expected less than %d", len(payload.Transactions), txs) } } - empty := payload.ResolveEmpty() - verify(empty, 0)   - full := payload.ResolveFull() - verify(full, len(pendingTxs)) + // make sure the 1559 params we've specied (if any) ends up in both the full and empty block headers + var expected []byte + if len(params1559) != 0 { + expected = []byte{0} + d, _ := eip1559.DecodeHolocene1559Params(params1559) + if d == 0 { + expected = append(expected, eip1559.EncodeHolocene1559Params(250, 6)...) // canyon defaults + } else { + expected = append(expected, params1559...) + } + } + if payload.full != nil && !bytes.Equal(payload.full.Header().Extra, expected) { + t.Fatalf("ExtraData doesn't match. want: %x, got %x", expected, payload.full.Header().Extra) + } + if payload.empty != nil && !bytes.Equal(payload.empty.Header().Extra, expected) { + t.Fatalf("ExtraData doesn't match on empty block. want: %x, got %x", expected, payload.empty.Header().Extra) + } + + if noTxPool { + // we only build the empty block when ignoring the tx pool + empty := payload.ResolveEmpty() + verify(empty, 0) + full := payload.ResolveFull() + verify(full, 0) + } else if interrupt { + full := payload.ResolveFull() + verify(full, len(pendingTxs)+numInterruptTxs) + } else { // tx-pool and no interrupt + payload.WaitFull() + full := payload.ResolveFull() + verify(full, len(pendingTxs)) + }   // Ensure resolve can be called multiple times and the // result should be unchanged @@ -195,6 +318,88 @@ dataTwo := payload.Resolve() if !reflect.DeepEqual(dataOne, dataTwo) { t.Fatal("Unexpected payload data") } +} + +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() + config := holoceneConfig() + if len(params1559) != 0 { + // deactivate holocene and make sure non-empty params get rejected + config.HoloceneTime = nil + } + w, b := newTestWorker(t, config, ethash.NewFaker(), db, 0) + + args := newPayloadArgs(b.chain.CurrentBlock().Hash(), params1559) + payload, err := w.buildPayload(args, false) + if err == nil && (payload == nil || payload.err == nil) { + t.Fatalf("expected error, got none") + } +} + +func TestBuildPayloadInvalidHoloceneParams(t *testing.T) { + t.Parallel() + db := rawdb.NewMemoryDatabase() + config := holoceneConfig() + w, b := newTestWorker(t, config, ethash.NewFaker(), db, 0) + + // 0 denominators shouldn't be allowed + badParams := eip1559.EncodeHolocene1559Params(0, 6) + + args := newPayloadArgs(b.chain.CurrentBlock().Hash(), badParams) + payload, err := w.buildPayload(args, false) + if err == nil && (payload == nil || payload.err == nil) { + t.Fatalf("expected error, got none") + } +} + +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++ { + // 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 + uint64(len(randomBytes))*16, + GasPrice: big.NewInt(params.InitialBaseFee), + Data: randomBytes, + }) + txs = append(txs, tx) + } + return txs }   func TestPayloadId(t *testing.T) {
diff --git go-ethereum/miner/worker.go op-geth/miner/worker.go index 930c3e8f5b1bfdcfb3ed82356e29f29a03c4ac02..c3bd100897b2766e2fe1bd8bb30b565423089182 100644 --- go-ethereum/miner/worker.go +++ op-geth/miner/worker.go @@ -17,6 +17,7 @@ package miner   import ( + "context" "errors" "fmt" "math/big" @@ -24,6 +25,7 @@ "sync/atomic" "time"   "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/consensus/misc/eip1559" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" @@ -31,16 +33,33 @@ "github.com/ethereum/go-ethereum/core/state" "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" + "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" )   +const ( + // minRecommitInterruptInterval is the minimum time interval used to interrupt filling a + // sealing block with pending transactions from the mempool + minRecommitInterruptInterval = 2 * time.Second +) + var ( + errTxConditionalInvalid = errors.New("transaction conditional failed") + errBlockInterruptedByNewHead = errors.New("new head arrived while building block") errBlockInterruptedByRecommit = errors.New("recommit interrupt while building block") errBlockInterruptedByTimeout = errors.New("timeout while building block") + errBlockInterruptedByResolve = errors.New("payload resolution while building block") + + 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 @@ -59,6 +78,9 @@ sidecars []*types.BlobTxSidecar blobs int   witness *stateless.Witness + + noTxs bool // true if we are reproducing a block, and do not have to check interop txs + rpcCtx context.Context // context to control block-building RPC work. No RPC allowed if nil. }   const ( @@ -66,6 +88,7 @@ commitInterruptNone int32 = iota commitInterruptNewHead commitInterruptResubmit commitInterruptTimeout + commitInterruptResolve )   // newPayloadResult is the result of payload generation. @@ -89,6 +112,14 @@ random common.Hash // The randomness generated by beacon chain, empty before the merge withdrawals types.Withdrawals // List of withdrawals to include in block (shanghai field) beaconRoot *common.Hash // The beacon root (cancun field). noTxs bool // Flag whether an empty block without any transaction is expected + + txs types.Transactions // Deposit transactions to include at the start of the block + gasLimit *uint64 // Optional gas limit override + 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. @@ -97,17 +128,51 @@ work, err := miner.prepareWork(params, witness) if err != nil { return &newPayloadResult{err: err} } + if work.gasPool == nil { + gasLimit := work.header.GasLimit + + // If we're building blocks with mempool transactions, we need to ensure that the + // gas limit is not higher than the effective gas limit. We must still accept any + // explicitly selected transactions with gas usage up to the block header's limit. + if !params.noTxs { + effectiveGasLimit := miner.config.EffectiveGasCeil + if effectiveGasLimit != 0 && effectiveGasLimit < gasLimit { + gasLimit = effectiveGasLimit + } + } + work.gasPool = new(core.GasPool).AddGas(gasLimit) + } + + misc.EnsureCreate2Deployer(miner.chainConfig, work.header.Time, work.state) + + for _, tx := range params.txs { + from, _ := types.Sender(work.signer, tx) + work.state.SetTxContext(tx.Hash(), work.tcount) + err = miner.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 !params.noTxs { - interrupt := new(atomic.Int32) - timer := time.AfterFunc(miner.config.Recommit, func() { + // use shared interrupt if present + interrupt := params.interrupt + if interrupt == nil { + interrupt = new(atomic.Int32) + } + timer := time.AfterFunc(max(minRecommitInterruptInterval, miner.config.Recommit), func() { interrupt.Store(commitInterruptTimeout) }) - defer timer.Stop()   err := miner.fillTransactions(interrupt, work) + timer.Stop() // don't need timeout interruption any more if errors.Is(err, errBlockInterruptedByTimeout) { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(miner.config.Recommit)) + } else if errors.Is(err, errBlockInterruptedByResolve) { + log.Info("Block building got interrupted by payload resolution") } + } + if intr := params.interrupt; intr != nil && params.isUpdate && intr.Load() != commitInterruptNone { + return &newPayloadResult{err: errInterruptedUpdate} }   body := types.Body{Transactions: work.txs, Withdrawals: params.withdrawals} @@ -171,7 +236,8 @@ Time: timestamp, Coinbase: genParams.coinbase, } // Set the extra field. - if len(miner.config.ExtraData) != 0 { + if len(miner.config.ExtraData) != 0 && miner.chainConfig.Optimism == nil { + // Optimism chains have their own ExtraData handling rules header.Extra = miner.config.ExtraData } // Set the randomness field from the beacon chain if it's available. @@ -180,12 +246,33 @@ header.MixDigest = genParams.random } // Set baseFee and GasLimit if we are on an EIP-1559 chain if miner.chainConfig.IsLondon(header.Number) { - header.BaseFee = eip1559.CalcBaseFee(miner.chainConfig, parent) + header.BaseFee = eip1559.CalcBaseFee(miner.chainConfig, parent, header.Time) if !miner.chainConfig.IsLondon(parent.Number) { parentGasLimit := parent.GasLimit * miner.chainConfig.ElasticityMultiplier() header.GasLimit = core.CalcGasLimit(parentGasLimit, miner.config.GasCeil) } } + if genParams.gasLimit != nil { // override gas limit if specified + header.GasLimit = *genParams.gasLimit + } else if miner.chain.Config().Optimism != nil && miner.config.GasCeil != 0 { + // configure the gas limit of pending blocks with the miner gas limit config when using optimism + header.GasLimit = miner.config.GasCeil + } + if miner.chainConfig.IsHolocene(header.Time) { + if err := eip1559.ValidateHolocene1559Params(genParams.eip1559Params); err != nil { + return nil, err + } + // If this is a holocene block and the params are 0, we must convert them to their previous + // constants in the header. + d, e := eip1559.DecodeHolocene1559Params(genParams.eip1559Params) + if d == 0 { + d = miner.chainConfig.BaseFeeChangeDenominator(header.Time) + e = miner.chainConfig.ElasticityMultiplier() + } + header.Extra = eip1559.EncodeHoloceneExtraData(d, e) + } else if genParams.eip1559Params != nil { + return nil, errors.New("got eip1559 params, expected none") + } // Run the consensus preparation with the default or customized consensus engine. // Note that the `header.Time` may be changed. if err := miner.engine.Prepare(miner.chain, header); err != nil { @@ -208,18 +295,19 @@ } // 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) + context := core.NewEVMBlockContext(header, miner.chain, nil, miner.chainConfig, env.state) vmenv := vm.NewEVM(context, vm.TxContext{}, env.state, miner.chainConfig, vm.Config{}) core.ProcessBeaconBlockRoot(*header.ParentBeaconRoot, vmenv, env.state) } if miner.chainConfig.IsPrague(header.Number, header.Time) { - context := core.NewEVMBlockContext(header, miner.chain, nil) + context := core.NewEVMBlockContext(header, miner.chain, nil, miner.chainConfig, env.state) vmenv := vm.NewEVM(context, vm.TxContext{}, env.state, miner.chainConfig, vm.Config{}) core.ProcessParentBlockHash(header.ParentHash, vmenv, env.state) } @@ -227,12 +315,25 @@ return env, nil }   // 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 { return nil, err } + if miner.chainConfig.Optimism != nil { // Allow the miner to reorg its own chain arbitrarily deep + if historicalBackend, ok := miner.backend.(BackendWithHistoricalState); ok { + var release tracers.StateReleaseFunc + parentBlock := miner.backend.BlockChain().GetBlockByHash(parent.Hash()) + state, release, err = historicalBackend.StateAtBlock(context.Background(), parentBlock, ^uint64(0), nil, false, false) + if err != nil { + return nil, err + } + state = state.Copy() + release() + } + } + if witness { bundle, err := stateless.NewWitness(header, miner.chain) if err != nil { @@ -247,6 +348,7 @@ state: state, coinbase: coinbase, header: header, witness: state.Witness(), + rpcCtx: rpcCtx, }, nil }   @@ -254,6 +356,20 @@ func (miner *Miner) commitTransaction(env *environment, tx *types.Transaction) error { if tx.Type() == types.BlobTxType { return miner.commitBlobTransaction(env, tx) } + + // If a conditional is set, check prior to applying + if conditional := tx.Conditional(); conditional != nil { + txConditionalMinedTimer.UpdateSince(tx.Time()) + + // check the conditional + if err := env.header.CheckTransactionConditional(conditional); err != nil { + return fmt.Errorf("failed header check: %s: %w", err, errTxConditionalInvalid) + } + if err := env.state.CheckTransactionConditional(conditional); err != nil { + return fmt.Errorf("failed state check: %s: %w", err, errTxConditionalInvalid) + } + } + receipt, err := miner.applyTransaction(env, tx) if err != nil { return err @@ -289,13 +405,37 @@ env.tcount++ 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) @@ -303,11 +443,48 @@ } 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 { @@ -361,6 +538,22 @@ log.Trace("Not enough blob gas left for transaction", "hash", ltx.Hash, "left", left, "needed", ltx.BlobGas) 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() + // If the number of remaining bytes is too few to hold even the minimum possible transaction size, + // then we can stop early. + daBytesRemaining := new(big.Int).Sub(miner.config.MaxDABlockSize, daBytesAfter) + if daBytesRemaining.Cmp(types.MinTransactionSize) < 0 { + break + } + continue + } + } // Transaction seems to fit, pull it up from the pool tx := ltx.Resolve() if tx == nil { @@ -389,8 +582,26 @@ // New head notification data race between the transaction pool and miner, shift log.Trace("Skipping transaction with low nonce", "hash", ltx.Hash, "sender", from, "nonce", tx.Nonce()) txs.Shift()   + case errors.Is(err, errTxConditionalInvalid): + // err contains contextual info on the failed conditional. + txConditionalRejectedCounter.Inc(1) + + // mark as rejected so that it can be ejected from the mempool + tx.SetRejected() + 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: @@ -413,7 +624,8 @@ miner.confMu.RUnlock()   // 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) @@ -481,7 +693,41 @@ case commitInterruptResubmit: return errBlockInterruptedByRecommit case commitInterruptTimeout: return errBlockInterruptedByTimeout + case commitInterruptResolve: + return errBlockInterruptedByResolve default: panic(fmt.Errorf("undefined signal %d", signal)) } } + +// validateParams validates the given parameters. +// It currently checks that the parent block is known and that the timestamp is valid, +// i.e., after the parent block's timestamp. +// It returns an upper bound of the payload building duration as computed +// by the difference in block timestamps between the parent and genParams. +func (miner *Miner) validateParams(genParams *generateParams) (time.Duration, error) { + miner.confMu.RLock() + defer miner.confMu.RUnlock() + + // Find the parent block for sealing task + parent := miner.chain.CurrentBlock() + if genParams.parentHash != (common.Hash{}) { + block := miner.chain.GetBlockByHash(genParams.parentHash) + if block == nil { + return 0, fmt.Errorf("missing parent %v", genParams.parentHash) + } + parent = block.Header() + } + + // Sanity check the timestamp correctness + blockTime := int64(genParams.timestamp) - int64(parent.Time) + if blockTime <= 0 && genParams.forceTime { + return 0, fmt.Errorf("invalid timestamp, parent %d given %d", parent.Time, genParams.timestamp) + } + + // minimum payload build time of 2s + if blockTime < 2 { + blockTime = 2 + } + return time.Duration(blockTime) * time.Second, nil +}

Transaction queueing and inclusion needs to account for the L1 cost component.

diff --git go-ethereum/core/txpool/blobpool/blobpool.go op-geth/core/txpool/blobpool/blobpool.go index 82df09a4bfc198789c0a8118232494bfac864420..19908d9e8fb61078ff0242728bbc89cac0c2371f 100644 --- go-ethereum/core/txpool/blobpool/blobpool.go +++ op-geth/core/txpool/blobpool/blobpool.go @@ -401,7 +401,7 @@ for addr := range p.index { p.recheck(addr, nil) } var ( - basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), p.head)) + basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), p.head, p.head.Time+1)) blobfee = uint256.NewInt(params.BlobTxMinBlobGasprice) ) if p.head.ExcessBlobGas != nil { @@ -822,7 +822,7 @@ p.limbo.finalize(p.chain.CurrentFinalBlock()) } // Reset the price heap for the new set of basefee/blobfee pairs var ( - basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), newHead)) + basefee = uint256.MustFromBig(eip1559.CalcBaseFee(p.chain.Config(), newHead, newHead.Time+1)) blobfee = uint256.MustFromBig(big.NewInt(params.BlobTxMinBlobGasprice)) ) if newHead.ExcessBlobGas != nil {
diff --git go-ethereum/core/txpool/blobpool/blobpool_test.go op-geth/core/txpool/blobpool/blobpool_test.go index 9bbc763e777f9bfecba83a5a98138bc7f5c0912e..ecef50294b59183f11928ed955ee13093221214a 100644 --- go-ethereum/core/txpool/blobpool/blobpool_test.go +++ op-geth/core/txpool/blobpool/blobpool_test.go @@ -89,7 +89,7 @@ Number: blockNumber, GasLimit: gasLimit, GasUsed: 0, BaseFee: mid, - }).Cmp(bc.basefee.ToBig()) > 0 { + }, blockTime).Cmp(bc.basefee.ToBig()) > 0 { hi = mid } else { lo = mid
diff --git go-ethereum/core/txpool/ingress_filters.go op-geth/core/txpool/ingress_filters.go new file mode 100644 index 0000000000000000000000000000000000000000..317585f7a84ff2d947ebcb85020bb84fcef187c3 --- /dev/null +++ op-geth/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 go-ethereum/core/txpool/ingress_filters_test.go op-geth/core/txpool/ingress_filters_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b2fcfd9018768560d3469c4421f4ccd4298a36a1 --- /dev/null +++ op-geth/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 go-ethereum/core/txpool/legacypool/legacypool.go op-geth/core/txpool/legacypool/legacypool.go index 5506ecc31fc37956932c63f863caad96ec7d6533..2579104e5a4e1aae06f9f3f9da0ddd50600df506 100644 --- go-ethereum/core/txpool/legacypool/legacypool.go +++ op-geth/core/txpool/legacypool/legacypool.go @@ -127,6 +127,10 @@ NoLocals bool // Whether local transaction handling should be disabled Journal string // Journal of local transactions to survive node restarts Rejournal time.Duration // Time interval to regenerate the local transaction journal   + // JournalRemote controls whether journaling includes remote transactions or not. + // When true, all transactions loaded from the journal are treated as remote. + JournalRemote bool + PriceLimit uint64 // Minimum gas price to enforce for acceptance into the pool PriceBump uint64 // Minimum price bump percentage to replace an already existing transaction (nonce)   @@ -136,6 +140,8 @@ AccountQueue uint64 // Maximum number of non-executable transaction slots permitted per account GlobalQueue uint64 // Maximum number of non-executable transaction slots for all accounts   Lifetime time.Duration // Maximum amount of time non-executable transaction are queued + + EffectiveGasCeil uint64 // if non-zero, a gas ceiling to enforce independent of the header's gaslimit value }   // DefaultConfig contains the default configurations for the transaction pool. @@ -232,6 +238,8 @@ wg sync.WaitGroup // tracks loop, scheduleReorgLoop initDoneCh chan struct{} // is closed once the pool is initialized (for tests)   changesSinceReorg int // A counter for how many drops we've performed in-between reorg. + + l1CostFn txpool.L1CostFunc // To apply L1 costs as rollup, optional field, may be nil. }   type txpoolResetRequest struct { @@ -268,7 +276,7 @@ pool.locals.add(addr) } pool.priced = newPricedList(pool.all)   - if !config.NoLocals && config.Journal != "" { + if (!config.NoLocals || config.JournalRemote) && config.Journal != "" { pool.journal = newTxJournal(config.Journal) } return pool @@ -317,10 +325,14 @@ go pool.scheduleReorgLoop()   // If local transactions and journaling is enabled, load from disk if pool.journal != nil { - if err := pool.journal.load(pool.addLocals); err != nil { + add := pool.addLocals + if pool.config.JournalRemote { + add = pool.addRemotesSync // Use sync version to match pool.AddLocals + } + if err := pool.journal.load(add); err != nil { log.Warn("Failed to load transaction journal", "err", err) } - if err := pool.journal.rotate(pool.local()); err != nil { + if err := pool.journal.rotate(pool.toJournal()); err != nil { log.Warn("Failed to rotate transaction journal", "err", err) } } @@ -390,7 +402,7 @@ // Handle local transaction journal rotation case <-journal.C: if pool.journal != nil { pool.mu.Lock() - if err := pool.journal.rotate(pool.local()); err != nil { + if err := pool.journal.rotate(pool.toJournal()); err != nil { log.Warn("Failed to rotate local tx journal", "err", err) } pool.mu.Unlock() @@ -551,6 +563,17 @@ // If the miner requests tip enforcement, cap the lists now if minTipBig != nil && !pool.locals.contains(addr) { for i, tx := range txs { if tx.EffectiveGasTipIntCmp(minTipBig, baseFeeBig) < 0 { + txs = txs[:i] + break + } + } + } + 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 } @@ -559,6 +582,7 @@ } 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(), @@ -568,6 +592,7 @@ GasFeeCap: uint256.MustFromBig(txs[i].GasFeeCap()), GasTipCap: uint256.MustFromBig(txs[i].GasTipCap()), Gas: txs[i].Gas(), BlobGas: txs[i].BlobGas(), + DABytes: daBytes, } } pending[addr] = lazies @@ -584,6 +609,23 @@ return pool.locals.flatten() }   +// toJournal retrieves all transactions that should be included in the journal, +// grouped by origin account and sorted by nonce. +// The returned transaction set is a copy and can be freely modified by calling code. +func (pool *LegacyPool) toJournal() map[common.Address]types.Transactions { + if !pool.config.JournalRemote { + return pool.local() + } + txs := make(map[common.Address]types.Transactions) + for addr, pending := range pool.pending { + txs[addr] = append(txs[addr], pending.Flatten()...) + } + for addr, queued := range pool.queue { + txs[addr] = append(txs[addr], queued.Flatten()...) + } + return txs +} + // local retrieves all currently known local transactions, grouped by origin // account and sorted by nonce. The returned transaction set is a copy and can be // freely modified by calling code. @@ -611,8 +653,9 @@ Accept: 0 | 1<<types.LegacyTxType | 1<<types.AccessListTxType | 1<<types.DynamicFeeTxType, - MaxSize: txMaxSize, - MinTip: pool.gasTip.Load().ToBig(), + MaxSize: txMaxSize, + MinTip: pool.gasTip.Load().ToBig(), + EffectiveGasCeil: pool.config.EffectiveGasCeil, } if local { opts.MinTip = new(big.Int) @@ -649,11 +692,18 @@ }, ExistingCost: func(addr common.Address, nonce uint64) *big.Int { if list := pool.pending[addr]; list != nil { if tx := list.txs.Get(nonce); tx != nil { - return tx.Cost() + cost := tx.Cost() + if pool.l1CostFn != nil { + if l1Cost := pool.l1CostFn(tx.RollupCostData()); l1Cost != nil { // add rollup cost + cost = cost.Add(cost, l1Cost) + } + } + return cost } } return nil }, + L1CostFn: pool.l1CostFn, } if err := txpool.ValidateTransactionWithState(tx, pool.signer, opts); err != nil { return err @@ -776,7 +826,7 @@ // Try to replace an existing transaction in the pending pool if list := pool.pending[from]; list != nil && list.Contains(tx.Nonce()) { // Nonce already pending, check if required price bump is met - inserted, old := list.Add(tx, pool.config.PriceBump) + inserted, old := list.Add(tx, pool.config.PriceBump, pool.l1CostFn) if !inserted { pendingDiscardMeter.Mark(1) return false, txpool.ErrReplaceUnderpriced @@ -850,7 +900,7 @@ from, _ := types.Sender(pool.signer, tx) // already validated if pool.queue[from] == nil { pool.queue[from] = newList(false) } - inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump) + inserted, old := pool.queue[from].Add(tx, pool.config.PriceBump, pool.l1CostFn) if !inserted { // An older transaction was better, discard this queuedDiscardMeter.Mark(1) @@ -885,7 +935,7 @@ // journalTx adds the specified transaction to the local disk journal if it is // deemed to have been sent from a local account. func (pool *LegacyPool) journalTx(from common.Address, tx *types.Transaction) { // Only journal if it's enabled and the transaction is local - if pool.journal == nil || !pool.locals.contains(from) { + if pool.journal == nil || (!pool.config.JournalRemote && !pool.locals.contains(from)) { return } if err := pool.journal.insert(tx); err != nil { @@ -904,7 +954,7 @@ pool.pending[addr] = newList(true) } list := pool.pending[addr]   - inserted, old := list.Add(tx, pool.config.PriceBump) + inserted, old := list.Add(tx, pool.config.PriceBump, pool.l1CostFn) if !inserted { // An older transaction was better, discard this pool.all.Remove(hash) @@ -1299,7 +1349,7 @@ if reset != nil { pool.demoteUnexecutables() if reset.newHead != nil { if pool.chainconfig.IsLondon(new(big.Int).Add(reset.newHead.Number, big.NewInt(1))) { - pendingBaseFee := eip1559.CalcBaseFee(pool.chainconfig, reset.newHead) + pendingBaseFee := eip1559.CalcBaseFee(pool.chainconfig, reset.newHead, reset.newHead.Time+1) pool.priced.SetBaseFee(pendingBaseFee) } else { pool.priced.Reheap() @@ -1431,12 +1481,36 @@ pool.currentHead.Store(newHead) pool.currentState = statedb pool.pendingNonces = newNoncer(statedb)   + if costFn := types.NewL1CostFunc(pool.chainconfig, statedb); costFn != nil { + pool.l1CostFn = func(rollupCostData types.RollupCostData) *big.Int { + return costFn(rollupCostData, newHead.Time) + } + } + // Inject any transactions discarded due to reorgs log.Debug("Reinjecting stale transactions", "count", len(reinject)) core.SenderCacher.Recover(pool.signer, reinject) pool.addTxsLocked(reinject, false) }   +// reduceBalanceByL1Cost returns the given balance, reduced by the L1Cost of the first transaction in list if applicable +// Other txs will get filtered out necessary. +func (pool *LegacyPool) reduceBalanceByL1Cost(list *list, balance *uint256.Int) *uint256.Int { + if !list.Empty() && pool.l1CostFn != nil { + el := list.txs.FirstElement() + if l1Cost := pool.l1CostFn(el.RollupCostData()); l1Cost != nil { + l1Cost256 := uint256.MustFromBig(l1Cost) + if l1Cost256.Cmp(balance) >= 0 { + // Avoid underflow + balance = uint256.NewInt(0) + } else { + balance = new(uint256.Int).Sub(balance, l1Cost256) + } + } + } + return balance +} + // promoteExecutables moves transactions that have become processable from the // future queue to the set of pending transactions. During this process, all // invalidated transactions (low nonce, low balance) are deleted. @@ -1445,7 +1519,7 @@ // Track the promoted transactions to broadcast them at once var promoted []*types.Transaction   // Iterate over all accounts and promote any executable transactions - gasLimit := pool.currentHead.Load().GasLimit + gasLimit := txpool.EffectiveGasLimit(pool.chainconfig, pool.currentHead.Load().GasLimit, pool.config.EffectiveGasCeil) for _, addr := range accounts { list := pool.queue[addr] if list == nil { @@ -1458,8 +1532,10 @@ hash := tx.Hash() pool.all.Remove(hash) } log.Trace("Removed old queued transactions", "count", len(forwards)) + balance := pool.currentState.GetBalance(addr) + balance = pool.reduceBalanceByL1Cost(list, balance) // Drop all transactions that are too costly (low balance or out of gas) - drops, _ := list.Filter(pool.currentState.GetBalance(addr), gasLimit) + drops, _ := list.Filter(balance, gasLimit) for _, tx := range drops { hash := tx.Hash() pool.all.Remove(hash) @@ -1648,7 +1724,7 @@ // is always explicitly triggered by SetBaseFee and it would be unnecessary and wasteful // to trigger a re-heap is this function func (pool *LegacyPool) demoteUnexecutables() { // Iterate over all accounts and demote any non-executable transactions - gasLimit := pool.currentHead.Load().GasLimit + gasLimit := txpool.EffectiveGasLimit(pool.chainconfig, pool.currentHead.Load().GasLimit, pool.config.EffectiveGasCeil) for addr, list := range pool.pending { nonce := pool.currentState.GetNonce(addr)   @@ -1659,8 +1735,10 @@ hash := tx.Hash() pool.all.Remove(hash) log.Trace("Removed old pending transaction", "hash", hash) } + balance := pool.currentState.GetBalance(addr) + balance = pool.reduceBalanceByL1Cost(list, balance) // Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later - drops, invalids := list.Filter(pool.currentState.GetBalance(addr), gasLimit) + drops, invalids := list.Filter(balance, gasLimit) for _, tx := range drops { hash := tx.Hash() log.Trace("Removed unpayable pending transaction", "hash", hash) @@ -1668,6 +1746,16 @@ pool.all.Remove(hash) } pendingNofundsMeter.Mark(int64(len(drops)))   + // Drop all transactions that were rejected by the miner + rejectedDrops := list.txs.Filter(func(tx *types.Transaction) bool { + return tx.Rejected() + }) + for _, tx := range rejectedDrops { + hash := tx.Hash() + pool.all.Remove(hash) + log.Trace("Removed rejected transaction", "hash", hash) + } + for _, tx := range invalids { hash := tx.Hash() log.Trace("Demoting pending transaction", "hash", hash) @@ -1675,9 +1763,9 @@ // Internal shuffle shouldn't touch the lookup set. pool.enqueueTx(hash, tx, false, false) } - pendingGauge.Dec(int64(len(olds) + len(drops) + len(invalids))) + pendingGauge.Dec(int64(len(olds) + len(drops) + len(invalids) + len(rejectedDrops))) if pool.locals.contains(addr) { - localGauge.Dec(int64(len(olds) + len(drops) + len(invalids))) + localGauge.Dec(int64(len(olds) + len(drops) + len(invalids) + len(rejectedDrops))) } // If there's a gap in front, alert (should never happen) and postpone all transactions if list.Len() > 0 && list.txs.Get(nonce) == nil {
diff --git go-ethereum/core/txpool/legacypool/legacypool_test.go op-geth/core/txpool/legacypool/legacypool_test.go index 39673d176db96a9a8e2a0486debb5106ecd060a2..4a5c07b60c442c6357b21b0b3462234e45527400 100644 --- go-ethereum/core/txpool/legacypool/legacypool_test.go +++ op-geth/core/txpool/legacypool/legacypool_test.go @@ -86,7 +86,7 @@ } }   func (bc *testBlockChain) GetBlock(hash common.Hash, number uint64) *types.Block { - return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil)) + return types.NewBlock(bc.CurrentBlock(), nil, nil, trie.NewStackTrie(nil), bc.config) }   func (bc *testBlockChain) StateAt(common.Hash) (*state.StateDB, error) { @@ -686,6 +686,39 @@ t.Errorf("over-gased queued transaction present: %v", tx11) } if pool.all.Count() != 2 { t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 2) + } +} + +// Tests that transactions marked as reject (by the miner in practice) +// are removed from the pool +func TestRejectedDropping(t *testing.T) { + t.Parallel() + + pool, key := setupPool() + defer pool.Close() + + account := crypto.PubkeyToAddress(key.PublicKey) + testAddBalance(pool, account, big.NewInt(1000)) + + // create some txs. tx0 has a conditional + tx0, tx1 := transaction(0, 100, key), transaction(1, 200, key) + + pool.all.Add(tx0, false) + pool.all.Add(tx1, false) + pool.promoteTx(account, tx0.Hash(), tx0) + pool.promoteTx(account, tx1.Hash(), tx1) + + // pool state is unchanged + <-pool.requestReset(nil, nil) + if pool.all.Count() != 2 { + t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 2) + } + + // tx0 conditional is marked as rejected and should be removed + tx0.SetRejected() + <-pool.requestReset(nil, nil) + if pool.all.Count() != 1 { + t.Errorf("total transaction mismatch: have %d, want %d", pool.all.Count(), 1) } }   @@ -2343,10 +2376,13 @@ }   // Tests that local transactions are journaled to disk, but remote transactions // get discarded between restarts. -func TestJournaling(t *testing.T) { testJournaling(t, false) } -func TestJournalingNoLocals(t *testing.T) { testJournaling(t, true) } +func TestJournaling(t *testing.T) { testJournaling(t, false, false) } +func TestJournalingNoLocals(t *testing.T) { testJournaling(t, true, false) } + +func TestJournalingRemotes(t *testing.T) { testJournaling(t, false, true) } +func TestJournalingRemotesNoLocals(t *testing.T) { testJournaling(t, true, true) }   -func testJournaling(t *testing.T, nolocals bool) { +func testJournaling(t *testing.T, nolocals bool, journalRemotes bool) { t.Parallel()   // Create a temporary file for the journal @@ -2367,6 +2403,7 @@ blockchain := newTestBlockChain(params.TestChainConfig, 1000000, statedb, new(event.Feed))   config := testTxPoolConfig config.NoLocals = nolocals + config.JournalRemote = journalRemotes config.Journal = journal config.Rejournal = time.Second   @@ -2415,10 +2452,14 @@ pending, queued = pool.Stats() if queued != 0 { t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 0) } - if nolocals { + if nolocals && !journalRemotes { if pending != 0 { t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 0) } + } else if journalRemotes { + if pending != 3 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 3) + } } else { if pending != 2 { t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 2) @@ -2439,10 +2480,16 @@ pool = New(config, blockchain) pool.Init(config.PriceLimit, blockchain.CurrentBlock(), makeAddressReserver())   pending, queued = pool.Stats() - if pending != 0 { - t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 0) + if journalRemotes { + if pending != 1 { // Remove the 2 replaced local transactions, but preserve the remote + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 1) + } + } else { + if pending != 0 { + t.Fatalf("pending transactions mismatched: have %d, want %d", pending, 0) + } } - if nolocals { + if nolocals && !journalRemotes { if queued != 0 { t.Fatalf("queued transactions mismatched: have %d, want %d", queued, 0) }
diff --git go-ethereum/core/txpool/legacypool/list.go op-geth/core/txpool/legacypool/list.go index b749db44d45e74b389f671e69dd5c9e3df3a6123..24b18d2273252af4115825c94aaf6f9d8904dd1f 100644 --- go-ethereum/core/txpool/legacypool/list.go +++ op-geth/core/txpool/legacypool/list.go @@ -27,6 +27,7 @@ "sync/atomic" "time"   "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/holiman/uint256" ) @@ -265,6 +266,15 @@ cache := m.flatten() return cache[len(cache)-1] }   +// FirstElement returns the first element from the heap (guaranteed to be lowest), thus, the +// transaction with the lowest nonce. Returns nil if there are no elements. +func (m *sortedMap) FirstElement() *types.Transaction { + if m.Len() == 0 { + return nil + } + return m.Get((*m.index)[0]) +} + // list is a "list" of transactions belonging to an account, sorted by account // nonce. The same type can be used both for storing contiguous transactions for // the executable/pending queue; and for storing gapped transactions for the non- @@ -300,7 +310,7 @@ // transaction was accepted, and if yes, any previous transaction it replaced. // // If the new transaction is accepted into the list, the lists' cost and gas // thresholds are also potentially updated. -func (l *list) Add(tx *types.Transaction, priceBump uint64) (bool, *types.Transaction) { +func (l *list) Add(tx *types.Transaction, priceBump uint64, l1CostFn txpool.L1CostFunc) (bool, *types.Transaction) { // If there's an older better transaction, abort old := l.txs.Get(tx.Nonce()) if old != nil { @@ -332,7 +342,11 @@ if overflow { return false, nil } l.totalcost.Add(l.totalcost, cost) - + if l1CostFn != nil { + if l1Cost := l1CostFn(tx.RollupCostData()); l1Cost != nil { // add rollup cost + l.totalcost.Add(l.totalcost, cost) + } + } // Otherwise overwrite the old transaction with the current one l.txs.Put(tx) if l.costcap.Cmp(cost) < 0 {
diff --git go-ethereum/core/txpool/legacypool/list_test.go op-geth/core/txpool/legacypool/list_test.go index 8587c66f7d22a07f5776d7e25f90499eddc239cc..9f341a7f2026a9ef79944f542fbde08d558c21e2 100644 --- go-ethereum/core/txpool/legacypool/list_test.go +++ op-geth/core/txpool/legacypool/list_test.go @@ -40,7 +40,7 @@ } // Insert the transactions in a random order list := newList(true) for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump) + list.Add(txs[v], DefaultConfig.PriceBump, nil) } // Verify internal state if len(list.txs.items) != len(txs) { @@ -64,7 +64,7 @@ gasprice, _ := new(big.Int).SetString("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 0) gaslimit := uint64(i) tx, _ := types.SignTx(types.NewTransaction(uint64(i), common.Address{}, value, gaslimit, gasprice, nil), types.HomesteadSigner{}, key) t.Logf("cost: %x bitlen: %d\n", tx.Cost(), tx.Cost().BitLen()) - list.Add(tx, DefaultConfig.PriceBump) + list.Add(tx, DefaultConfig.PriceBump, nil) } }   @@ -82,7 +82,7 @@ b.ResetTimer() for i := 0; i < b.N; i++ { list := newList(true) for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump) + list.Add(txs[v], DefaultConfig.PriceBump, nil) list.Filter(priceLimit, DefaultConfig.PriceBump) } } @@ -102,7 +102,7 @@ for i := 0; i < b.N; i++ { list := newList(true) // Insert the transactions in a random order for _, v := range rand.Perm(len(txs)) { - list.Add(txs[v], DefaultConfig.PriceBump) + list.Add(txs[v], DefaultConfig.PriceBump, nil) } b.StartTimer() list.Cap(list.Len() - 1)
diff --git go-ethereum/core/txpool/subpool.go op-geth/core/txpool/subpool.go index 9881ed1b8f960f0a61b7666a5a22412fa0228779..4e2a0133f33fb592666404afbec7a9898125c7d5 100644 --- go-ethereum/core/txpool/subpool.go +++ op-geth/core/txpool/subpool.go @@ -41,6 +41,8 @@ GasTipCap *uint256.Int // Maximum miner tip per gas the transaction can pay   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 @@ BlobFee *uint256.Int // Minimum 4844 blobfee needed to include a blob transaction   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 go-ethereum/core/txpool/txpool.go op-geth/core/txpool/txpool.go index be7435247d92749027ccbee4e92f0d44153b4a4b..db4ee13ee4994d94b0937d2b97e4a02c1e2111d8 100644 --- go-ethereum/core/txpool/txpool.go +++ op-geth/core/txpool/txpool.go @@ -17,6 +17,7 @@ package txpool   import ( + "context" "errors" "fmt" "math/big" @@ -29,6 +30,8 @@ "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" ) + +type L1CostFunc func(dataGas types.RollupCostData) *big.Int   // TxStatus is the current status of a transaction as seen by the pool. type TxStatus uint @@ -75,22 +78,32 @@ quit chan chan error // Quit channel to tear down the head updater 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 { @@ -153,6 +166,8 @@ // Close terminates the transaction pool and all its subpools. 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) @@ -317,11 +332,23 @@ // We also need to track how the transactions were split across the subpools, // 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) { @@ -339,6 +366,11 @@ errsets[i] = p.subpools[i].Add(txsets[i], local, sync) } 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 go-ethereum/core/txpool/validation.go op-geth/core/txpool/validation.go index 7fd5f8bc79e656e92e9d6b2d373911d88606170b..17de989ad309ccd58a547d7538d9df410a6d4e31 100644 --- go-ethereum/core/txpool/validation.go +++ op-geth/core/txpool/validation.go @@ -31,12 +31,31 @@ "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" )   +// L1 Info Gas Overhead is the amount of gas the the L1 info deposit consumes. +// It is removed from the tx pool max gas to better indicate that L2 transactions +// are not able to consume all of the gas in a L2 block as the L1 info deposit is always present. +const l1InfoGasOverhead = uint64(70_000) + var ( // blobTxMinBlobGasPrice is the big.Int version of the configured protocol // parameter to avoid constructing a new big integer for every transaction. blobTxMinBlobGasPrice = big.NewInt(params.BlobTxMinBlobGasprice) )   +func EffectiveGasLimit(chainConfig *params.ChainConfig, gasLimit uint64, effectiveLimit uint64) uint64 { + if effectiveLimit != 0 && effectiveLimit < gasLimit { + gasLimit = effectiveLimit + } + if chainConfig.Optimism != nil { + if l1InfoGasOverhead < gasLimit { + gasLimit -= l1InfoGasOverhead + } else { + gasLimit = 0 + } + } + return gasLimit +} + // ValidationOptions define certain differences between transaction validation // across the different pools without having to duplicate those checks. type ValidationOptions struct { @@ -45,6 +64,8 @@ Accept uint8 // Bitmap of transaction types that should be accepted for the calling pool MaxSize uint64 // Maximum size of a transaction that the caller can meaningfully handle MinTip *big.Int // Minimum gas tip needed to allow a transaction into the caller pool + + EffectiveGasCeil uint64 // if non-zero, a gas ceiling to enforce independent of the header's gaslimit value }   // ValidateTransaction is a helper method to check whether a transaction is valid @@ -54,6 +75,15 @@ // // This check is public to allow different transaction pools to check the basic // rules without duplicating code and running the risk of missed updates. func ValidateTransaction(tx *types.Transaction, head *types.Header, signer types.Signer, opts *ValidationOptions) error { + // No unauthenticated deposits allowed in the transaction pool. + // This is for spam protection, not consensus, + // as the external engine-API user authenticates deposits. + if tx.Type() == types.DepositTxType { + return core.ErrTxTypeNotSupported + } + if opts.Config.IsOptimism() && tx.Type() == types.BlobTxType { + return core.ErrTxTypeNotSupported + } // Ensure transactions not implemented by the calling pool are rejected if opts.Accept&(1<<tx.Type()) == 0 { return fmt.Errorf("%w: tx type %v not supported by this pool", core.ErrTxTypeNotSupported, tx.Type()) @@ -83,7 +113,7 @@ if tx.Value().Sign() < 0 { return ErrNegativeValue } // Ensure the transaction doesn't exceed the current block limit gas - if head.GasLimit < tx.Gas() { + if EffectiveGasLimit(opts.Config, head.GasLimit, opts.EffectiveGasCeil) < tx.Gas() { return ErrGasLimit } // Sanity check for extremely large numbers (supported by RLP or RPC) @@ -192,6 +222,9 @@ // ExistingCost is a mandatory callback to retrieve an already pooled // transaction's cost with the given nonce to check for overdrafts. ExistingCost func(addr common.Address, nonce uint64) *big.Int + + // L1CostFn is an optional extension, to validate L1 rollup costs of a tx + L1CostFn L1CostFunc }   // ValidateTransactionWithState is a helper method to check whether a transaction @@ -222,6 +255,11 @@ var ( balance = opts.State.GetBalance(from).ToBig() cost = tx.Cost() ) + if opts.L1CostFn != nil { + if l1Cost := opts.L1CostFn(tx.RollupCostData()); l1Cost != nil { // add rollup cost + cost = cost.Add(cost, l1Cost) + } + } if balance.Cmp(cost) < 0 { return fmt.Errorf("%w: balance %v, tx cost %v, overshot %v", core.ErrInsufficientFunds, balance, cost, new(big.Int).Sub(cost, balance)) }
diff --git go-ethereum/core/vm/contracts.go op-geth/core/vm/contracts.go index 104d2ba814b2f11a9ccf53cc6652e0e361f2a394..58146bdc526fa331be3fe349992fe27431fb07fc 100644 --- go-ethereum/core/vm/contracts.go +++ op-geth/core/vm/contracts.go @@ -35,6 +35,7 @@ "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/blake2b" "github.com/ethereum/go-ethereum/crypto/bn256" "github.com/ethereum/go-ethereum/crypto/kzg4844" + "github.com/ethereum/go-ethereum/crypto/secp256r1" "github.com/ethereum/go-ethereum/params" "golang.org/x/crypto/ripemd160" ) @@ -143,7 +144,41 @@ var PrecompiledContractsBLS = PrecompiledContractsPrague   var PrecompiledContractsVerkle = PrecompiledContractsPrague   +// PrecompiledContractsFjord contains the default set of pre-compiled Ethereum +// contracts used in the Fjord release. +var PrecompiledContractsFjord = map[common.Address]PrecompiledContract{ + common.BytesToAddress([]byte{1}): &ecrecover{}, + common.BytesToAddress([]byte{2}): &sha256hash{}, + common.BytesToAddress([]byte{3}): &ripemd160hash{}, + common.BytesToAddress([]byte{4}): &dataCopy{}, + common.BytesToAddress([]byte{5}): &bigModExp{eip2565: true}, + common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, + common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, + common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, + common.BytesToAddress([]byte{9}): &blake2F{}, + common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, + common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{}, +} + +// PrecompiledContractsGranite contains the default set of pre-compiled Ethereum +// contracts used in the Granite release. +var PrecompiledContractsGranite = map[common.Address]PrecompiledContract{ + common.BytesToAddress([]byte{1}): &ecrecover{}, + common.BytesToAddress([]byte{2}): &sha256hash{}, + common.BytesToAddress([]byte{3}): &ripemd160hash{}, + common.BytesToAddress([]byte{4}): &dataCopy{}, + common.BytesToAddress([]byte{5}): &bigModExp{eip2565: true}, + common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, + common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, + common.BytesToAddress([]byte{8}): &bn256PairingGranite{}, + common.BytesToAddress([]byte{9}): &blake2F{}, + common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, + common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{}, +} + var ( + PrecompiledAddressesGranite []common.Address + PrecompiledAddressesFjord []common.Address PrecompiledAddressesPrague []common.Address PrecompiledAddressesCancun []common.Address PrecompiledAddressesBerlin []common.Address @@ -171,10 +206,20 @@ } for k := range PrecompiledContractsPrague { PrecompiledAddressesPrague = append(PrecompiledAddressesPrague, k) } + for k := range PrecompiledContractsFjord { + PrecompiledAddressesFjord = append(PrecompiledAddressesFjord, k) + } + for k := range PrecompiledContractsGranite { + PrecompiledAddressesGranite = append(PrecompiledAddressesGranite, k) + } }   func activePrecompiledContracts(rules params.Rules) PrecompiledContracts { switch { + case rules.IsOptimismGranite: + return PrecompiledContractsGranite + case rules.IsOptimismFjord: + return PrecompiledContractsFjord case rules.IsVerkle: return PrecompiledContractsVerkle case rules.IsPrague: @@ -200,6 +245,10 @@ // ActivePrecompiles returns the precompile addresses enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { switch { + case rules.IsOptimismGranite: + return PrecompiledAddressesGranite + case rules.IsOptimismFjord: + return PrecompiledAddressesFjord case rules.IsPrague: return PrecompiledAddressesPrague case rules.IsCancun: @@ -578,6 +627,9 @@ false32Byte = make([]byte, 32)   // errBadPairingInput is returned if the bn256 pairing input is invalid. errBadPairingInput = errors.New("bad elliptic curve pairing size") + + // errBadPairingInputSize is returned if the bn256 pairing input size is invalid. + errBadPairingInputSize = errors.New("bad elliptic curve pairing input size") )   // runBn256Pairing implements the Bn256Pairing precompile, referenced by both @@ -609,6 +661,22 @@ if bn256.PairingCheck(cs, ts) { return true32Byte, nil } return false32Byte, nil +} + +// bn256PairingGranite implements a pairing pre-compile for the bn256 curve +// conforming to Granite consensus rules. +type bn256PairingGranite struct{} + +// RequiredGas returns the gas required to execute the pre-compiled contract. +func (c *bn256PairingGranite) RequiredGas(input []byte) uint64 { + return new(bn256PairingIstanbul).RequiredGas(input) +} + +func (c *bn256PairingGranite) Run(input []byte) ([]byte, error) { + if len(input) > int(params.Bn256PairingMaxInputSizeGranite) { + return nil, errBadPairingInputSize + } + return runBn256Pairing(input) }   // bn256PairingIstanbul implements a pairing pre-compile for the bn256 curve @@ -1251,3 +1319,37 @@ h[0] = blobCommitmentVersionKZG   return h } + +// P256VERIFY (secp256r1 signature verification) +// implemented as a native contract +type p256Verify struct{} + +// RequiredGas returns the gas required to execute the precompiled contract +func (c *p256Verify) RequiredGas(input []byte) uint64 { + return params.P256VerifyGas +} + +// Run executes the precompiled contract with given 160 bytes of param, returning the output and the used gas +func (c *p256Verify) Run(input []byte) ([]byte, error) { + // Required input length is 160 bytes + const p256VerifyInputLength = 160 + // Check the input length + if len(input) != p256VerifyInputLength { + // Input length is invalid + return nil, nil + } + + // Extract the hash, r, s, x, y from the input + hash := input[0:32] + r, s := new(big.Int).SetBytes(input[32:64]), new(big.Int).SetBytes(input[64:96]) + x, y := new(big.Int).SetBytes(input[96:128]), new(big.Int).SetBytes(input[128:160]) + + // Verify the secp256r1 signature + if secp256r1.Verify(hash, r, s, x, y) { + // Signature is valid + return common.LeftPadBytes([]byte{1}, 32), nil + } else { + // Signature is invalid + return nil, nil + } +}
diff --git go-ethereum/crypto/secp256r1/publickey.go op-geth/crypto/secp256r1/publickey.go new file mode 100644 index 0000000000000000000000000000000000000000..885a3e1a624b85310225913c44d294e402a50fcd --- /dev/null +++ op-geth/crypto/secp256r1/publickey.go @@ -0,0 +1,21 @@ +package secp256r1 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "math/big" +) + +// Generates appropriate public key format from given coordinates +func newPublicKey(x, y *big.Int) *ecdsa.PublicKey { + // Check if the given coordinates are valid + if x == nil || y == nil || !elliptic.P256().IsOnCurve(x, y) { + return nil + } + + return &ecdsa.PublicKey{ + Curve: elliptic.P256(), + X: x, + Y: y, + } +}
diff --git go-ethereum/crypto/secp256r1/verifier.go op-geth/crypto/secp256r1/verifier.go new file mode 100644 index 0000000000000000000000000000000000000000..ffb4839d8d560a5892175c737bff136d73ee6e12 --- /dev/null +++ op-geth/crypto/secp256r1/verifier.go @@ -0,0 +1,22 @@ +package secp256r1 + +import ( + "crypto/ecdsa" + "math/big" +) + +// Verify verifies the given signature (r, s) for the given hash and public key (x, y). +// It returns true if the signature is valid, false otherwise. +func Verify(hash []byte, r, s, x, y *big.Int) bool { + // Create the public key format + publicKey := newPublicKey(x, y) + + // Check if they are invalid public key coordinates + if publicKey == nil { + return false + } + + // Verify the signature with the public key, + // then return true if it's valid, false otherwise + return ecdsa.Verify(publicKey, hash, r, s) +}

The rollup functionality is enabled with the optimism field in the chain config. The EIP-1559 parameters are configurable to adjust for faster more frequent and smaller blocks. The parameters can be overriden for testing.

diff --git go-ethereum/core/genesis.go op-geth/core/genesis.go index 31db49f527e454c626f3bf50d598ac739d43db99..278cde106876e3e9acb10f6d05e6b2e01534c3d1 100644 --- go-ethereum/core/genesis.go +++ op-geth/core/genesis.go @@ -24,6 +24,8 @@ "fmt" "math/big" "strings"   + "github.com/ethereum-optimism/superchain-registry/superchain" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -73,6 +75,11 @@ ParentHash common.Hash `json:"parentHash"` BaseFee *big.Int `json:"baseFeePerGas"` // EIP-1559 ExcessBlobGas *uint64 `json:"excessBlobGas"` // EIP-4844 BlobGasUsed *uint64 `json:"blobGasUsed"` // EIP-4844 + + // StateHash represents the genesis state, to allow instantiation of a chain with missing initial state. + // Chains with history pruning, or extraordinarily large genesis allocation (e.g. after a regenesis event) + // may utilize this to get started, and then state-sync the latest state, while still verifying the header chain. + StateHash *common.Hash `json:"stateHash,omitempty"` }   func ReadGenesis(db ethdb.Database) (*Genesis, error) { @@ -109,12 +116,21 @@ genesis.Coinbase = genesisHeader.Coinbase genesis.BaseFee = genesisHeader.BaseFee genesis.ExcessBlobGas = genesisHeader.ExcessBlobGas genesis.BlobGasUsed = genesisHeader.BlobGasUsed + // A nil or empty alloc, with a non-matching state-root in the block header, intents to override the state-root. + if genesis.Alloc == nil || (len(genesis.Alloc) == 0 && genesisHeader.Root != types.EmptyRootHash) { + h := genesisHeader.Root // the genesis block is encoded as RLP in the DB and will contain the state-root + genesis.StateHash = &h + genesis.Alloc = nil + }   return &genesis, nil }   -// hashAlloc computes the state root according to the genesis specification. -func hashAlloc(ga *types.GenesisAlloc, isVerkle bool) (common.Hash, error) { +// hashAlloc returns the following: +// * computed state root according to the genesis specification. +// * storage root of the L2ToL1MessagePasser contract. +// * error if any, when committing the genesis state (if so, state root and storage root will be empty). +func hashAlloc(ga *types.GenesisAlloc, isVerkle, isIsthmus bool) (common.Hash, common.Hash, error) { // If a genesis-time verkle trie is requested, create a trie config // with the verkle trie enabled so that the tree can be initialized // as such. @@ -130,7 +146,7 @@ // all the derived states will be discarded to not pollute disk. db := rawdb.NewMemoryDatabase() statedb, err := state.New(types.EmptyRootHash, state.NewDatabase(triedb.NewDatabase(db, config), nil)) if err != nil { - return common.Hash{}, err + return common.Hash{}, common.Hash{}, err } for addr, account := range *ga { if account.Balance != nil { @@ -142,15 +158,27 @@ for key, value := range account.Storage { statedb.SetState(addr, key, value) } } - return statedb.Commit(0, false) + + stateRoot, err := statedb.Commit(0, false) + if err != nil { + return common.Hash{}, common.Hash{}, err + } + // get the storage root of the L2ToL1MessagePasser contract + var storageRootMessagePasser common.Hash + if isIsthmus { + storageRootMessagePasser = statedb.GetStorageRoot(params.OptimismL2ToL1MessagePasser) + } + + return stateRoot, storageRootMessagePasser, nil }   // flushAlloc is very similar with hash, but the main difference is all the -// generated states will be persisted into the given database. -func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database) (common.Hash, error) { +// generated states will be persisted into the given database. Returns the +// same values as hashAlloc. +func flushAlloc(ga *types.GenesisAlloc, triedb *triedb.Database, isIsthmus bool) (common.Hash, common.Hash, error) { statedb, err := state.New(types.EmptyRootHash, state.NewDatabase(triedb, nil)) if err != nil { - return common.Hash{}, err + return common.Hash{}, common.Hash{}, err } for addr, account := range *ga { if account.Balance != nil { @@ -164,17 +192,22 @@ for key, value := range account.Storage { statedb.SetState(addr, key, value) } } - root, err := statedb.Commit(0, false) + stateRoot, err := statedb.Commit(0, false) if err != nil { - return common.Hash{}, err + return common.Hash{}, common.Hash{}, err + } + // get the storage root of the L2ToL1MessagePasser contract + var storageRootMessagePasser common.Hash + if isIsthmus { + storageRootMessagePasser = statedb.GetStorageRoot(params.OptimismL2ToL1MessagePasser) } // Commit newly generated states into disk if it's not empty. - if root != types.EmptyRootHash { - if err := triedb.Commit(root, true); err != nil { - return common.Hash{}, err + if stateRoot != types.EmptyRootHash { + if err := triedb.Commit(stateRoot, true); err != nil { + return common.Hash{}, common.Hash{}, err } } - return root, nil + return stateRoot, storageRootMessagePasser, nil }   func getGenesisState(db ethdb.Database, blockhash common.Hash) (alloc types.GenesisAlloc, err error) { @@ -237,6 +270,14 @@ // ChainOverrides contains the changes to chain config. type ChainOverrides struct { OverrideCancun *uint64 OverrideVerkle *uint64 + // optimism + OverrideOptimismCanyon *uint64 + OverrideOptimismEcotone *uint64 + OverrideOptimismFjord *uint64 + OverrideOptimismGranite *uint64 + OverrideOptimismHolocene *uint64 + OverrideOptimismInterop *uint64 + ApplySuperchainUpgrades bool }   // SetupGenesisBlock writes or updates the genesis block in db. @@ -262,12 +303,48 @@ return params.AllEthashProtocolChanges, common.Hash{}, errGenesisNoConfig } applyOverrides := func(config *params.ChainConfig) { if config != nil { + // If applying the superchain-registry to a known OP-Stack chain, + // then override the local chain-config with that from the registry. + if overrides != nil && overrides.ApplySuperchainUpgrades && config.IsOptimism() && config.ChainID != nil && config.ChainID.IsUint64() { + if _, ok := superchain.OPChains[config.ChainID.Uint64()]; ok { + conf, err := params.LoadOPStackChainConfig(config.ChainID.Uint64()) + if err != nil { + log.Warn("failed to load chain config from superchain-registry, skipping override", "err", err, "chain_id", config.ChainID) + } else { + *config = *conf + } + } + } if overrides != nil && overrides.OverrideCancun != nil { config.CancunTime = overrides.OverrideCancun } if overrides != nil && overrides.OverrideVerkle != nil { config.VerkleTime = overrides.OverrideVerkle } + if overrides != nil && overrides.OverrideOptimismCanyon != nil { + config.CanyonTime = overrides.OverrideOptimismCanyon + config.ShanghaiTime = overrides.OverrideOptimismCanyon + if config.Optimism != nil && (config.Optimism.EIP1559DenominatorCanyon == nil || *config.Optimism.EIP1559DenominatorCanyon == 0) { + eip1559DenominatorCanyon := uint64(250) + config.Optimism.EIP1559DenominatorCanyon = &eip1559DenominatorCanyon + } + } + if overrides != nil && overrides.OverrideOptimismEcotone != nil { + config.EcotoneTime = overrides.OverrideOptimismEcotone + config.CancunTime = overrides.OverrideOptimismEcotone + } + if overrides != nil && overrides.OverrideOptimismFjord != nil { + config.FjordTime = overrides.OverrideOptimismFjord + } + if overrides != nil && overrides.OverrideOptimismGranite != nil { + config.GraniteTime = overrides.OverrideOptimismGranite + } + if overrides != nil && overrides.OverrideOptimismHolocene != nil { + config.HoloceneTime = overrides.OverrideOptimismHolocene + } + if overrides != nil && overrides.OverrideOptimismInterop != nil { + config.InteropTime = overrides.OverrideOptimismInterop + } } } // Just commit the new block if there is no stored genesis block. @@ -291,8 +368,13 @@ // The genesis block is present(perhaps in ancient database) while the // state database is not initialized yet. It can happen that the node // is initialized with an external ancient store. Commit genesis state // in this case. + // If the bedrock block is not 0, that implies that the network was migrated at the bedrock block. + // In this case the genesis state may not be in the state database (e.g. op-geth is performing a snap + // sync without an existing datadir) & even if it were, would not be useful as op-geth is not able to + // execute the pre-bedrock STF. header := rawdb.ReadHeader(db, stored, 0) - if header.Root != types.EmptyRootHash && !triedb.Initialized(header.Root) { + transitionedNetwork := genesis != nil && genesis.Config != nil && genesis.Config.BedrockBlock != nil && genesis.Config.BedrockBlock.Uint64() != 0 + if header.Root != types.EmptyRootHash && !triedb.Initialized(header.Root) && !transitionedNetwork { if genesis == nil { genesis = DefaultGenesisBlock() } @@ -319,6 +401,7 @@ } // Get the existing chain configuration. newcfg := genesis.configOrDefault(stored) applyOverrides(newcfg) + if err := newcfg.CheckConfigForkOrder(); err != nil { return newcfg, common.Hash{}, err } @@ -328,7 +411,9 @@ log.Warn("Found genesis block without chain config") rawdb.WriteChainConfig(db, stored, newcfg) return newcfg, stored, nil } + storedData, _ := json.Marshal(storedcfg) + log.Info("Stored config", "json", string(storedData)) // Special case: if a private network is being used (no genesis and also no // mainnet hash in the database), we must not apply the `configOrDefault` // chain config as that would be AllProtocolChanges (applying any new fork @@ -338,19 +423,30 @@ if genesis == nil && stored != params.MainnetGenesisHash { newcfg = storedcfg applyOverrides(newcfg) } + newData, _ := json.Marshal(newcfg) + log.Info("New config", "json", string(newData), "genesis-nil", genesis == nil) + // Check config compatibility and write the config. Compatibility errors // are returned to the caller unless we're already at block zero. head := rawdb.ReadHeadHeader(db) if head == nil { return newcfg, stored, errors.New("missing head header") } - compatErr := storedcfg.CheckCompatible(newcfg, head.Number.Uint64(), head.Time) + var genesisTimestamp *uint64 + if genesis != nil { + genesisTimestamp = &genesis.Timestamp + } + compatErr := storedcfg.CheckCompatible(newcfg, head.Number.Uint64(), head.Time, genesisTimestamp) if compatErr != nil && ((head.Number.Uint64() != 0 && compatErr.RewindToBlock != 0) || (head.Time != 0 && compatErr.RewindToTime != 0)) { return newcfg, stored, compatErr } + // Don't overwrite if the old is identical to the new - if newData, _ := json.Marshal(newcfg); !bytes.Equal(storedData, newData) { + if !bytes.Equal(storedData, newData) { + log.Info("Configs differ") rawdb.WriteChainConfig(db, stored, newcfg) + } else { + log.Info("Configs equal") } return newcfg, stored, nil } @@ -411,15 +507,27 @@ }   // ToBlock returns the genesis block according to genesis specification. func (g *Genesis) ToBlock() *types.Block { - root, err := hashAlloc(&g.Alloc, g.IsVerkle()) - if err != nil { + var stateRoot, storageRootMessagePasser common.Hash + var err error + if g.StateHash != nil { + if len(g.Alloc) > 0 { + panic(fmt.Errorf("cannot both have genesis hash %s "+ + "and non-empty state-allocation", *g.StateHash)) + } + // g.StateHash is only relevant for pre-bedrock (and hence pre-isthmus) chains. + // we bail here since this is not a valid usage of StateHash + if g.Config.IsOptimismIsthmus(g.Timestamp) { + panic(fmt.Errorf("stateHash usage disallowed in chain with isthmus active at genesis")) + } + stateRoot = *g.StateHash + } else if stateRoot, storageRootMessagePasser, err = hashAlloc(&g.Alloc, g.IsVerkle(), g.Config.IsOptimismIsthmus(g.Timestamp)); err != nil { panic(err) } - return g.toBlockWithRoot(root) + return g.toBlockWithRoot(stateRoot, storageRootMessagePasser) }   // toBlockWithRoot constructs the genesis block with the given genesis state root. -func (g *Genesis) toBlockWithRoot(root common.Hash) *types.Block { +func (g *Genesis) toBlockWithRoot(stateRoot, storageRootMessagePasser common.Hash) *types.Block { head := &types.Header{ Number: new(big.Int).SetUint64(g.Number), Nonce: types.EncodeNonce(g.Nonce), @@ -432,7 +540,7 @@ BaseFee: g.BaseFee, Difficulty: g.Difficulty, MixDigest: g.Mixhash, Coinbase: g.Coinbase, - Root: root, + Root: stateRoot, } if g.GasLimit == 0 { head.GasLimit = params.GenesisGasLimit @@ -476,8 +584,16 @@ if conf.IsPrague(num, g.Timestamp) { head.RequestsHash = &types.EmptyRequestsHash requests = make(types.Requests, 0) } + // If Isthmus is active at genesis, set the WithdrawalRoot to the storage root of the L2ToL1MessagePasser contract. + if g.Config.IsOptimismIsthmus(g.Timestamp) { + if storageRootMessagePasser == (common.Hash{}) { + // if there was no MessagePasser contract storage, set the WithdrawalsHash to the empty hash + storageRootMessagePasser = types.EmptyWithdrawalsHash + } + head.WithdrawalsHash = &storageRootMessagePasser + } } - return types.NewBlock(head, &types.Body{Withdrawals: withdrawals, Requests: requests}, nil, trie.NewStackTrie(nil)) + return types.NewBlock(head, &types.Body{Withdrawals: withdrawals, Requests: requests}, nil, trie.NewStackTrie(nil), g.Config) }   // Commit writes the block and state of a genesis specification to the database. @@ -496,12 +612,23 @@ } if config.Clique != nil && len(g.ExtraData) < 32+crypto.SignatureLength { return nil, errors.New("can't start clique chain without signers") } - // flush the data to disk and compute the state root - root, err := flushAlloc(&g.Alloc, triedb) - if err != nil { - return nil, err + var stateRoot, storageRootMessagePasser common.Hash + var err error + if len(g.Alloc) == 0 { + if g.StateHash == nil { + log.Warn("Empty genesis alloc, and no 'stateHash' override was set") + stateRoot = types.EmptyRootHash // default to the hash of the empty state. Some unit-tests rely on this. + } else { + stateRoot = *g.StateHash + } + } else { + // flush the data to disk and compute the state root + stateRoot, storageRootMessagePasser, err = flushAlloc(&g.Alloc, triedb, g.Config.IsIsthmus(g.Timestamp)) + if err != nil { + return nil, err + } } - block := g.toBlockWithRoot(root) + block := g.toBlockWithRoot(stateRoot, storageRootMessagePasser)   // Marshal the genesis state specification and persist. blob, err := json.Marshal(g.Alloc)
diff --git go-ethereum/params/config.go op-geth/params/config.go index 9ecf465bb67afddbc72a11f33aa009db45cd99e6..1b4368e76d15cbaa210010d583f57b42768a448b 100644 --- go-ethereum/params/config.go +++ op-geth/params/config.go @@ -21,6 +21,7 @@ "fmt" "math/big"   "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params/forks" )   @@ -29,6 +30,12 @@ var ( MainnetGenesisHash = common.HexToHash("0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") HoleskyGenesisHash = common.HexToHash("0xb5f7f912443c940f21fd611f12828d75b534364ed9e95ca4e307729a4661bde4") SepoliaGenesisHash = common.HexToHash("0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9") +) + +const ( + OPMainnetChainID = 10 + BaseMainnetChainID = 8453 + baseSepoliaChainID = 84532 )   func newUint64(val uint64) *uint64 { return &val } @@ -282,6 +289,16 @@ Ethash: new(EthashConfig), Clique: nil, } TestRules = TestChainConfig.Rules(new(big.Int), false, 0) + + // This is an Optimism chain config with bedrock starting a block 5, introduced for historical endpoint testing, largely based on the clique config + OptimismTestConfig = func() *ChainConfig { + conf := *AllCliqueProtocolChanges // copy the config + conf.Clique = nil + conf.TerminalTotalDifficultyPassed = true + conf.BedrockBlock = big.NewInt(5) + conf.Optimism = &OptimismConfig{EIP1559Elasticity: 50, EIP1559Denominator: 10} + return &conf + }() )   // NetworkNames are user friendly names to use in the chain spec banner. @@ -327,6 +344,18 @@ CancunTime *uint64 `json:"cancunTime,omitempty"` // Cancun switch time (nil = no fork, 0 = already on cancun) PragueTime *uint64 `json:"pragueTime,omitempty"` // Prague switch time (nil = no fork, 0 = already on prague) VerkleTime *uint64 `json:"verkleTime,omitempty"` // Verkle switch time (nil = no fork, 0 = already on verkle)   + BedrockBlock *big.Int `json:"bedrockBlock,omitempty"` // Bedrock switch block (nil = no fork, 0 = already on optimism bedrock) + RegolithTime *uint64 `json:"regolithTime,omitempty"` // Regolith switch time (nil = no fork, 0 = already on optimism regolith) + CanyonTime *uint64 `json:"canyonTime,omitempty"` // Canyon switch time (nil = no fork, 0 = already on optimism canyon) + // Delta: the Delta upgrade does not affect the execution-layer, and is thus not configurable in the chain config. + EcotoneTime *uint64 `json:"ecotoneTime,omitempty"` // Ecotone switch time (nil = no fork, 0 = already on optimism ecotone) + FjordTime *uint64 `json:"fjordTime,omitempty"` // Fjord switch time (nil = no fork, 0 = already on Optimism Fjord) + GraniteTime *uint64 `json:"graniteTime,omitempty"` // Granite switch time (nil = no fork, 0 = already on Optimism Granite) + HoloceneTime *uint64 `json:"holoceneTime,omitempty"` // Holocene switch time (nil = no fork, 0 = already on Optimism Holocene) + IsthmusTime *uint64 `json:"isthmusTime,omitempty"` // Isthmus switch time (nil = no fork, 0 = already on Optimism Isthmus) + + InteropTime *uint64 `json:"interopTime,omitempty"` // Interop switch time (nil = no fork, 0 = already on optimism interop) + // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` @@ -343,6 +372,9 @@ // Various consensus engines Ethash *EthashConfig `json:"ethash,omitempty"` Clique *CliqueConfig `json:"clique,omitempty"` + + // Optimism config, nil if not active + Optimism *OptimismConfig `json:"optimism,omitempty"` }   // EthashConfig is the consensus engine configs for proof-of-work based sealing. @@ -364,6 +396,18 @@ func (c CliqueConfig) String() string { return fmt.Sprintf("clique(period: %d, epoch: %d)", c.Period, c.Epoch) }   +// OptimismConfig is the optimism config. +type OptimismConfig struct { + EIP1559Elasticity uint64 `json:"eip1559Elasticity"` + EIP1559Denominator uint64 `json:"eip1559Denominator"` + EIP1559DenominatorCanyon *uint64 `json:"eip1559DenominatorCanyon,omitempty"` +} + +// String implements the stringer interface, returning the optimism fee config details. +func (o *OptimismConfig) String() string { + return "optimism" +} + // Description returns a human-readable description of ChainConfig. func (c *ChainConfig) Description() string { var banner string @@ -375,6 +419,8 @@ network = "unknown" } banner += fmt.Sprintf("Chain ID: %v (%s)\n", c.ChainID, network) switch { + case c.Optimism != nil: + banner += "Consensus: Optimism\n" case c.Ethash != nil: if c.TerminalTotalDifficulty == nil { banner += "Consensus: Ethash (proof-of-work)\n" @@ -453,6 +499,30 @@ } if c.VerkleTime != nil { banner += fmt.Sprintf(" - Verkle: @%-10v\n", *c.VerkleTime) } + if c.RegolithTime != nil { + banner += fmt.Sprintf(" - Regolith: @%-10v\n", *c.RegolithTime) + } + if c.CanyonTime != nil { + banner += fmt.Sprintf(" - Canyon: @%-10v\n", *c.CanyonTime) + } + if c.EcotoneTime != nil { + banner += fmt.Sprintf(" - Ecotone: @%-10v\n", *c.EcotoneTime) + } + if c.FjordTime != nil { + banner += fmt.Sprintf(" - Fjord: @%-10v\n", *c.FjordTime) + } + if c.GraniteTime != nil { + banner += fmt.Sprintf(" - Granite: @%-10v\n", *c.GraniteTime) + } + if c.HoloceneTime != nil { + banner += fmt.Sprintf(" - Holocene: @%-10v\n", *c.HoloceneTime) + } + if c.IsthmusTime != nil { + banner += fmt.Sprintf(" - Isthmus: @%-10v\n", *c.IsthmusTime) + } + if c.InteropTime != nil { + banner += fmt.Sprintf(" - Interop: @%-10v\n", *c.InteropTime) + } return banner }   @@ -561,9 +631,89 @@ func (c *ChainConfig) IsEIP4762(num *big.Int, time uint64) bool { return c.IsVerkle(num, time) }   +// IsBedrock returns whether num is either equal to the Bedrock fork block or greater. +func (c *ChainConfig) IsBedrock(num *big.Int) bool { + return isBlockForked(c.BedrockBlock, num) +} + +func (c *ChainConfig) IsRegolith(time uint64) bool { + return isTimestampForked(c.RegolithTime, time) +} + +func (c *ChainConfig) IsCanyon(time uint64) bool { + return isTimestampForked(c.CanyonTime, time) +} + +func (c *ChainConfig) IsEcotone(time uint64) bool { + return isTimestampForked(c.EcotoneTime, time) +} + +func (c *ChainConfig) IsFjord(time uint64) bool { + return isTimestampForked(c.FjordTime, time) +} + +func (c *ChainConfig) IsGranite(time uint64) bool { + return isTimestampForked(c.GraniteTime, time) +} + +func (c *ChainConfig) IsHolocene(time uint64) bool { + return isTimestampForked(c.HoloceneTime, time) +} + +func (c *ChainConfig) IsIsthmus(time uint64) bool { + return isTimestampForked(c.IsthmusTime, time) +} + +func (c *ChainConfig) IsInterop(time uint64) bool { + return isTimestampForked(c.InteropTime, time) +} + +// IsOptimism returns whether the node is an optimism node or not. +func (c *ChainConfig) IsOptimism() bool { + return c.Optimism != nil +} + +// IsOptimismBedrock returns true iff this is an optimism node & bedrock is active +func (c *ChainConfig) IsOptimismBedrock(num *big.Int) bool { + return c.IsOptimism() && c.IsBedrock(num) +} + +func (c *ChainConfig) IsOptimismRegolith(time uint64) bool { + return c.IsOptimism() && c.IsRegolith(time) +} + +func (c *ChainConfig) IsOptimismCanyon(time uint64) bool { + return c.IsOptimism() && c.IsCanyon(time) +} + +func (c *ChainConfig) IsOptimismEcotone(time uint64) bool { + return c.IsOptimism() && c.IsEcotone(time) +} + +func (c *ChainConfig) IsOptimismFjord(time uint64) bool { + return c.IsOptimism() && c.IsFjord(time) +} + +func (c *ChainConfig) IsOptimismGranite(time uint64) bool { + return c.IsOptimism() && c.IsGranite(time) +} + +func (c *ChainConfig) IsOptimismHolocene(time uint64) bool { + return c.IsOptimism() && c.IsHolocene(time) +} + +func (c *ChainConfig) IsOptimismIsthmus(time uint64) bool { + return c.IsOptimism() && c.IsIsthmus(time) +} + +// IsOptimismPreBedrock returns true iff this is an optimism node & bedrock is not yet active +func (c *ChainConfig) IsOptimismPreBedrock(num *big.Int) bool { + return c.IsOptimism() && !c.IsBedrock(num) +} + // CheckCompatible checks whether scheduled fork transitions have been imported // with a mismatching chain configuration. -func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64, time uint64) *ConfigCompatError { +func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height, time uint64, genesisTimestamp *uint64) *ConfigCompatError { var ( bhead = new(big.Int).SetUint64(height) btime = time @@ -571,7 +721,8 @@ ) // Iterate checkCompatible to find the lowest conflict. var lasterr *ConfigCompatError for { - err := c.checkCompatible(newcfg, bhead, btime) + err := c.checkCompatible(newcfg, bhead, btime, genesisTimestamp) + log.Info("Checking compatibility", "height", bhead, "time", btime, "error", err) if err == nil || (lasterr != nil && err.RewindToBlock == lasterr.RewindToBlock && err.RewindToTime == lasterr.RewindToTime) { break } @@ -654,7 +805,7 @@ } return nil }   -func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64) *ConfigCompatError { +func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64, genesisTimestamp *uint64) *ConfigCompatError { if isForkBlockIncompatible(c.HomesteadBlock, newcfg.HomesteadBlock, headNumber) { return newBlockCompatError("Homestead fork block", c.HomesteadBlock, newcfg.HomesteadBlock) } @@ -710,28 +861,65 @@ } if isForkBlockIncompatible(c.MergeNetsplitBlock, newcfg.MergeNetsplitBlock, headNumber) { return newBlockCompatError("Merge netsplit fork block", c.MergeNetsplitBlock, newcfg.MergeNetsplitBlock) } - if isForkTimestampIncompatible(c.ShanghaiTime, newcfg.ShanghaiTime, headTimestamp) { + if isForkTimestampIncompatible(c.ShanghaiTime, newcfg.ShanghaiTime, headTimestamp, genesisTimestamp) { return newTimestampCompatError("Shanghai fork timestamp", c.ShanghaiTime, newcfg.ShanghaiTime) } - if isForkTimestampIncompatible(c.CancunTime, newcfg.CancunTime, headTimestamp) { + if isForkTimestampIncompatible(c.CancunTime, newcfg.CancunTime, headTimestamp, genesisTimestamp) { return newTimestampCompatError("Cancun fork timestamp", c.CancunTime, newcfg.CancunTime) } - if isForkTimestampIncompatible(c.PragueTime, newcfg.PragueTime, headTimestamp) { + if isForkTimestampIncompatible(c.PragueTime, newcfg.PragueTime, headTimestamp, genesisTimestamp) { return newTimestampCompatError("Prague fork timestamp", c.PragueTime, newcfg.PragueTime) } - if isForkTimestampIncompatible(c.VerkleTime, newcfg.VerkleTime, headTimestamp) { + if isForkTimestampIncompatible(c.VerkleTime, newcfg.VerkleTime, headTimestamp, genesisTimestamp) { return newTimestampCompatError("Verkle fork timestamp", c.VerkleTime, newcfg.VerkleTime) } + if isForkBlockIncompatible(c.BedrockBlock, newcfg.BedrockBlock, headNumber) { + return newBlockCompatError("Bedrock fork block", c.BedrockBlock, newcfg.BedrockBlock) + } + if isForkTimestampIncompatible(c.RegolithTime, newcfg.RegolithTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("Regolith fork timestamp", c.RegolithTime, newcfg.RegolithTime) + } + if isForkTimestampIncompatible(c.CanyonTime, newcfg.CanyonTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("Canyon fork timestamp", c.CanyonTime, newcfg.CanyonTime) + } + if isForkTimestampIncompatible(c.EcotoneTime, newcfg.EcotoneTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("Ecotone fork timestamp", c.EcotoneTime, newcfg.EcotoneTime) + } + if isForkTimestampIncompatible(c.FjordTime, newcfg.FjordTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("Fjord fork timestamp", c.FjordTime, newcfg.FjordTime) + } + if isForkTimestampIncompatible(c.GraniteTime, newcfg.GraniteTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("Granite fork timestamp", c.GraniteTime, newcfg.GraniteTime) + } + if isForkTimestampIncompatible(c.HoloceneTime, newcfg.HoloceneTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("Holocene fork timestamp", c.HoloceneTime, newcfg.HoloceneTime) + } + if isForkTimestampIncompatible(c.InteropTime, newcfg.InteropTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("Interop fork timestamp", c.InteropTime, newcfg.InteropTime) + } return nil }   // BaseFeeChangeDenominator bounds the amount the base fee can change between blocks. -func (c *ChainConfig) BaseFeeChangeDenominator() uint64 { +// The time parameters is the timestamp of the block to determine if Canyon is active or not +func (c *ChainConfig) BaseFeeChangeDenominator(time uint64) uint64 { + if c.Optimism != nil { + if c.IsCanyon(time) { + if c.Optimism.EIP1559DenominatorCanyon == nil || *c.Optimism.EIP1559DenominatorCanyon == 0 { + panic("invalid ChainConfig.Optimism.EIP1559DenominatorCanyon value: '0' or 'nil'") + } + return *c.Optimism.EIP1559DenominatorCanyon + } + return c.Optimism.EIP1559Denominator + } return DefaultBaseFeeChangeDenominator }   // ElasticityMultiplier bounds the maximum gas limit an EIP-1559 block may have. func (c *ChainConfig) ElasticityMultiplier() uint64 { + if c.Optimism != nil { + return c.Optimism.EIP1559Elasticity + } return DefaultElasticityMultiplier }   @@ -753,7 +941,7 @@ } }   // isForkBlockIncompatible returns true if a fork scheduled at block s1 cannot be -// rescheduled to block s2 because head is already past the fork. +// rescheduled to block s2 because head is already past the fork and the fork was scheduled after genesis func isForkBlockIncompatible(s1, s2, head *big.Int) bool { return (isBlockForked(s1, head) || isBlockForked(s2, head)) && !configBlockEqual(s1, s2) } @@ -780,8 +968,15 @@ }   // isForkTimestampIncompatible returns true if a fork scheduled at timestamp s1 // cannot be rescheduled to timestamp s2 because head is already past the fork. -func isForkTimestampIncompatible(s1, s2 *uint64, head uint64) bool { - return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2) +func isForkTimestampIncompatible(s1, s2 *uint64, head uint64, genesis *uint64) bool { + return (isTimestampForked(s1, head) || isTimestampForked(s2, head)) && !configTimestampEqual(s1, s2) && !(isTimestampPreGenesis(s1, genesis) && isTimestampPreGenesis(s2, genesis)) +} + +func isTimestampPreGenesis(s, genesis *uint64) bool { + if s == nil || genesis == nil { + return false + } + return *s < *genesis }   // isTimestampForked returns whether a fork scheduled at timestamp s is active @@ -894,6 +1089,9 @@ IsByzantium, IsConstantinople, IsPetersburg, IsIstanbul bool IsBerlin, IsLondon bool IsMerge, IsShanghai, IsCancun, IsPrague bool IsVerkle bool + IsOptimismBedrock, IsOptimismRegolith bool + IsOptimismCanyon, IsOptimismFjord bool + IsOptimismGranite, IsOptimismHolocene bool }   // Rules ensures c's ChainID is not nil. @@ -924,5 +1122,16 @@ IsCancun: isMerge && c.IsCancun(num, timestamp), IsPrague: isMerge && c.IsPrague(num, timestamp), IsVerkle: isVerkle, IsEIP4762: isVerkle, + // Optimism + IsOptimismBedrock: isMerge && c.IsOptimismBedrock(num), + IsOptimismRegolith: isMerge && c.IsOptimismRegolith(timestamp), + IsOptimismCanyon: isMerge && c.IsOptimismCanyon(timestamp), + IsOptimismFjord: isMerge && c.IsOptimismFjord(timestamp), + IsOptimismGranite: isMerge && c.IsOptimismGranite(timestamp), + IsOptimismHolocene: isMerge && c.IsOptimismHolocene(timestamp), } } + +func (c *ChainConfig) HasOptimismWithdrawalsRoot(blockTime uint64) bool { + return c.IsOptimismIsthmus(blockTime) +}
diff --git go-ethereum/params/protocol_params.go op-geth/params/protocol_params.go index 638f58a33992f5048396ee306d95b5b6e3c70677..3c5ae0a8b4026cce8997b44b02caa99d47a5af77 100644 --- go-ethereum/params/protocol_params.go +++ op-geth/params/protocol_params.go @@ -22,6 +22,15 @@ "github.com/ethereum/go-ethereum/common" )   +var ( + // The base fee portion of the transaction fee accumulates at this predeploy + OptimismBaseFeeRecipient = common.HexToAddress("0x4200000000000000000000000000000000000019") + // The L1 portion of the transaction fee accumulates at this predeploy + OptimismL1FeeRecipient = common.HexToAddress("0x420000000000000000000000000000000000001A") + // The L2 withdrawals contract predeploy address + OptimismL2ToL1MessagePasser = common.HexToAddress("0x4200000000000000000000000000000000000016") +) + const ( GasLimitBoundDivisor uint64 = 1024 // The bound divisor of the gas limit, used in update calculations. MinGasLimit uint64 = 5000 // Minimum the gas limit may ever be. @@ -151,6 +160,8 @@ Bn256PairingBaseGasIstanbul uint64 = 45000 // Base price for an elliptic curve pairing check Bn256PairingPerPointGasByzantium uint64 = 80000 // Byzantium per-point price for an elliptic curve pairing check Bn256PairingPerPointGasIstanbul uint64 = 34000 // Per-point price for an elliptic curve pairing check   + Bn256PairingMaxInputSizeGranite uint64 = 112687 // Maximum input size for an elliptic curve pairing check + Bls12381G1AddGas uint64 = 500 // Price for BLS12-381 elliptic curve G1 point addition Bls12381G1MulGas uint64 = 12000 // Price for BLS12-381 elliptic curve G1 point scalar multiplication Bls12381G2AddGas uint64 = 800 // Price for BLS12-381 elliptic curve G2 point addition @@ -159,6 +170,8 @@ Bls12381PairingBaseGas uint64 = 65000 // Base gas price for BLS12-381 elliptic curve pairing check Bls12381PairingPerPairGas uint64 = 43000 // Per-point pair gas price for BLS12-381 elliptic curve pairing check Bls12381MapG1Gas uint64 = 5500 // Gas price for BLS12-381 mapping field element to G1 operation Bls12381MapG2Gas uint64 = 75000 // Gas price for BLS12-381 mapping field element to G2 operation + + P256VerifyGas uint64 = 3450 // secp256r1 elliptic curve signature verifier gas price   // The Refund Quotient is the cap on how much of the used gas can be refunded. Before EIP-3529, // up to half the consumed gas could be refunded. Redefined as 1/5th in EIP-3529

The optimism Goerli testnet used clique-config data to make geth internals accept blocks. Post-bedrock the beacon-consensus (i.e. follow Engine API) is now used, and the clique config is removed.

diff --git go-ethereum/core/rawdb/accessors_metadata.go op-geth/core/rawdb/accessors_metadata.go index 859566f722f5960b0ce417514d6f20ff35a5cbe9..7dae87a98f2f5383b3b14f0d04012eb64c98a91b 100644 --- go-ethereum/core/rawdb/accessors_metadata.go +++ op-geth/core/rawdb/accessors_metadata.go @@ -64,6 +64,9 @@ if err := json.Unmarshal(data, &config); err != nil { log.Error("Invalid chain config JSON", "hash", hash, "err", err) return nil } + if config.Optimism != nil { + config.Clique = nil // get rid of legacy clique data in chain config (optimism goerli issue) + } return &config }
diff --git go-ethereum/core/gen_genesis.go op-geth/core/gen_genesis.go index 2028f98edc0691b4dce10bbe0a8019f7931637f2..64840dec2d95080f15212107b7cb673ca1477489 100644 --- go-ethereum/core/gen_genesis.go +++ op-geth/core/gen_genesis.go @@ -34,6 +34,7 @@ ParentHash common.Hash `json:"parentHash"` BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + StateHash *common.Hash `json:"stateHash,omitempty"` } var enc Genesis enc.Config = g.Config @@ -56,6 +57,7 @@ enc.ParentHash = g.ParentHash enc.BaseFee = (*math.HexOrDecimal256)(g.BaseFee) enc.ExcessBlobGas = (*math.HexOrDecimal64)(g.ExcessBlobGas) enc.BlobGasUsed = (*math.HexOrDecimal64)(g.BlobGasUsed) + enc.StateHash = g.StateHash return json.Marshal(&enc) }   @@ -77,6 +79,7 @@ ParentHash *common.Hash `json:"parentHash"` BaseFee *math.HexOrDecimal256 `json:"baseFeePerGas"` ExcessBlobGas *math.HexOrDecimal64 `json:"excessBlobGas"` BlobGasUsed *math.HexOrDecimal64 `json:"blobGasUsed"` + StateHash *common.Hash `json:"stateHash,omitempty"` } var dec Genesis if err := json.Unmarshal(input, &dec); err != nil { @@ -132,6 +135,9 @@ g.ExcessBlobGas = (*uint64)(dec.ExcessBlobGas) } if dec.BlobGasUsed != nil { g.BlobGasUsed = (*uint64)(dec.BlobGasUsed) + } + if dec.StateHash != nil { + g.StateHash = dec.StateHash } return nil }

Testing of the superchain configuration

diff --git go-ethereum/core/superchain.go op-geth/core/superchain.go new file mode 100644 index 0000000000000000000000000000000000000000..f1849ae7310aa49e414ba2247ae999e86bc30ce0 --- /dev/null +++ op-geth/core/superchain.go @@ -0,0 +1,101 @@ +package core + +import ( + "fmt" + "math/big" + + "github.com/ethereum-optimism/superchain-registry/superchain" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +func LoadOPStackGenesis(chainID uint64) (*Genesis, error) { + chConfig, ok := superchain.OPChains[chainID] + if !ok { + return nil, fmt.Errorf("unknown chain ID: %d", chainID) + } + + cfg, err := params.LoadOPStackChainConfig(chainID) + if err != nil { + return nil, fmt.Errorf("failed to load params.ChainConfig for chain %d: %w", chainID, err) + } + + gen, err := superchain.LoadGenesis(chainID) + if err != nil { + return nil, fmt.Errorf("failed to load genesis definition for chain %d: %w", chainID, err) + } + + genesis := &Genesis{ + Config: cfg, + Nonce: gen.Nonce, + Timestamp: gen.Timestamp, + ExtraData: gen.ExtraData, + GasLimit: gen.GasLimit, + Difficulty: (*big.Int)(gen.Difficulty), + Mixhash: common.Hash(gen.Mixhash), + Coinbase: common.Address(gen.Coinbase), + Alloc: make(types.GenesisAlloc), + Number: gen.Number, + GasUsed: gen.GasUsed, + ParentHash: common.Hash(gen.ParentHash), + BaseFee: (*big.Int)(gen.BaseFee), + ExcessBlobGas: gen.ExcessBlobGas, + BlobGasUsed: gen.BlobGasUsed, + } + + for addr, acc := range gen.Alloc { + var code []byte + if acc.CodeHash != ([32]byte{}) { + dat, err := superchain.LoadContractBytecode(acc.CodeHash) + if err != nil { + return nil, fmt.Errorf("failed to load bytecode %s of address %s in chain %d: %w", acc.CodeHash, addr, chainID, err) + } + code = dat + } + var storage map[common.Hash]common.Hash + if len(acc.Storage) > 0 { + storage = make(map[common.Hash]common.Hash) + for k, v := range acc.Storage { + storage[common.Hash(k)] = common.Hash(v) + } + } + bal := common.Big0 + if acc.Balance != nil { + bal = (*big.Int)(acc.Balance) + } + genesis.Alloc[common.Address(addr)] = GenesisAccount{ + Code: code, + Storage: storage, + Balance: bal, + Nonce: acc.Nonce, + } + } + if gen.StateHash != nil { + if len(gen.Alloc) > 0 { + return nil, fmt.Errorf("chain definition unexpectedly contains both allocation (%d) and state-hash %s", len(gen.Alloc), *gen.StateHash) + } + genesis.StateHash = (*common.Hash)(gen.StateHash) + genesis.Alloc = nil + } + + genesisBlock := genesis.ToBlock() + genesisBlockHash := genesisBlock.Hash() + expectedHash := common.Hash([32]byte(chConfig.Genesis.L2.Hash)) + + // Verify we correctly produced the genesis config by recomputing the genesis-block-hash, + // and check the genesis matches the chain genesis definition. + if chConfig.Genesis.L2.Number != genesisBlock.NumberU64() { + switch chainID { + case params.OPMainnetChainID: + expectedHash = common.HexToHash("0x7ca38a1916c42007829c55e69d3e9a73265554b586a499015373241b8a3fa48b") + default: + return nil, fmt.Errorf("unknown stateless genesis definition for chain %d", chainID) + } + } + if expectedHash != genesisBlockHash { + return nil, fmt.Errorf("chainID=%d: produced genesis with hash %s but expected %s", chainID, genesisBlockHash, expectedHash) + } + return genesis, nil +}
diff --git go-ethereum/params/superchain.go op-geth/params/superchain.go new file mode 100644 index 0000000000000000000000000000000000000000..110d7d98f5c7931f5a9526479b0e5de917b4fd95 --- /dev/null +++ op-geth/params/superchain.go @@ -0,0 +1,266 @@ +package params + +import ( + "encoding/binary" + "fmt" + "math/big" + "sort" + "strings" + + "github.com/ethereum-optimism/superchain-registry/superchain" + "github.com/ethereum/go-ethereum/common" +) + +var OPStackSupport = ProtocolVersionV0{Build: [8]byte{}, Major: 9, Minor: 0, Patch: 0, PreRelease: 0}.Encode() + +func init() { + for id, ch := range superchain.OPChains { + NetworkNames[fmt.Sprintf("%d", id)] = ch.Name + } +} + +func OPStackChainIDByName(name string) (uint64, error) { + for id, ch := range superchain.OPChains { + if ch.Chain+"-"+ch.Superchain == name { + return id, nil + } + } + return 0, fmt.Errorf("unknown chain %q", name) +} + +func OPStackChainNames() (out []string) { + for _, ch := range superchain.OPChains { + out = append(out, ch.Chain+"-"+ch.Superchain) + } + sort.Strings(out) + return +} + +// uint64ptr is a weird helper to allow 1-line constant pointer creation. +func uint64ptr(n uint64) *uint64 { + return &n +} + +func LoadOPStackChainConfig(chainID uint64) (*ChainConfig, error) { + chConfig, ok := superchain.OPChains[chainID] + if !ok { + return nil, fmt.Errorf("unknown chain ID: %d", chainID) + } + + genesisActivation := uint64(0) + out := &ChainConfig{ + ChainID: new(big.Int).SetUint64(chainID), + HomesteadBlock: common.Big0, + DAOForkBlock: nil, + DAOForkSupport: false, + EIP150Block: common.Big0, + EIP155Block: common.Big0, + EIP158Block: common.Big0, + ByzantiumBlock: common.Big0, + ConstantinopleBlock: common.Big0, + PetersburgBlock: common.Big0, + IstanbulBlock: common.Big0, + MuirGlacierBlock: common.Big0, + BerlinBlock: common.Big0, + LondonBlock: common.Big0, + ArrowGlacierBlock: common.Big0, + GrayGlacierBlock: common.Big0, + MergeNetsplitBlock: common.Big0, + ShanghaiTime: chConfig.CanyonTime, // Shanghai activates with Canyon + CancunTime: chConfig.EcotoneTime, // Cancun activates with Ecotone + PragueTime: nil, + BedrockBlock: common.Big0, + RegolithTime: &genesisActivation, + CanyonTime: chConfig.CanyonTime, + EcotoneTime: chConfig.EcotoneTime, + FjordTime: chConfig.FjordTime, + GraniteTime: chConfig.GraniteTime, + HoloceneTime: chConfig.HoloceneTime, + TerminalTotalDifficulty: common.Big0, + TerminalTotalDifficultyPassed: true, + Ethash: nil, + Clique: nil, + } + + if chConfig.Optimism != nil { + out.Optimism = &OptimismConfig{ + EIP1559Elasticity: chConfig.Optimism.EIP1559Elasticity, + EIP1559Denominator: chConfig.Optimism.EIP1559Denominator, + } + if chConfig.Optimism.EIP1559DenominatorCanyon != nil { + out.Optimism.EIP1559DenominatorCanyon = uint64ptr(*chConfig.Optimism.EIP1559DenominatorCanyon) + } + } + + // special overrides for OP-Stack chains with pre-Regolith upgrade history + switch chainID { + case OPMainnetChainID: + out.BerlinBlock = big.NewInt(3950000) + out.LondonBlock = big.NewInt(105235063) + out.ArrowGlacierBlock = big.NewInt(105235063) + out.GrayGlacierBlock = big.NewInt(105235063) + out.MergeNetsplitBlock = big.NewInt(105235063) + out.BedrockBlock = big.NewInt(105235063) + } + + return out, nil +} + +// ProtocolVersion encodes the OP-Stack protocol version. See OP-Stack superchain-upgrade specification. +type ProtocolVersion [32]byte + +func (p ProtocolVersion) MarshalText() ([]byte, error) { + return common.Hash(p).MarshalText() +} + +func (p *ProtocolVersion) UnmarshalText(input []byte) error { + return (*common.Hash)(p).UnmarshalText(input) +} + +func (p ProtocolVersion) Parse() (versionType uint8, build [8]byte, major, minor, patch, preRelease uint32) { + versionType = p[0] + if versionType != 0 { + return + } + // bytes 1:8 reserved for future use + copy(build[:], p[8:16]) // differentiates forks and custom-builds of standard protocol + major = binary.BigEndian.Uint32(p[16:20]) // incompatible API changes + minor = binary.BigEndian.Uint32(p[20:24]) // identifies additional functionality in backwards compatible manner + patch = binary.BigEndian.Uint32(p[24:28]) // identifies backward-compatible bug-fixes + preRelease = binary.BigEndian.Uint32(p[28:32]) // identifies unstable versions that may not satisfy the above + return +} + +func (p ProtocolVersion) String() string { + versionType, build, major, minor, patch, preRelease := p.Parse() + if versionType != 0 { + return "v0.0.0-unknown." + common.Hash(p).String() + } + ver := fmt.Sprintf("v%d.%d.%d", major, minor, patch) + if preRelease != 0 { + ver += fmt.Sprintf("-%d", preRelease) + } + if build != ([8]byte{}) { + if humanBuildTag(build) { + ver += fmt.Sprintf("+%s", strings.TrimRight(string(build[:]), "\x00")) + } else { + ver += fmt.Sprintf("+0x%x", build) + } + } + return ver +} + +// humanBuildTag identifies which build tag we can stringify for human-readable versions +func humanBuildTag(v [8]byte) bool { + for i, c := range v { // following semver.org advertised regex, alphanumeric with '-' and '.', except leading '.'. + if c == 0 { // trailing zeroed are allowed + for _, d := range v[i:] { + if d != 0 { + return false + } + } + return true + } + if !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || (c == '.' && i > 0)) { + return false + } + } + return true +} + +// ProtocolVersionComparison is used to identify how far ahead/outdated a protocol version is relative to another. +// This value is used in metrics and switch comparisons, to easily identify each type of version difference. +// Negative values mean the version is outdated. +// Positive values mean the version is up-to-date. +// Matching versions have a 0. +type ProtocolVersionComparison int + +const ( + AheadMajor ProtocolVersionComparison = 4 + OutdatedMajor ProtocolVersionComparison = -4 + AheadMinor ProtocolVersionComparison = 3 + OutdatedMinor ProtocolVersionComparison = -3 + AheadPatch ProtocolVersionComparison = 2 + OutdatedPatch ProtocolVersionComparison = -2 + AheadPrerelease ProtocolVersionComparison = 1 + OutdatedPrerelease ProtocolVersionComparison = -1 + Matching ProtocolVersionComparison = 0 + DiffVersionType ProtocolVersionComparison = 100 + DiffBuild ProtocolVersionComparison = 101 + EmptyVersion ProtocolVersionComparison = 102 + InvalidVersion ProtocolVersionComparison = 103 +) + +func (p ProtocolVersion) Compare(other ProtocolVersion) (cmp ProtocolVersionComparison) { + if p == (ProtocolVersion{}) || (other == (ProtocolVersion{})) { + return EmptyVersion + } + aVersionType, aBuild, aMajor, aMinor, aPatch, aPreRelease := p.Parse() + bVersionType, bBuild, bMajor, bMinor, bPatch, bPreRelease := other.Parse() + if aVersionType != bVersionType { + return DiffVersionType + } + if aBuild != bBuild { + return DiffBuild + } + // max values are reserved, consider versions with these values invalid + if aMajor == ^uint32(0) || aMinor == ^uint32(0) || aPatch == ^uint32(0) || aPreRelease == ^uint32(0) || + bMajor == ^uint32(0) || bMinor == ^uint32(0) || bPatch == ^uint32(0) || bPreRelease == ^uint32(0) { + return InvalidVersion + } + fn := func(a, b uint32, ahead, outdated ProtocolVersionComparison) ProtocolVersionComparison { + if a == b { + return Matching + } + if a > b { + return ahead + } + return outdated + } + if aPreRelease != 0 { // if A is a pre-release, then decrement the version before comparison + if aPatch != 0 { + aPatch -= 1 + } else if aMinor != 0 { + aMinor -= 1 + aPatch = ^uint32(0) // max value + } else if aMajor != 0 { + aMajor -= 1 + aMinor = ^uint32(0) // max value + } + } + if bPreRelease != 0 { // if B is a pre-release, then decrement the version before comparison + if bPatch != 0 { + bPatch -= 1 + } else if bMinor != 0 { + bMinor -= 1 + bPatch = ^uint32(0) // max value + } else if bMajor != 0 { + bMajor -= 1 + bMinor = ^uint32(0) // max value + } + } + if c := fn(aMajor, bMajor, AheadMajor, OutdatedMajor); c != Matching { + return c + } + if c := fn(aMinor, bMinor, AheadMinor, OutdatedMinor); c != Matching { + return c + } + if c := fn(aPatch, bPatch, AheadPatch, OutdatedPatch); c != Matching { + return c + } + return fn(aPreRelease, bPreRelease, AheadPrerelease, OutdatedPrerelease) +} + +type ProtocolVersionV0 struct { + Build [8]byte + Major, Minor, Patch, PreRelease uint32 +} + +func (v ProtocolVersionV0) Encode() (out ProtocolVersion) { + copy(out[8:16], v.Build[:]) + binary.BigEndian.PutUint32(out[16:20], v.Major) + binary.BigEndian.PutUint32(out[20:24], v.Minor) + binary.BigEndian.PutUint32(out[24:28], v.Patch) + binary.BigEndian.PutUint32(out[28:32], v.PreRelease) + return +}

Changes to the node configuration and services.

Flag changes: - Transactions can be forwarded to an RPC for sequencing. - Historical calls can be forwarded to a legacy node. - The tx pool propagation can be enabled/disabled. - The Optimism bedrock fork activation can be changed for testing.

diff --git go-ethereum/cmd/geth/config.go op-geth/cmd/geth/config.go index fd57ff40def499b95bd6ca18fbc0ef8111bae3d3..df12a831b62fc21d4b73a6b56d50b82888b857de 100644 --- go-ethereum/cmd/geth/config.go +++ op-geth/cmd/geth/config.go @@ -187,6 +187,37 @@ if ctx.IsSet(utils.OverrideCancun.Name) { v := ctx.Uint64(utils.OverrideCancun.Name) cfg.Eth.OverrideCancun = &v } + + if ctx.IsSet(utils.OverrideOptimismCanyon.Name) { + v := ctx.Uint64(utils.OverrideOptimismCanyon.Name) + cfg.Eth.OverrideOptimismCanyon = &v + } + + if ctx.IsSet(utils.OverrideOptimismEcotone.Name) { + v := ctx.Uint64(utils.OverrideOptimismEcotone.Name) + cfg.Eth.OverrideOptimismEcotone = &v + } + + if ctx.IsSet(utils.OverrideOptimismFjord.Name) { + v := ctx.Uint64(utils.OverrideOptimismFjord.Name) + cfg.Eth.OverrideOptimismFjord = &v + } + + if ctx.IsSet(utils.OverrideOptimismGranite.Name) { + v := ctx.Uint64(utils.OverrideOptimismGranite.Name) + cfg.Eth.OverrideOptimismGranite = &v + } + + if ctx.IsSet(utils.OverrideOptimismHolocene.Name) { + v := ctx.Uint64(utils.OverrideOptimismHolocene.Name) + cfg.Eth.OverrideOptimismHolocene = &v + } + + if ctx.IsSet(utils.OverrideOptimismInterop.Name) { + v := ctx.Uint64(utils.OverrideOptimismInterop.Name) + cfg.Eth.OverrideOptimismInterop = &v + } + if ctx.IsSet(utils.OverrideVerkle.Name) { v := ctx.Uint64(utils.OverrideVerkle.Name) cfg.Eth.OverrideVerkle = &v
diff --git go-ethereum/cmd/geth/main.go op-geth/cmd/geth/main.go index 2675a616759c99fcf3495c42decd2ac324eeee47..30c7df3b84c347e116ddd3b74de1a9bd9dfc1c79 100644 --- go-ethereum/cmd/geth/main.go +++ op-geth/cmd/geth/main.go @@ -66,10 +66,17 @@ utils.USBFlag, utils.SmartCardDaemonPathFlag, utils.OverrideCancun, utils.OverrideVerkle, + utils.OverrideOptimismCanyon, + utils.OverrideOptimismEcotone, + utils.OverrideOptimismFjord, + utils.OverrideOptimismGranite, + utils.OverrideOptimismHolocene, + utils.OverrideOptimismInterop, utils.EnablePersonal, utils.TxPoolLocalsFlag, utils.TxPoolNoLocalsFlag, utils.TxPoolJournalFlag, + utils.TxPoolJournalRemotesFlag, utils.TxPoolRejournalFlag, utils.TxPoolPriceLimitFlag, utils.TxPoolPriceBumpFlag, @@ -117,6 +124,7 @@ utils.MaxPeersFlag, utils.MaxPendingPeersFlag, utils.MiningEnabledFlag, // deprecated utils.MinerGasLimitFlag, + utils.MinerEffectiveGasLimitFlag, utils.MinerGasPriceFlag, utils.MinerEtherbaseFlag, // deprecated utils.MinerExtraDataFlag, @@ -145,6 +153,18 @@ utils.GpoBlocksFlag, utils.GpoPercentileFlag, utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, + utils.GpoMinSuggestedPriorityFeeFlag, + utils.RollupSequencerHTTPFlag, + utils.RollupSequencerTxConditionalEnabledFlag, + utils.RollupSequencerTxConditionalCostRateLimitFlag, + utils.RollupHistoricalRPCFlag, + utils.RollupHistoricalRPCTimeoutFlag, + utils.RollupInteropRPCFlag, + utils.RollupInteropMempoolFilteringFlag, + utils.RollupDisableTxPoolGossipFlag, + utils.RollupComputePendingBlock, + utils.RollupHaltOnIncompatibleProtocolVersionFlag, + utils.RollupSuperchainUpgradesFlag, configFileFlag, utils.LogDebugFlag, utils.LogBacktraceAtFlag, @@ -312,6 +332,9 @@ 5. Networking is disabled; there is no listen-address, the maximum number of peers is set to 0, and discovery is disabled. `)   + case ctx.IsSet(utils.OPNetworkFlag.Name): + log.Info("Starting geth on an OP network...", "network", ctx.String(utils.OPNetworkFlag.Name)) + case !ctx.IsSet(utils.NetworkIdFlag.Name): log.Info("Starting Geth on Ethereum mainnet...") } @@ -322,7 +345,14 @@ if !ctx.IsSet(utils.HoleskyFlag.Name) && !ctx.IsSet(utils.SepoliaFlag.Name) && !ctx.IsSet(utils.DeveloperFlag.Name) { // Nope, we're really on mainnet. Bump that cache up! + // Note: If we don't set the OPNetworkFlag and have already initialized the database, we may hit this case. log.Info("Bumping default cache on mainnet", "provided", ctx.Int(utils.CacheFlag.Name), "updated", 4096) + ctx.Set(utils.CacheFlag.Name, strconv.Itoa(4096)) + } + } else if ctx.String(utils.SyncModeFlag.Name) != "light" && !ctx.IsSet(utils.CacheFlag.Name) && ctx.IsSet(utils.OPNetworkFlag.Name) { + // We haven't set the cache, but may used the OP network flag we may be on an OP stack mainnet. + if strings.Contains(ctx.String(utils.OPNetworkFlag.Name), "mainnet") { + log.Info("Bumping default cache on mainnet", "provided", ctx.Int(utils.CacheFlag.Name), "updated", 4096, "network", ctx.String(utils.OPNetworkFlag.Name)) ctx.Set(utils.CacheFlag.Name, strconv.Itoa(4096)) } }
diff --git go-ethereum/cmd/utils/flags.go op-geth/cmd/utils/flags.go index 6db88ff661832c694ef42ebeec81d787db894222..8eda534783632a780823c576fd0389dbdddd8148 100644 --- go-ethereum/cmd/utils/flags.go +++ op-geth/cmd/utils/flags.go @@ -153,6 +153,15 @@ Name: "holesky", Usage: "Holesky network: pre-configured proof-of-stake test network", Category: flags.EthCategory, } + + OPNetworkFlag = &cli.StringFlag{ + Name: "op-network", + Aliases: []string{"beta.op-network"}, + Usage: "Select a pre-configured OP-Stack network (warning: op-mainnet and op-goerli require special sync," + + " datadir is recommended), options: " + strings.Join(params.OPStackChainNames(), ", "), + Category: flags.EthCategory, + } + // Dev mode DeveloperFlag = &cli.BoolFlag{ Name: "dev", @@ -250,6 +259,36 @@ Name: "override.verkle", Usage: "Manually specify the Verkle fork timestamp, overriding the bundled setting", Category: flags.EthCategory, } + OverrideOptimismCanyon = &cli.Uint64Flag{ + Name: "override.canyon", + Usage: "Manually specify the Optimism Canyon fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } + OverrideOptimismEcotone = &cli.Uint64Flag{ + Name: "override.ecotone", + Usage: "Manually specify the Optimism Ecotone fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } + OverrideOptimismFjord = &cli.Uint64Flag{ + Name: "override.fjord", + Usage: "Manually specify the Optimism Fjord fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } + OverrideOptimismGranite = &cli.Uint64Flag{ + Name: "override.granite", + Usage: "Manually specify the Optimism Granite fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } + OverrideOptimismHolocene = &cli.Uint64Flag{ + Name: "override.holocene", + Usage: "Manually specify the Optimism Holocene fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } + OverrideOptimismInterop = &cli.Uint64Flag{ + Name: "override.interop", + Usage: "Manually specify the Optimsim Interop feature-set fork timestamp, overriding the bundled setting", + Category: flags.EthCategory, + } SyncModeFlag = &flags.TextMarshalerFlag{ Name: "syncmode", Usage: `Blockchain sync mode ("snap" or "full")`, @@ -346,6 +385,11 @@ TxPoolJournalFlag = &cli.StringFlag{ Name: "txpool.journal", Usage: "Disk journal for local transaction to survive node restarts", Value: ethconfig.Defaults.TxPool.Journal, + Category: flags.TxPoolCategory, + } + TxPoolJournalRemotesFlag = &cli.BoolFlag{ + Name: "txpool.journalremotes", + Usage: "Includes remote transactions in the journal", Category: flags.TxPoolCategory, } TxPoolRejournalFlag = &cli.DurationFlag{ @@ -479,6 +523,12 @@ MinerGasLimitFlag = &cli.Uint64Flag{ Name: "miner.gaslimit", Usage: "Target gas ceiling for mined blocks", Value: ethconfig.Defaults.Miner.GasCeil, + Category: flags.MinerCategory, + } + MinerEffectiveGasLimitFlag = &cli.Uint64Flag{ + Name: "miner.effectivegaslimit", + Usage: "If non-zero, an effective gas limit to apply in addition to the block header gaslimit.", + Value: 0, Category: flags.MinerCategory, } MinerGasPriceFlag = &flags.BigFlag{ @@ -797,7 +847,7 @@ Name: "discovery.v4", Aliases: []string{"discv4"}, Usage: "Enables the V4 discovery mechanism", Category: flags.NetworkingCategory, - Value: true, + Value: false, } DiscoveryV5Flag = &cli.BoolFlag{ Name: "discovery.v5", @@ -862,6 +912,84 @@ Usage: "Gas price below which gpo will ignore transactions", Value: ethconfig.Defaults.GPO.IgnorePrice.Int64(), Category: flags.GasPriceCategory, } + GpoMinSuggestedPriorityFeeFlag = &cli.Int64Flag{ + Name: "gpo.minsuggestedpriorityfee", + Usage: "Minimum transaction priority fee to suggest. Used on OP chains when blocks are not full.", + Value: ethconfig.Defaults.GPO.MinSuggestedPriorityFee.Int64(), + Category: flags.GasPriceCategory, + } + + // Rollup Flags + RollupSequencerHTTPFlag = &cli.StringFlag{ + Name: "rollup.sequencerhttp", + Usage: "HTTP endpoint for the sequencer mempool", + Category: flags.RollupCategory, + } + + RollupHistoricalRPCFlag = &cli.StringFlag{ + Name: "rollup.historicalrpc", + Usage: "RPC endpoint for historical data.", + Category: flags.RollupCategory, + } + + RollupHistoricalRPCTimeoutFlag = &cli.StringFlag{ + Name: "rollup.historicalrpctimeout", + Usage: "Timeout for historical RPC requests.", + Value: "5s", + 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.", + Category: flags.RollupCategory, + } + RollupEnableTxPoolAdmissionFlag = &cli.BoolFlag{ + Name: "rollup.enabletxpooladmission", + Usage: "Add RPC-submitted transactions to the txpool (on by default if --rollup.sequencerhttp is not set).", + Category: flags.RollupCategory, + } + RollupComputePendingBlock = &cli.BoolFlag{ + Name: "rollup.computependingblock", + Usage: "By default the pending block equals the latest block to save resources and not leak txs from the tx-pool, this flag enables computing of the pending block from the tx-pool instead.", + Category: flags.RollupCategory, + } + RollupHaltOnIncompatibleProtocolVersionFlag = &cli.StringFlag{ + Name: "rollup.halt", + Usage: "Opt-in option to halt on incompatible protocol version requirements of the given level (major/minor/patch/none), as signaled through the Engine API by the rollup node", + Category: flags.RollupCategory, + } + RollupSuperchainUpgradesFlag = &cli.BoolFlag{ + Name: "rollup.superchain-upgrades", + Aliases: []string{"beta.rollup.superchain-upgrades"}, + Usage: "Apply superchain-registry config changes to the local chain-configuration", + Category: flags.RollupCategory, + Value: true, + } + RollupSequencerTxConditionalEnabledFlag = &cli.BoolFlag{ + Name: "rollup.sequencertxconditionalenabled", + Usage: "Serve the eth_sendRawTransactionConditional endpoint and apply the conditional constraints on mempool inclusion & block building", + Category: flags.RollupCategory, + Value: false, + } + RollupSequencerTxConditionalCostRateLimitFlag = &cli.IntFlag{ + Name: "rollup.sequencertxconditionalcostratelimit", + Usage: "Maximum cost -- storage lookups -- allowed for conditional transactions in a given second", + Category: flags.RollupCategory, + Value: 5000, + }   // Metrics flags MetricsEnabledFlag = &cli.BoolFlag{ @@ -960,7 +1088,7 @@ SepoliaFlag, HoleskyFlag, } // NetworkFlags is the flag group of all built-in supported networks. - NetworkFlags = append([]cli.Flag{MainnetFlag}, TestnetFlags...) + NetworkFlags = append([]cli.Flag{MainnetFlag, OPNetworkFlag}, TestnetFlags...)   // DatabaseFlags is the flag group of all database flags. DatabaseFlags = []cli.Flag{ @@ -984,6 +1112,9 @@ } if ctx.Bool(HoleskyFlag.Name) { return filepath.Join(path, "holesky") } + if ctx.IsSet(OPNetworkFlag.Name) { + return filepath.Join(path, ctx.String(OPNetworkFlag.Name)) + } return path } Fatalf("Cannot determine default data directory, please set manually (--datadir)") @@ -1073,6 +1204,13 @@ case ctx.IsSet(BootnodesFlag.Name): urls = SplitAndTrim(ctx.String(BootnodesFlag.Name)) case cfg.BootstrapNodesV5 != nil: return // already set, don't apply defaults. + case ctx.IsSet(OPNetworkFlag.Name): + network := ctx.String(OPNetworkFlag.Name) + if strings.Contains(strings.ToLower(network), "mainnet") { + urls = params.V5OPBootnodes + } else { + urls = params.V5OPTestnetBootnodes + } }   cfg.BootstrapNodesV5 = make([]*enode.Node, 0, len(urls)) @@ -1472,6 +1610,8 @@ case ctx.Bool(SepoliaFlag.Name) && cfg.DataDir == node.DefaultDataDir(): cfg.DataDir = filepath.Join(node.DefaultDataDir(), "sepolia") case ctx.Bool(HoleskyFlag.Name) && cfg.DataDir == node.DefaultDataDir(): cfg.DataDir = filepath.Join(node.DefaultDataDir(), "holesky") + case ctx.IsSet(OPNetworkFlag.Name) && cfg.DataDir == node.DefaultDataDir(): + cfg.DataDir = filepath.Join(node.DefaultDataDir(), ctx.String(OPNetworkFlag.Name)) } }   @@ -1488,6 +1628,9 @@ } if ctx.IsSet(GpoIgnoreGasPriceFlag.Name) { cfg.IgnorePrice = big.NewInt(ctx.Int64(GpoIgnoreGasPriceFlag.Name)) } + if ctx.IsSet(GpoMinSuggestedPriorityFeeFlag.Name) { + cfg.MinSuggestedPriorityFee = big.NewInt(ctx.Int64(GpoMinSuggestedPriorityFeeFlag.Name)) + } }   func setTxPool(ctx *cli.Context, cfg *legacypool.Config) { @@ -1507,6 +1650,9 @@ } if ctx.IsSet(TxPoolJournalFlag.Name) { cfg.Journal = ctx.String(TxPoolJournalFlag.Name) } + if ctx.IsSet(TxPoolJournalRemotesFlag.Name) { + cfg.JournalRemote = ctx.Bool(TxPoolJournalRemotesFlag.Name) + } if ctx.IsSet(TxPoolRejournalFlag.Name) { cfg.Rejournal = ctx.Duration(TxPoolRejournalFlag.Name) } @@ -1531,6 +1677,11 @@ } if ctx.IsSet(TxPoolLifetimeFlag.Name) { cfg.Lifetime = ctx.Duration(TxPoolLifetimeFlag.Name) } + if ctx.IsSet(MinerEffectiveGasLimitFlag.Name) { + // While technically this is a miner config parameter, we also want the txpool to enforce + // it to avoid accepting transactions that can never be included in a block. + cfg.EffectiveGasCeil = ctx.Uint64(MinerEffectiveGasLimitFlag.Name) + } }   func setBlobPool(ctx *cli.Context, cfg *blobpool.Config) { @@ -1555,6 +1706,9 @@ } if ctx.IsSet(MinerGasLimitFlag.Name) { cfg.GasCeil = ctx.Uint64(MinerGasLimitFlag.Name) } + if ctx.IsSet(MinerEffectiveGasLimitFlag.Name) { + cfg.EffectiveGasCeil = ctx.Uint64(MinerEffectiveGasLimitFlag.Name) + } if ctx.IsSet(MinerGasPriceFlag.Name) { cfg.GasPrice = flags.GlobalBig(ctx, MinerGasPriceFlag.Name) } @@ -1564,6 +1718,9 @@ } if ctx.IsSet(MinerNewPayloadTimeoutFlag.Name) { log.Warn("The flag --miner.newpayload-timeout is deprecated and will be removed, please use --miner.recommit") cfg.Recommit = ctx.Duration(MinerNewPayloadTimeoutFlag.Name) + } + if ctx.IsSet(RollupComputePendingBlock.Name) { + cfg.RollupComputePendingBlock = ctx.Bool(RollupComputePendingBlock.Name) } }   @@ -1639,7 +1796,7 @@ // SetEthConfig applies eth-related command line flags to the config. func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { // Avoid conflicting network flags - CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag) + CheckExclusive(ctx, MainnetFlag, DeveloperFlag, SepoliaFlag, HoleskyFlag, OPNetworkFlag) CheckExclusive(ctx, DeveloperFlag, ExternalSignerFlag) // Can't use both ephemeral unlocked and external signer   // Set configurations from CLI flags @@ -1786,6 +1943,29 @@ } else { cfg.EthDiscoveryURLs = SplitAndTrim(urls) } } + // Only configure sequencer http flag if we're running in verifier mode i.e. --mine is disabled. + if ctx.IsSet(RollupSequencerHTTPFlag.Name) && !ctx.IsSet(MiningEnabledFlag.Name) { + cfg.RollupSequencerHTTP = ctx.String(RollupSequencerHTTPFlag.Name) + } + if ctx.IsSet(RollupHistoricalRPCFlag.Name) { + cfg.RollupHistoricalRPC = ctx.String(RollupHistoricalRPCFlag.Name) + } + 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) + cfg.ApplySuperchainUpgrades = ctx.Bool(RollupSuperchainUpgradesFlag.Name) + cfg.RollupSequencerTxConditionalEnabled = ctx.Bool(RollupSequencerTxConditionalEnabledFlag.Name) + cfg.RollupSequencerTxConditionalCostRateLimit = ctx.Int(RollupSequencerTxConditionalCostRateLimitFlag.Name) + // Override any default configs for hard coded networks. switch { case ctx.Bool(MainnetFlag.Name): @@ -1861,7 +2041,7 @@ chaindb := tryMakeReadOnlyDatabase(ctx, stack) if rawdb.ReadCanonicalHash(chaindb, 0) != (common.Hash{}) { cfg.Genesis = nil // fallback to db content   - //validate genesis has PoS enabled in block 0 + // validate genesis has PoS enabled in block 0 genesis, err := core.ReadGenesis(chaindb) if err != nil { Fatalf("Could not read genesis from database: %v", err) @@ -1883,6 +2063,12 @@ } if !ctx.IsSet(MinerGasPriceFlag.Name) { cfg.Miner.GasPrice = big.NewInt(1) } + case ctx.IsSet(OPNetworkFlag.Name): + genesis := MakeGenesis(ctx) + if !ctx.IsSet(NetworkIdFlag.Name) { + cfg.NetworkId = genesis.Config.ChainID.Uint64() + } + cfg.Genesis = genesis default: if cfg.NetworkId == 1 { SetDNSDiscoveryDefaults(cfg, params.MainnetGenesisHash) @@ -2084,8 +2270,7 @@ }   func IsNetworkPreset(ctx *cli.Context) bool { for _, flag := range NetworkFlags { - bFlag, _ := flag.(*cli.BoolFlag) - if ctx.IsSet(bFlag.Name) { + if ctx.IsSet(flag.Names()[0]) { return true } } @@ -2125,6 +2310,17 @@ case ctx.Bool(HoleskyFlag.Name): genesis = core.DefaultHoleskyGenesisBlock() case ctx.Bool(SepoliaFlag.Name): genesis = core.DefaultSepoliaGenesisBlock() + case ctx.IsSet(OPNetworkFlag.Name): + name := ctx.String(OPNetworkFlag.Name) + ch, err := params.OPStackChainIDByName(name) + if err != nil { + Fatalf("failed to load OP-Stack chain %q: %v", name, err) + } + genesis, err := core.LoadOPStackGenesis(ch) + if err != nil { + Fatalf("failed to load genesis for OP-Stack chain %q (%d): %v", name, ch, err) + } + return genesis case ctx.Bool(DeveloperFlag.Name): Fatalf("Developer chains are ephemeral") }
diff --git go-ethereum/internal/flags/categories.go op-geth/internal/flags/categories.go index d426add55b10b62cc75ba4f294f9f2fd1b4ae06a..d9c2d9894afae2099988e8b2729454d77509ae11 100644 --- go-ethereum/internal/flags/categories.go +++ op-geth/internal/flags/categories.go @@ -32,6 +32,7 @@ NetworkingCategory = "NETWORKING" MinerCategory = "MINER" GasPriceCategory = "GAS PRICE ORACLE" VMCategory = "VIRTUAL MACHINE" + RollupCategory = "ROLLUP NODE" LoggingCategory = "LOGGING AND DEBUGGING" MetricsCategory = "METRICS AND STATS" MiscCategory = "MISC"

List the op-geth and upstream go-ethereum versions.

diff --git go-ethereum/build/ci.go op-geth/build/ci.go index 0d3cdd019d8410cb9184fc509e586341020a08ed..31a87de0d57ce56a6fdb08f52b4d3e187a05585d 100644 --- go-ethereum/build/ci.go +++ op-geth/build/ci.go @@ -244,6 +244,9 @@ if env.Commit != "" { ld = append(ld, "-X", "github.com/ethereum/go-ethereum/internal/version.gitCommit="+env.Commit) ld = append(ld, "-X", "github.com/ethereum/go-ethereum/internal/version.gitDate="+env.Date) } + if env.Tag != "" { + ld = append(ld, "-X", "github.com/ethereum/go-ethereum/params.gitTag="+env.Tag) + } // Strip DWARF on darwin. This used to be required for certain things, // and there is no downside to this, so we just keep doing it. if runtime.GOOS == "darwin" { @@ -757,7 +760,7 @@ switch { case env.Branch == "master": tags = []string{"latest"} case strings.HasPrefix(env.Tag, "v1."): - tags = []string{"stable", fmt.Sprintf("release-1.%d", params.VersionMinor), "v" + params.Version} + tags = []string{"stable", fmt.Sprintf("release-1.%d", params.OPVersionMinor), "v" + params.Version} } // Need to create a mult-arch builder build.MustRunCommand("docker", "buildx", "create", "--use", "--name", "multi-arch-builder", "--platform", *platform) @@ -953,10 +956,10 @@ return wdflag }   func isUnstableBuild(env build.Environment) bool { - if env.Tag != "" { - return false + if env.Tag == "untagged" || env.Tag == "" { + return true } - return true + return false }   type debPackage struct {
diff --git go-ethereum/cmd/geth/misccmd.go op-geth/cmd/geth/misccmd.go index f3530c30fb69fe08024d9f62a1a377d416c684d9..e7fb108ebfb6a065eb1232ced8fcdf1c49eee4c5 100644 --- go-ethereum/cmd/geth/misccmd.go +++ op-geth/cmd/geth/misccmd.go @@ -80,6 +80,7 @@ } if git.Date != "" { fmt.Println("Git Commit Date:", git.Date) } + fmt.Println("Upstream Version:", params.GethVersionWithMeta) fmt.Println("Architecture:", runtime.GOARCH) fmt.Println("Go Version:", runtime.Version()) fmt.Println("Operating System:", runtime.GOOS)
diff --git go-ethereum/params/version.go op-geth/params/version.go index b5c0dff7c0aa2c7b8d3291fcbf8b2e7317f54367..c12857951b19fb517693e69e9299b2c90d2d9ee5 100644 --- go-ethereum/params/version.go +++ op-geth/params/version.go @@ -18,8 +18,11 @@ package params   import ( "fmt" + "regexp" + "strconv" )   +// Version is the version of upstream geth const ( VersionMajor = 1 // Major version component of the current release VersionMinor = 14 // Minor version component of the current release @@ -27,14 +30,58 @@ VersionPatch = 11 // Patch version component of the current release VersionMeta = "stable" // Version metadata to append to the version string )   +// OPVersion is the version of op-geth +var ( + OPVersionMajor = 0 // Major version component of the current release + OPVersionMinor = 1 // Minor version component of the current release + OPVersionPatch = 0 // Patch version component of the current release + OPVersionMeta = "untagged" // Version metadata to append to the version string +) + +// This is set at build-time by the linker when the build is done by build/ci.go. +var gitTag string + +// Override the version variables if the gitTag was set at build time. +var _ = func() (_ string) { + semver := regexp.MustCompile(`^v([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$`) + version := semver.FindStringSubmatch(gitTag) + if version == nil { + return + } + if version[4] == "" { + version[4] = "stable" + } + OPVersionMajor, _ = strconv.Atoi(version[1]) + OPVersionMinor, _ = strconv.Atoi(version[2]) + OPVersionPatch, _ = strconv.Atoi(version[3]) + OPVersionMeta = version[4] + return +}() + // Version holds the textual version string. var Version = func() string { - return fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch) + if OPVersionMeta == "untagged" { + return OPVersionMeta + } + return fmt.Sprintf("%d.%d.%d", OPVersionMajor, OPVersionMinor, OPVersionPatch) }()   // VersionWithMeta holds the textual version string including the metadata. var VersionWithMeta = func() string { - v := Version + if OPVersionMeta != "untagged" { + return Version + "-" + OPVersionMeta + } + return Version +}() + +// GethVersion holds the textual geth version string. +var GethVersion = func() string { + return fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch) +}() + +// GethVersionWithMeta holds the textual geth version string including the metadata. +var GethVersionWithMeta = func() string { + v := GethVersion if VersionMeta != "" { v += "-" + VersionMeta } @@ -46,8 +93,8 @@ // "1.8.11-dea1ce05" for stable releases, or "1.8.13-unstable-21c059b6" for unstable // releases. func ArchiveVersion(gitCommit string) string { vsn := Version - if VersionMeta != "stable" { - vsn += "-" + VersionMeta + if OPVersionMeta != "stable" { + vsn += "-" + OPVersionMeta } if len(gitCommit) >= 8 { vsn += "-" + gitCommit[:8] @@ -60,7 +107,7 @@ vsn := VersionWithMeta if len(gitCommit) >= 8 { vsn += "-" + gitCommit[:8] } - if (VersionMeta != "stable") && (gitDate != "") { + if (OPVersionMeta != "stable") && (gitDate != "") { vsn += "-" + gitDate } return vsn
diff --git go-ethereum/eth/ethconfig/config.go op-geth/eth/ethconfig/config.go index c781a639408ac94fff8a02174ca048e8a4b59e1e..29eca7bb283e461816305242bc20c964a19fbc1a 100644 --- go-ethereum/eth/ethconfig/config.go +++ op-geth/eth/ethconfig/config.go @@ -38,12 +38,13 @@ )   // FullNodeGPO contains default gasprice oracle settings for full node. var FullNodeGPO = gasprice.Config{ - Blocks: 20, - Percentile: 60, - MaxHeaderHistory: 1024, - MaxBlockHistory: 1024, - MaxPrice: gasprice.DefaultMaxPrice, - IgnorePrice: gasprice.DefaultIgnorePrice, + Blocks: 20, + Percentile: 60, + MaxHeaderHistory: 1024, + MaxBlockHistory: 1024, + MaxPrice: gasprice.DefaultMaxPrice, + IgnorePrice: gasprice.DefaultIgnorePrice, + MinSuggestedPriorityFee: gasprice.DefaultMinSuggestedPriorityFee, }   // Defaults contains default settings for use on the Ethereum main net. @@ -156,12 +157,42 @@ OverrideCancun *uint64 `toml:",omitempty"`   // OverrideVerkle (TODO: remove after the fork) OverrideVerkle *uint64 `toml:",omitempty"` + + OverrideOptimismCanyon *uint64 `toml:",omitempty"` + + OverrideOptimismEcotone *uint64 `toml:",omitempty"` + + OverrideOptimismFjord *uint64 `toml:",omitempty"` + + OverrideOptimismGranite *uint64 `toml:",omitempty"` + + OverrideOptimismHolocene *uint64 `toml:",omitempty"` + + OverrideOptimismInterop *uint64 `toml:",omitempty"` + + // ApplySuperchainUpgrades requests the node to load chain-configuration from the superchain-registry. + ApplySuperchainUpgrades bool `toml:",omitempty"` + + RollupSequencerHTTP string + RollupSequencerTxConditionalEnabled bool + RollupSequencerTxConditionalCostRateLimit int + RollupHistoricalRPC string + RollupHistoricalRPCTimeout time.Duration + RollupDisableTxPoolGossip bool + RollupDisableTxPoolAdmission bool + RollupHaltOnIncompatibleProtocolVersion string + + InteropMessageRPC string `toml:",omitempty"` + InteropMempoolFiltering bool `toml:",omitempty"` }   // CreateConsensusEngine creates a consensus engine for the given chain config. // Clique is allowed for now to live standalone, but ethash is forbidden and can // only exist on already merged networks. func CreateConsensusEngine(config *params.ChainConfig, db ethdb.Database) (consensus.Engine, error) { + if config.Optimism != nil { + return beacon.New(&beacon.OpLegacy{}), nil + } // Geth v1.14.0 dropped support for non-merged networks in any consensus // mode. If such a network is requested, reject startup. if !config.TerminalTotalDifficultyPassed {
diff --git go-ethereum/eth/handler.go op-geth/eth/handler.go index d5117584c0014e24f8f6267054f33fff752c8dad..7e1a9f2191f6d94f67829bb89b4c0814fa7201bc 100644 --- go-ethereum/eth/handler.go +++ op-geth/eth/handler.go @@ -93,6 +93,7 @@ Sync downloader.SyncMode // Whether to snap or full sync BloomCache uint64 // Megabytes to alloc for snap sync bloom EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges + NoTxGossip bool // Disable P2P transaction gossip }   type handler struct { @@ -107,6 +108,8 @@ database ethdb.Database txpool txPool chain *core.BlockChain maxPeers int + + noTxGossip bool   downloader *downloader.Downloader txFetcher *fetcher.TxFetcher @@ -140,6 +143,7 @@ forkFilter: forkid.NewFilter(config.Chain), eventMux: config.EventMux, database: config.Database, txpool: config.TxPool, + noTxGossip: config.NoTxGossip, chain: config.Chain, peers: newPeerSet(), requiredBlocks: config.RequiredBlocks, @@ -166,9 +170,10 @@ log.Warn("Switch sync mode from full sync to snap sync", "reason", "head state missing") } } else { head := h.chain.CurrentBlock() - if head.Number.Uint64() > 0 && h.chain.HasState(head.Root) { + if head.Number.Uint64() > 0 && h.chain.HasState(head.Root) && (!config.Chain.Config().IsOptimism() || head.Number.Cmp(config.Chain.Config().BedrockBlock) != 0) { // Print warning log if database is not empty to run snap sync. - log.Warn("Switch sync mode from snap sync to full sync", "reason", "snap sync complete") + // For OP chains, snap sync from bedrock block is allowed. + log.Warn("Switch sync mode from snap sync to full sync") } else { // If snap sync was requested and our database is empty, grant it h.snapSync.Store(true) @@ -179,8 +184,14 @@ // If snap sync is requested but snapshots are disabled, fail loudly if h.snapSync.Load() && config.Chain.Snapshots() == nil { return nil, errors.New("snap sync not supported with snapshots disabled") } + // if the chainID is set, pass it to the downloader for use in sync + // this might not be set in tests + var chainID uint64 + if cid := h.chain.Config().ChainID; cid != nil { + chainID = cid.Uint64() + } // Construct the downloader (long sync) - h.downloader = downloader.New(config.Database, h.eventMux, h.chain, h.removePeer, h.enableSyncedFeatures) + h.downloader = downloader.New(config.Database, h.eventMux, h.chain, h.removePeer, h.enableSyncedFeatures, chainID)   fetchTx := func(peer string, hashes []common.Hash) error { p := h.peers.peer(peer)
diff --git go-ethereum/eth/handler_eth.go op-geth/eth/handler_eth.go index b2cd52a22105ac700782008678229d33853e4004..be0abc1b60855ad2c941f829515ebf8e6b28390f 100644 --- go-ethereum/eth/handler_eth.go +++ op-geth/eth/handler_eth.go @@ -20,6 +20,7 @@ import ( "errors" "fmt"   + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/protocols/eth" @@ -31,7 +32,20 @@ // packets that are sent as replies or broadcasts. type ethHandler handler   func (h *ethHandler) Chain() *core.BlockChain { return h.chain } -func (h *ethHandler) TxPool() eth.TxPool { return h.txpool } + +// NilPool satisfies the TxPool interface but does not return any tx in the +// pool. It is used to disable transaction gossip. +type NilPool struct{} + +// NilPool Get always returns nil +func (n NilPool) Get(hash common.Hash) *types.Transaction { return nil } + +func (h *ethHandler) TxPool() eth.TxPool { + if h.noTxGossip { + return &NilPool{} + } + return h.txpool +}   // RunPeer is invoked when a peer joins on the `eth` protocol. func (h *ethHandler) RunPeer(peer *eth.Peer, hand eth.Handler) error { @@ -49,6 +63,9 @@ // AcceptTxs retrieves whether transaction processing is enabled on the node // or if inbound transactions should simply be dropped. func (h *ethHandler) AcceptTxs() bool { + if h.noTxGossip { + return false + } return h.synced.Load() }
diff --git go-ethereum/core/blockchain.go op-geth/core/blockchain.go index f7c921fe64fe8d4444f399d2ea19725539d956c2..b9fd783e625a466bb766df8bb35e694037d130ab 100644 --- go-ethereum/core/blockchain.go +++ op-geth/core/blockchain.go @@ -286,6 +286,10 @@ } log.Info(strings.Repeat("-", 153)) log.Info("")   + if chainConfig.IsOptimism() && chainConfig.RegolithTime == nil { + log.Warn("Optimism RegolithTime has not been set") + } + bc := &BlockChain{ chainConfig: chainConfig, cacheConfig: cacheConfig, @@ -406,10 +410,6 @@ return nil, err } } } - // The first thing the node will do is reconstruct the verification data for - // the head block (ethash cache or clique voting snapshot). Might as well do - // it in advance. - bc.engine.VerifyHeader(bc, bc.CurrentHeader())   if bc.logger != nil && bc.logger.OnBlockchainInit != nil { bc.logger.OnBlockchainInit(chainConfig) @@ -462,6 +462,11 @@ bc.SetHead(compat.RewindToBlock) } rawdb.WriteChainConfig(db, genesisHash, chainConfig) } + + // The first thing the node will do is reconstruct the verification data for + // the head block (ethash cache or clique voting snapshot). Might as well do + // it in advance. + bc.engine.VerifyHeader(bc, bc.CurrentHeader())   // Start tx indexer if it's enabled. if txLookupLimit != nil {
diff --git go-ethereum/eth/catalyst/superchain.go op-geth/eth/catalyst/superchain.go new file mode 100644 index 0000000000000000000000000000000000000000..503519889f4f050ad0aa739525022044fa842bc6 --- /dev/null +++ op-geth/eth/catalyst/superchain.go @@ -0,0 +1,66 @@ +package catalyst + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/params" +) + +var ( + requiredProtocolDeltaGauge = metrics.NewRegisteredGauge("superchain/required/delta", nil) + recommendedProtocolDeltaGauge = metrics.NewRegisteredGauge("superchain/recommended/delta", nil) +) + +type SuperchainSignal struct { + Recommended params.ProtocolVersion `json:"recommended"` + Required params.ProtocolVersion `json:"required"` +} + +func (api *ConsensusAPI) SignalSuperchainV1(signal *SuperchainSignal) (params.ProtocolVersion, error) { + if signal == nil { + log.Info("Received empty superchain version signal", "local", params.OPStackSupport) + return params.OPStackSupport, nil + } + // update metrics and log any warnings/info + requiredProtocolDeltaGauge.Update(int64(params.OPStackSupport.Compare(signal.Required))) + recommendedProtocolDeltaGauge.Update(int64(params.OPStackSupport.Compare(signal.Recommended))) + logger := log.New("local", params.OPStackSupport, "required", signal.Required, "recommended", signal.Recommended) + LogProtocolVersionSupport(logger, params.OPStackSupport, signal.Recommended, "recommended") + LogProtocolVersionSupport(logger, params.OPStackSupport, signal.Required, "required") + + if err := api.eth.HandleRequiredProtocolVersion(signal.Required); err != nil { + log.Error("Failed to handle required protocol version", "err", err, "required", signal.Required) + return params.OPStackSupport, err + } + + return params.OPStackSupport, nil +} + +func LogProtocolVersionSupport(logger log.Logger, local, other params.ProtocolVersion, name string) { + switch local.Compare(other) { + case params.AheadMajor: + logger.Info(fmt.Sprintf("Ahead with major %s protocol version change", name)) + case params.AheadMinor, params.AheadPatch, params.AheadPrerelease: + logger.Debug(fmt.Sprintf("Ahead with compatible %s protocol version change", name)) + case params.Matching: + logger.Debug(fmt.Sprintf("Latest %s protocol version is supported", name)) + case params.OutdatedMajor: + logger.Error(fmt.Sprintf("Outdated with major %s protocol change", name)) + case params.OutdatedMinor: + logger.Warn(fmt.Sprintf("Outdated with minor backward-compatible %s protocol change", name)) + case params.OutdatedPatch: + logger.Info(fmt.Sprintf("Outdated with support backward-compatible %s protocol change", name)) + case params.OutdatedPrerelease: + logger.Debug(fmt.Sprintf("New %s protocol pre-release is available", name)) + case params.DiffBuild: + logger.Debug(fmt.Sprintf("Ignoring %s protocolversion signal, local build is different", name)) + case params.DiffVersionType: + logger.Warn(fmt.Sprintf("Failed to recognize %s protocol version signal version-type", name)) + case params.EmptyVersion: + logger.Debug(fmt.Sprintf("No %s protocol version available to check", name)) + case params.InvalidVersion: + logger.Warn(fmt.Sprintf("Invalid protocol version comparison with %s", name)) + } +}

Snap-sync does not serve unprefixed code by default.

diff --git go-ethereum/core/blockchain_reader.go op-geth/core/blockchain_reader.go index 6b8dffdcdc63eb1f085ef518e06c09a6eabe0179..4f3be640d763f57a14ead4c287214ef979be2413 100644 --- go-ethereum/core/blockchain_reader.go +++ op-geth/core/blockchain_reader.go @@ -346,6 +346,14 @@ // in Verkle scheme. Fix it once snap-sync is supported for Verkle. return bc.statedb.ContractCodeWithPrefix(common.Address{}, hash) }   +// ContractCode retrieves a blob of data associated with a contract hash +// either from ephemeral in-memory cache, or from persistent storage. +// This is a legacy-method, replaced by ContractCodeWithPrefix, +// but required for old databases to serve snap-sync. +func (bc *BlockChain) ContractCode(hash common.Hash) ([]byte, error) { + return bc.StateCache().ContractCode(common.Address{}, hash) +} + // State returns a new mutable state based on the current HEAD block. func (bc *BlockChain) State() (*state.StateDB, error) { return bc.StateAt(bc.CurrentBlock().Root)
diff --git go-ethereum/eth/protocols/snap/handler.go op-geth/eth/protocols/snap/handler.go index a6c60bc0757f732ab4668867128e33a73385189f..6bc3e6e42a10ea2ae7263866135c62457cb5b57e 100644 --- go-ethereum/eth/protocols/snap/handler.go +++ op-geth/eth/protocols/snap/handler.go @@ -456,7 +456,7 @@ if hash == types.EmptyCodeHash { // Peers should not request the empty code, but if they do, at // least sent them back a correct response without db lookups codes = append(codes, []byte{}) - } else if blob, err := chain.ContractCodeWithPrefix(hash); err == nil { + } else if blob, err := chain.ContractCode(hash); err == nil { codes = append(codes, blob) bytes += uint64(len(blob)) }

Snap-sync has access to trusted Deposit Transaction Nonce Data.

diff --git go-ethereum/eth/downloader/downloader.go op-geth/eth/downloader/downloader.go index d147414859f2d9a9ad1031cdeac52edef537618a..fc79401dc1e9061451755e46e9cf20ca358fd06e 100644 --- go-ethereum/eth/downloader/downloader.go +++ op-geth/eth/downloader/downloader.go @@ -139,10 +139,18 @@ // Progress reporting metrics syncStartBlock uint64 // Head snap block when Geth was started syncStartTime time.Time // Time instance when chain sync started syncLogTime time.Time // Time instance when status was last reported + + // Chain ID for downloaders to reference + chainID uint64 }   // BlockChain encapsulates functions required to sync a (full or snap) blockchain. type BlockChain interface { + // Config returns the chain configuration. + // OP-Stack diff, to adjust withdrawal-hash verification. + // Usage of ths in the Downloader is discouraged. + Config() *params.ChainConfig + // HasHeader verifies a header's presence in the local chain. HasHeader(common.Hash, uint64) bool   @@ -194,11 +202,11 @@ TrieDB() *triedb.Database }   // New creates a new downloader to fetch hashes and blocks from remote peers. -func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, dropPeer peerDropFn, success func()) *Downloader { +func New(stateDb ethdb.Database, mux *event.TypeMux, chain BlockChain, dropPeer peerDropFn, success func(), chainID uint64) *Downloader { dl := &Downloader{ stateDB: stateDb, mux: mux, - queue: newQueue(blockCacheMaxItems, blockCacheInitialItems), + queue: newQueue(chain.Config(), blockCacheMaxItems, blockCacheInitialItems), peers: newPeerSet(), blockchain: chain, dropPeer: dropPeer, @@ -207,6 +215,7 @@ quitCh: make(chan struct{}), SnapSyncer: snap.NewSyncer(stateDb, chain.TrieDB().Scheme()), stateSyncStart: make(chan *stateSync), syncStartBlock: chain.CurrentSnapBlock().Number.Uint64(), + chainID: chainID, } // Create the post-merge skeleton syncer and start the process dl.skeleton = newSkeleton(stateDb, dl.peers, dropPeer, newBeaconBackfiller(dl, success)) @@ -982,7 +991,7 @@ blocks := make([]*types.Block, len(results)) receipts := make([]types.Receipts, len(results)) for i, result := range results { blocks[i] = types.NewBlockWithHeader(result.Header).WithBody(result.body()) - receipts[i] = result.Receipts + receipts[i] = correctReceipts(result.Receipts, result.Transactions, blocks[i].NumberU64(), d.chainID) } if index, err := d.blockchain.InsertReceiptChain(blocks, receipts, d.ancientLimit); err != nil { log.Debug("Downloaded item processing failed", "number", results[index].Header.Number, "hash", results[index].Header.Hash(), "err", err)
diff --git go-ethereum/eth/downloader/receiptreference.go op-geth/eth/downloader/receiptreference.go new file mode 100644 index 0000000000000000000000000000000000000000..b4c5623ddc9033ac6c03e48710f44e5c00cf1d94 --- /dev/null +++ op-geth/eth/downloader/receiptreference.go @@ -0,0 +1,144 @@ +package downloader + +import ( + "bytes" + "embed" + "encoding/gob" + "fmt" + "path" + "strings" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// userDepositNonces is a struct to hold the reference data for user deposits +// The reference data is used to correct the deposit nonce in the receipts +type userDepositNonces struct { + ChainID uint64 + First uint64 + Last uint64 // non inclusive + Results map[uint64][]uint64 +} + +var ( + systemAddress = common.HexToAddress("0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001") + receiptReferencePath = "userDepositData" + //go:embed userDepositData/*.gob + receiptReference embed.FS + userDepositNoncesAlreadySearched = map[uint64]bool{} + userDepositNoncesReference = map[uint64]userDepositNonces{} +) + +// lazy load the reference data for the requested chain +// if this chain data was already requested, returns early +func initReceiptReferences(chainID uint64) { + // if already loaded, return + if userDepositNoncesAlreadySearched[chainID] { + return + } + // look for a file prefixed by the chainID + fPrefix := fmt.Sprintf("%d.", chainID) + files, err := receiptReference.ReadDir(receiptReferencePath) + if err != nil { + log.Warn("Receipt Correction: Failed to read reference directory", "err", err) + return + } + // mark as loaded so we don't try again, even if no files match + userDepositNoncesAlreadySearched[chainID] = true + for _, file := range files { + // skip files which don't match the prefix + if !strings.HasPrefix(file.Name(), fPrefix) { + continue + } + fpath := path.Join(receiptReferencePath, file.Name()) + bs, err := receiptReference.ReadFile(fpath) + if err != nil { + log.Warn("Receipt Correction: Failed to read reference data", "err", err) + continue + } + udns := userDepositNonces{} + err = gob.NewDecoder(bytes.NewReader(bs)).Decode(&udns) + if err != nil { + log.Warn("Receipt Correction: Failed to decode reference data", "err", err) + continue + } + userDepositNoncesReference[udns.ChainID] = udns + return + } +} + +// correctReceipts corrects the deposit nonce in the receipts using the reference data +// prior to Canyon Hard Fork, DepositNonces were not cryptographically verifiable. +// As a consequence, the deposit nonces found during Snap Sync may be incorrect. +// This function inspects transaction data for user deposits, and if it is found to be incorrect, it is corrected. +// The data used to correct the deposit nonce is stored in the userDepositData directory, +// and was generated with the receipt reference tool in the optimism monorepo. +func correctReceipts(receipts types.Receipts, transactions types.Transactions, blockNumber uint64, chainID uint64) types.Receipts { + initReceiptReferences(chainID) + // if there is no data even after initialization, return the receipts as is + depositNoncesForChain, ok := userDepositNoncesReference[chainID] + if !ok { + log.Trace("Receipt Correction: No data source for chain", "chainID", chainID) + return receipts + } + + // check that the block number being examined is within the range of the reference data + if blockNumber < depositNoncesForChain.First || blockNumber > depositNoncesForChain.Last { + log.Trace("Receipt Correction: Block is out of range for receipt reference", + "blockNumber", blockNumber, + "start", depositNoncesForChain.First, + "end", depositNoncesForChain.Last) + return receipts + } + + // get the block nonces + blockNonces, ok := depositNoncesForChain.Results[blockNumber] + if !ok { + log.Trace("Receipt Correction: Block does not contain user deposits", "blockNumber", blockNumber) + return receipts + } + + // iterate through the receipts and transactions to correct the deposit nonce + // user deposits should always be at the front of the block, but we will check all transactions to be sure + udCount := 0 + for i := 0; i < len(receipts); i++ { + r := receipts[i] + tx := transactions[i] + from, err := types.Sender(types.LatestSignerForChainID(tx.ChainId()), tx) + if err != nil { + log.Warn("Receipt Correction: Failed to determine sender", "err", err) + continue + } + // break as soon as a non deposit is found + if r.Type != types.DepositTxType { + break + } + // ignore any transactions from the system address + if from != systemAddress { + // prevent index out of range (indicates a problem with the reference data or the block data) + if udCount >= len(blockNonces) { + log.Warn("Receipt Correction: More user deposits in block than included in reference data", "in_reference", len(blockNonces)) + break + } + nonce := blockNonces[udCount] + udCount++ + log.Trace("Receipt Correction: User Deposit detected", "address", from, "nonce", nonce) + if nonce != *r.DepositNonce { + // correct the deposit nonce + // warn because this should not happen unless the data was modified by corruption or a malicious peer + // by correcting the nonce, the entire block is still valid for use + log.Warn("Receipt Correction: Corrected deposit nonce", "nonce", *r.DepositNonce, "corrected", nonce) + r.DepositNonce = &nonce + } + } + } + // check for unused reference data (indicates a problem with the reference data or the block data) + if udCount < len(blockNonces) { + log.Warn("Receipt Correction: More user deposits in reference data than found in block", "in_reference", len(blockNonces), "in_block", udCount) + } + + log.Trace("Receipt Correction: Completed", "blockNumber", blockNumber, "userDeposits", udCount, "receipts", len(receipts), "transactions", len(transactions)) + return receipts +}

Fix discv5 option to allow discv5 to be an active source for node-discovery.

diff --git go-ethereum/p2p/server.go op-geth/p2p/server.go index 172f0667eb13240aa2f43a12d119c1182e6ba402..0d1e9bd75e60cf1f8a95724987fc2c9745b19eba 100644 --- go-ethereum/p2p/server.go +++ op-geth/p2p/server.go @@ -591,6 +591,7 @@ srv.discv5, err = discover.ListenV5(sconn, srv.localnode, cfg) if err != nil { return err } + srv.discmix.AddSource(srv.discv5.RandomNodes()) }   // Add protocol-specific discovery sources.

Discovery bootnode addresses.

diff --git go-ethereum/params/bootnodes.go op-geth/params/bootnodes.go index 124765313a81cc6df296e117af346f85dcf3114f..f4523220659dbac715083f66203b1cda1afb9be8 100644 --- go-ethereum/params/bootnodes.go +++ op-geth/params/bootnodes.go @@ -70,6 +70,29 @@ "enr:-LK4QA8FfhaAjlb_BXsXxSfiysR7R52Nhi9JBt4F8SPssu8hdE1BXQQEtVDC3qStCW60LSO7hEsVHv5zm8_6Vnjhcn0Bh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhAN4aBKJc2VjcDI1NmsxoQJerDhsJ-KxZ8sHySMOCmTO6sHM3iCFQ6VMvLTe948MyYN0Y3CCI4yDdWRwgiOM", // 3.120.104.18 | aws-eu-central-1-frankfurt "enr:-LK4QKWrXTpV9T78hNG6s8AM6IO4XH9kFT91uZtFg1GcsJ6dKovDOr1jtAAFPnS2lvNltkOGA9k29BUN7lFh_sjuc9QBh2F0dG5ldHOIAAAAAAAAAACEZXRoMpC1MD8qAAAAAP__________gmlkgnY0gmlwhANAdd-Jc2VjcDI1NmsxoQLQa6ai7y9PMN5hpLe5HmiJSlYzMuzP7ZhwRiwHvqNXdoN0Y3CCI4yDdWRwgiOM", // 3.64.117.223 | aws-eu-central-1-frankfurt} }   +var V5OPBootnodes = []string{ + // OP Labs + "enode://ca2774c3c401325850b2477fd7d0f27911efbf79b1e8b335066516e2bd8c4c9e0ba9696a94b1cb030a88eac582305ff55e905e64fb77fe0edcd70a4e5296d3ec@34.65.175.185:30305", + "enode://dd751a9ef8912be1bfa7a5e34e2c3785cc5253110bd929f385e07ba7ac19929fb0e0c5d93f77827291f4da02b2232240fbc47ea7ce04c46e333e452f8656b667@34.65.107.0:30305", + "enode://c5d289b56a77b6a2342ca29956dfd07aadf45364dde8ab20d1dc4efd4d1bc6b4655d902501daea308f4d8950737a4e93a4dfedd17b49cd5760ffd127837ca965@34.65.202.239:30305", + // Base + "enode://87a32fd13bd596b2ffca97020e31aef4ddcc1bbd4b95bb633d16c1329f654f34049ed240a36b449fda5e5225d70fe40bc667f53c304b71f8e68fc9d448690b51@3.231.138.188:30301", + "enode://ca21ea8f176adb2e229ce2d700830c844af0ea941a1d8152a9513b966fe525e809c3a6c73a2c18a12b74ed6ec4380edf91662778fe0b79f6a591236e49e176f9@184.72.129.189:30301", + "enode://acf4507a211ba7c1e52cdf4eef62cdc3c32e7c9c47998954f7ba024026f9a6b2150cd3f0b734d9c78e507ab70d59ba61dfe5c45e1078c7ad0775fb251d7735a2@3.220.145.177:30301", + "enode://8a5a5006159bf079d06a04e5eceab2a1ce6e0f721875b2a9c96905336219dbe14203d38f70f3754686a6324f786c2f9852d8c0dd3adac2d080f4db35efc678c5@3.231.11.52:30301", + "enode://cdadbe835308ad3557f9a1de8db411da1a260a98f8421d62da90e71da66e55e98aaa8e90aa7ce01b408a54e4bd2253d701218081ded3dbe5efbbc7b41d7cef79@54.198.153.150:30301", +} + +var V5OPTestnetBootnodes = []string{ + // OP Labs + "enode://2bd2e657bb3c8efffb8ff6db9071d9eb7be70d7c6d7d980ff80fc93b2629675c5f750bc0a5ef27cd788c2e491b8795a7e9a4a6e72178c14acc6753c0e5d77ae4@34.65.205.244:30305", + "enode://db8e1cab24624cc62fc35dbb9e481b88a9ef0116114cd6e41034c55b5b4f18755983819252333509bd8e25f6b12aadd6465710cd2e956558faf17672cce7551f@34.65.173.88:30305", + "enode://bfda2e0110cfd0f4c9f7aa5bf5ec66e6bd18f71a2db028d36b8bf8b0d6fdb03125c1606a6017b31311d96a36f5ef7e1ad11604d7a166745e6075a715dfa67f8a@34.65.229.245:30305", + // Base + "enode://548f715f3fc388a7c917ba644a2f16270f1ede48a5d88a4d14ea287cc916068363f3092e39936f1a3e7885198bef0e5af951f1d7b1041ce8ba4010917777e71f@18.210.176.114:30301", + "enode://6f10052847a966a725c9f4adf6716f9141155b99a0fb487fea3f51498f4c2a2cb8d534e680ee678f9447db85b93ff7c74562762c3714783a7233ac448603b25f@107.21.251.55:30301", +} + const dnsPrefix = "enrtree://AKA3AM6LPBYEUDMVNU3BSVQJ5AD45Y7YPOHJLEF6W26QOE4VTUDPE@"   // KnownDNSNetwork returns the address of a public DNS-based node list for the given
diff --git go-ethereum/eth/ethconfig/gen_config.go op-geth/eth/ethconfig/gen_config.go index d96ba0ccb734bf629813989f8723b2d96998c657..72f0e1dc73b552c72b4e005162352a0d290f976d 100644 --- go-ethereum/eth/ethconfig/gen_config.go +++ op-geth/eth/ethconfig/gen_config.go @@ -17,41 +17,58 @@ // MarshalTOML marshals as TOML. func (c Config) MarshalTOML() (interface{}, error) { type Config struct { - Genesis *core.Genesis `toml:",omitempty"` - NetworkId uint64 - SyncMode downloader.SyncMode - EthDiscoveryURLs []string - SnapDiscoveryURLs []string - NoPruning bool - NoPrefetch bool - TxLookupLimit uint64 `toml:",omitempty"` - TransactionHistory uint64 `toml:",omitempty"` - StateHistory uint64 `toml:",omitempty"` - StateScheme string `toml:",omitempty"` - RequiredBlocks map[uint64]common.Hash `toml:"-"` - SkipBcVersionCheck bool `toml:"-"` - DatabaseHandles int `toml:"-"` - DatabaseCache int - DatabaseFreezer string - TrieCleanCache int - TrieDirtyCache int - TrieTimeout time.Duration - SnapshotCache int - Preimages bool - FilterLogCacheSize int - Miner miner.Config - TxPool legacypool.Config - BlobPool blobpool.Config - GPO gasprice.Config - EnablePreimageRecording bool - VMTrace string - VMTraceJsonConfig string - DocRoot string `toml:"-"` - RPCGasCap uint64 - RPCEVMTimeout time.Duration - RPCTxFeeCap float64 - OverrideCancun *uint64 `toml:",omitempty"` - OverrideVerkle *uint64 `toml:",omitempty"` + Genesis *core.Genesis `toml:",omitempty"` + NetworkId uint64 + SyncMode downloader.SyncMode + EthDiscoveryURLs []string + SnapDiscoveryURLs []string + NoPruning bool + NoPrefetch bool + TxLookupLimit uint64 `toml:",omitempty"` + TransactionHistory uint64 `toml:",omitempty"` + StateHistory uint64 `toml:",omitempty"` + StateScheme string `toml:",omitempty"` + RequiredBlocks map[uint64]common.Hash `toml:"-"` + SkipBcVersionCheck bool `toml:"-"` + DatabaseHandles int `toml:"-"` + DatabaseCache int + DatabaseFreezer string + TrieCleanCache int + TrieDirtyCache int + TrieTimeout time.Duration + SnapshotCache int + Preimages bool + FilterLogCacheSize int + Miner miner.Config + TxPool legacypool.Config + BlobPool blobpool.Config + GPO gasprice.Config + EnablePreimageRecording bool + VMTrace string + VMTraceJsonConfig string + DocRoot string `toml:"-"` + RPCGasCap uint64 + RPCEVMTimeout time.Duration + RPCTxFeeCap float64 + OverrideCancun *uint64 `toml:",omitempty"` + OverrideVerkle *uint64 `toml:",omitempty"` + OverrideOptimismCanyon *uint64 `toml:",omitempty"` + OverrideOptimismEcotone *uint64 `toml:",omitempty"` + OverrideOptimismFjord *uint64 `toml:",omitempty"` + OverrideOptimismGranite *uint64 `toml:",omitempty"` + OverrideOptimismHolocene *uint64 `toml:",omitempty"` + OverrideOptimismInterop *uint64 `toml:",omitempty"` + ApplySuperchainUpgrades bool `toml:",omitempty"` + RollupSequencerHTTP string + RollupSequencerTxConditionalEnabled bool + RollupSequencerTxConditionalCostRateLimit int + RollupHistoricalRPC string + RollupHistoricalRPCTimeout time.Duration + RollupDisableTxPoolGossip bool + RollupDisableTxPoolAdmission bool + RollupHaltOnIncompatibleProtocolVersion string + InteropMessageRPC string `toml:",omitempty"` + InteropMempoolFiltering bool `toml:",omitempty"` } var enc Config enc.Genesis = c.Genesis @@ -89,47 +106,81 @@ enc.RPCEVMTimeout = c.RPCEVMTimeout enc.RPCTxFeeCap = c.RPCTxFeeCap enc.OverrideCancun = c.OverrideCancun enc.OverrideVerkle = c.OverrideVerkle + enc.OverrideOptimismCanyon = c.OverrideOptimismCanyon + enc.OverrideOptimismEcotone = c.OverrideOptimismEcotone + enc.OverrideOptimismFjord = c.OverrideOptimismFjord + enc.OverrideOptimismGranite = c.OverrideOptimismGranite + enc.OverrideOptimismHolocene = c.OverrideOptimismHolocene + enc.OverrideOptimismInterop = c.OverrideOptimismInterop + enc.ApplySuperchainUpgrades = c.ApplySuperchainUpgrades + enc.RollupSequencerHTTP = c.RollupSequencerHTTP + enc.RollupSequencerTxConditionalEnabled = c.RollupSequencerTxConditionalEnabled + enc.RollupSequencerTxConditionalCostRateLimit = c.RollupSequencerTxConditionalCostRateLimit + enc.RollupHistoricalRPC = c.RollupHistoricalRPC + enc.RollupHistoricalRPCTimeout = c.RollupHistoricalRPCTimeout + enc.RollupDisableTxPoolGossip = c.RollupDisableTxPoolGossip + enc.RollupDisableTxPoolAdmission = c.RollupDisableTxPoolAdmission + enc.RollupHaltOnIncompatibleProtocolVersion = c.RollupHaltOnIncompatibleProtocolVersion + enc.InteropMessageRPC = c.InteropMessageRPC + enc.InteropMempoolFiltering = c.InteropMempoolFiltering return &enc, nil }   // UnmarshalTOML unmarshals from TOML. func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { type Config struct { - Genesis *core.Genesis `toml:",omitempty"` - NetworkId *uint64 - SyncMode *downloader.SyncMode - EthDiscoveryURLs []string - SnapDiscoveryURLs []string - NoPruning *bool - NoPrefetch *bool - TxLookupLimit *uint64 `toml:",omitempty"` - TransactionHistory *uint64 `toml:",omitempty"` - StateHistory *uint64 `toml:",omitempty"` - StateScheme *string `toml:",omitempty"` - RequiredBlocks map[uint64]common.Hash `toml:"-"` - SkipBcVersionCheck *bool `toml:"-"` - DatabaseHandles *int `toml:"-"` - DatabaseCache *int - DatabaseFreezer *string - TrieCleanCache *int - TrieDirtyCache *int - TrieTimeout *time.Duration - SnapshotCache *int - Preimages *bool - FilterLogCacheSize *int - Miner *miner.Config - TxPool *legacypool.Config - BlobPool *blobpool.Config - GPO *gasprice.Config - EnablePreimageRecording *bool - VMTrace *string - VMTraceJsonConfig *string - DocRoot *string `toml:"-"` - RPCGasCap *uint64 - RPCEVMTimeout *time.Duration - RPCTxFeeCap *float64 - OverrideCancun *uint64 `toml:",omitempty"` - OverrideVerkle *uint64 `toml:",omitempty"` + Genesis *core.Genesis `toml:",omitempty"` + NetworkId *uint64 + SyncMode *downloader.SyncMode + EthDiscoveryURLs []string + SnapDiscoveryURLs []string + NoPruning *bool + NoPrefetch *bool + TxLookupLimit *uint64 `toml:",omitempty"` + TransactionHistory *uint64 `toml:",omitempty"` + StateHistory *uint64 `toml:",omitempty"` + StateScheme *string `toml:",omitempty"` + RequiredBlocks map[uint64]common.Hash `toml:"-"` + SkipBcVersionCheck *bool `toml:"-"` + DatabaseHandles *int `toml:"-"` + DatabaseCache *int + DatabaseFreezer *string + TrieCleanCache *int + TrieDirtyCache *int + TrieTimeout *time.Duration + SnapshotCache *int + Preimages *bool + FilterLogCacheSize *int + Miner *miner.Config + TxPool *legacypool.Config + BlobPool *blobpool.Config + GPO *gasprice.Config + EnablePreimageRecording *bool + VMTrace *string + VMTraceJsonConfig *string + DocRoot *string `toml:"-"` + RPCGasCap *uint64 + RPCEVMTimeout *time.Duration + RPCTxFeeCap *float64 + OverrideCancun *uint64 `toml:",omitempty"` + OverrideVerkle *uint64 `toml:",omitempty"` + OverrideOptimismCanyon *uint64 `toml:",omitempty"` + OverrideOptimismEcotone *uint64 `toml:",omitempty"` + OverrideOptimismFjord *uint64 `toml:",omitempty"` + OverrideOptimismGranite *uint64 `toml:",omitempty"` + OverrideOptimismHolocene *uint64 `toml:",omitempty"` + OverrideOptimismInterop *uint64 `toml:",omitempty"` + ApplySuperchainUpgrades *bool `toml:",omitempty"` + RollupSequencerHTTP *string + RollupSequencerTxConditionalEnabled *bool + RollupSequencerTxConditionalCostRateLimit *int + RollupHistoricalRPC *string + RollupHistoricalRPCTimeout *time.Duration + RollupDisableTxPoolGossip *bool + RollupDisableTxPoolAdmission *bool + RollupHaltOnIncompatibleProtocolVersion *string + InteropMessageRPC *string `toml:",omitempty"` + InteropMempoolFiltering *bool `toml:",omitempty"` } var dec Config if err := unmarshal(&dec); err != nil { @@ -239,6 +290,57 @@ c.OverrideCancun = dec.OverrideCancun } if dec.OverrideVerkle != nil { c.OverrideVerkle = dec.OverrideVerkle + } + if dec.OverrideOptimismCanyon != nil { + c.OverrideOptimismCanyon = dec.OverrideOptimismCanyon + } + if dec.OverrideOptimismEcotone != nil { + c.OverrideOptimismEcotone = dec.OverrideOptimismEcotone + } + if dec.OverrideOptimismFjord != nil { + c.OverrideOptimismFjord = dec.OverrideOptimismFjord + } + if dec.OverrideOptimismGranite != nil { + c.OverrideOptimismGranite = dec.OverrideOptimismGranite + } + if dec.OverrideOptimismHolocene != nil { + c.OverrideOptimismHolocene = dec.OverrideOptimismHolocene + } + if dec.OverrideOptimismInterop != nil { + c.OverrideOptimismInterop = dec.OverrideOptimismInterop + } + if dec.ApplySuperchainUpgrades != nil { + c.ApplySuperchainUpgrades = *dec.ApplySuperchainUpgrades + } + if dec.RollupSequencerHTTP != nil { + c.RollupSequencerHTTP = *dec.RollupSequencerHTTP + } + if dec.RollupSequencerTxConditionalEnabled != nil { + c.RollupSequencerTxConditionalEnabled = *dec.RollupSequencerTxConditionalEnabled + } + if dec.RollupSequencerTxConditionalCostRateLimit != nil { + c.RollupSequencerTxConditionalCostRateLimit = *dec.RollupSequencerTxConditionalCostRateLimit + } + if dec.RollupHistoricalRPC != nil { + c.RollupHistoricalRPC = *dec.RollupHistoricalRPC + } + if dec.RollupHistoricalRPCTimeout != nil { + c.RollupHistoricalRPCTimeout = *dec.RollupHistoricalRPCTimeout + } + if dec.RollupDisableTxPoolGossip != nil { + c.RollupDisableTxPoolGossip = *dec.RollupDisableTxPoolGossip + } + if dec.RollupDisableTxPoolAdmission != nil { + c.RollupDisableTxPoolAdmission = *dec.RollupDisableTxPoolAdmission + } + 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 }

From version 1.13.15 to 1.14.0 upstream go-ethereum changed the PathDB journal version, without migration path. For a better experience when updating op-geth, to not roll back any journaled stage changes, op-geth implements backward-compatibility with the old journal version. See upstream Geth PR 28940, and op-geth PR 368 for details.

diff --git go-ethereum/triedb/pathdb/journal.go op-geth/triedb/pathdb/journal.go index 1740ec593511d422432046736218760e39a82e94..4af5c4a03011842da58f51a985bea82d1a29c026 100644 --- go-ethereum/triedb/pathdb/journal.go +++ op-geth/triedb/pathdb/journal.go @@ -47,7 +47,10 @@ // Changelog: // // - Version 0: initial version // - Version 1: storage.Incomplete field is removed -const journalVersion uint64 = 1 +const ( + journalVersion uint64 = 1 + journalVersionV0 uint64 = 0 +)   // journalNode represents a trie node persisted in the journal. type journalNode struct { @@ -75,6 +78,14 @@ Hashes []common.Hash Slots [][]byte }   +// journalStorageV0 represents a journal version 0 storage update. +type journalStorageV0 struct { + Incomplete bool // In V0, to handle self-destructs, the stateDB would abort a storage-diff if it got too large. + Account common.Address + Hashes []common.Hash + Slots [][]byte +} + // loadJournal tries to parse the layer journal from the disk. func (db *Database) loadJournal(diskRoot common.Hash) (layer, error) { journal := rawdb.ReadTrieJournal(db.diskdb) @@ -88,7 +99,7 @@ version, err := r.Uint64() if err != nil { return nil, errMissVersion } - if version != journalVersion { + if version != journalVersionV0 && version != journalVersion { return nil, fmt.Errorf("%w want %d got %d", errUnexpectedVersion, journalVersion, version) } // Secondly, resolve the disk layer root, ensure it's continuous @@ -109,7 +120,7 @@ if err != nil { return nil, err } // Load all the diff layers from the journal - head, err := db.loadDiffLayer(base, r) + head, err := db.loadDiffLayer(base, r, version) if err != nil { return nil, err } @@ -120,7 +131,7 @@ // loadLayers loads a pre-existing state layer backed by a key-value store. func (db *Database) loadLayers() layer { // Retrieve the root node of persistent state. - var root = types.EmptyRootHash + root := types.EmptyRootHash if blob := rawdb.ReadAccountTrieNode(db.diskdb, nil); len(blob) > 0 { root = crypto.Keccak256Hash(blob) } @@ -182,7 +193,7 @@ }   // loadDiffLayer reads the next sections of a layer journal, reconstructing a new // diff and verifying that it can be linked to the requested parent. -func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream) (layer, error) { +func (db *Database) loadDiffLayer(parent layer, r *rlp.Stream, layerJournalVersion uint64) (layer, error) { // Read the next diff journal entry var root common.Hash if err := r.Decode(&root); err != nil { @@ -226,7 +237,25 @@ } for i, addr := range jaccounts.Addresses { accounts[addr] = jaccounts.Accounts[i] } - if err := r.Decode(&jstorages); err != nil { + if layerJournalVersion == journalVersionV0 { + log.Warn("loading legacy v0 journal") + var jstoragesV0 []journalStorageV0 + if err := r.Decode(&jstoragesV0); err != nil { + return nil, fmt.Errorf("load diff storages: %w", err) + } + jstorages = make([]journalStorage, 0, len(jstoragesV0)) + for _, st := range jstoragesV0 { + if st.Incomplete { // Storage diff entries that are complete are compatible with the journal v1 type. + log.Warn("legacy v0 diff layer shows incomplete storage-diff write, cannot recover this, have to drop journal") + return nil, fmt.Errorf("legacy v0 diff layer with incomplete storage-diff: %w", errUnexpectedVersion) + } + jstorages = append(jstorages, journalStorage{ + Account: st.Account, + Hashes: st.Hashes, + Slots: st.Slots, + }) + } + } else if err := r.Decode(&jstorages); err != nil { return nil, fmt.Errorf("load diff storages: %v", err) } for _, entry := range jstorages { @@ -240,7 +269,7 @@ } } storages[entry.Account] = set } - return db.loadDiffLayer(newDiffLayer(parent, root, parent.stateID()+1, block, nodes, triestate.New(accounts, storages)), r) + return db.loadDiffLayer(newDiffLayer(parent, root, parent.stateID()+1, block, nodes, triestate.New(accounts, storages)), r, layerJournalVersion) }   // journal implements the layer interface, marshaling the un-flushed trie nodes

The cannon fault proofs virtual machine does not support the creation of threads. To ensure compatibility, thread creation is avoided when only a single CPU is available.

diff --git go-ethereum/core/state/workers.go op-geth/core/state/workers.go new file mode 100644 index 0000000000000000000000000000000000000000..72b93cd7ad0a3e3ea7566f98cc86090c72dab9e6 --- /dev/null +++ op-geth/core/state/workers.go @@ -0,0 +1,37 @@ +package state + +import ( + "errors" + + "golang.org/x/sync/errgroup" +) + +type workerGroup interface { + Go(func() error) + SetLimit(int) + Wait() error +} + +func newWorkerGroup(singlethreaded bool) workerGroup { + if singlethreaded { + return &inlineWorkerGroup{} + } else { + var grp errgroup.Group + return &grp + } +} + +type inlineWorkerGroup struct { + err error +} + +func (i *inlineWorkerGroup) Go(action func() error) { + i.err = errors.Join(i.err, action()) +} + +func (i *inlineWorkerGroup) SetLimit(_ int) { +} + +func (i *inlineWorkerGroup) Wait() error { + return i.err +}
diff --git go-ethereum/trie/hasher.go op-geth/trie/hasher.go index abf654c709cf1195b23fd1cf169a04cfb275fd3d..9b19abbe6f9d468416222304519cf7af5862af59 100644 --- go-ethereum/trie/hasher.go +++ op-geth/trie/hasher.go @@ -17,6 +17,7 @@ package trie   import ( + "runtime" "sync"   "github.com/ethereum/go-ethereum/crypto" @@ -45,7 +46,7 @@ }   func newHasher(parallel bool) *hasher { h := hasherPool.Get().(*hasher) - h.parallel = parallel + h.parallel = parallel && runtime.NumCPU() > 1 return h }

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.

diff --git go-ethereum/eth/interop.go op-geth/eth/interop.go new file mode 100644 index 0000000000000000000000000000000000000000..f044f20120b40939518f3a8c12b37031a570b7a1 --- /dev/null +++ op-geth/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 +}

Encode the Deposit Tx properties, the L1 costs, and daisy-chain RPC-calls for pre-Bedrock historical data

Pre-Bedrock L1-cost receipt data is loaded from the database if available, and post-Bedrock the L1-cost metadata is hydrated on-the-fly based on the L1 fee information in the corresponding block.

diff --git go-ethereum/core/rawdb/accessors_chain.go op-geth/core/rawdb/accessors_chain.go index c4735c850c02785330703b563fa21a1eea998dcd..4914d961ec60e48ba3194f8f2a2eecfd8909c55d 100644 --- go-ethereum/core/rawdb/accessors_chain.go +++ op-geth/core/rawdb/accessors_chain.go @@ -625,7 +625,6 @@ log.Error("Missing body but have receipt", "hash", hash, "number", number) return nil } header := ReadHeader(db, hash, number) - var baseFee *big.Int if header == nil { baseFee = big.NewInt(0) @@ -675,6 +674,15 @@ type storedReceiptRLP struct { PostStateOrStatus []byte CumulativeGasUsed uint64 Logs []*types.Log + + // Remaining fields are declared to allow the receipt RLP to be parsed without errors. + // However, they must not be used as they may not be populated correctly due to multiple receipt formats + // being combined into a single list of optional fields which can be mistaken for each other. + // DepositNonce (*uint64) from Regolith deposit tx receipts will be parsed into L1GasUsed + L1GasUsed *big.Int `rlp:"optional"` // OVM legacy + L1GasPrice *big.Int `rlp:"optional"` // OVM legacy + L1Fee *big.Int `rlp:"optional"` // OVM legacy + FeeScalar string `rlp:"optional"` // OVM legacy }   // ReceiptLogs is a barebone version of ReceiptForStorage which only keeps
diff --git go-ethereum/core/types/gen_receipt_json.go op-geth/core/types/gen_receipt_json.go index 4c641a972795f70ce27e98baf1b3c2e032fe89c2..4e544a0b52b61bac03f57c71581117f04280f81e 100644 --- go-ethereum/core/types/gen_receipt_json.go +++ op-geth/core/types/gen_receipt_json.go @@ -16,21 +16,30 @@ // MarshalJSON marshals as JSON. func (r Receipt) MarshalJSON() ([]byte, error) { type Receipt struct { - Type hexutil.Uint64 `json:"type,omitempty"` - PostState hexutil.Bytes `json:"root"` - Status hexutil.Uint64 `json:"status"` - CumulativeGasUsed hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"` - Bloom Bloom `json:"logsBloom" gencodec:"required"` - Logs []*Log `json:"logs" gencodec:"required"` - TxHash common.Hash `json:"transactionHash" gencodec:"required"` - ContractAddress common.Address `json:"contractAddress"` - GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"` - BlobGasUsed hexutil.Uint64 `json:"blobGasUsed,omitempty"` - BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"` - BlockHash common.Hash `json:"blockHash,omitempty"` - BlockNumber *hexutil.Big `json:"blockNumber,omitempty"` - TransactionIndex hexutil.Uint `json:"transactionIndex"` + Type hexutil.Uint64 `json:"type,omitempty"` + PostState hexutil.Bytes `json:"root"` + Status hexutil.Uint64 `json:"status"` + CumulativeGasUsed hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"` + Bloom Bloom `json:"logsBloom" gencodec:"required"` + Logs []*Log `json:"logs" gencodec:"required"` + TxHash common.Hash `json:"transactionHash" gencodec:"required"` + ContractAddress common.Address `json:"contractAddress"` + GasUsed hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"` + BlobGasUsed hexutil.Uint64 `json:"blobGasUsed,omitempty"` + BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"` + DepositNonce *hexutil.Uint64 `json:"depositNonce,omitempty"` + DepositReceiptVersion *hexutil.Uint64 `json:"depositReceiptVersion,omitempty"` + BlockHash common.Hash `json:"blockHash,omitempty"` + BlockNumber *hexutil.Big `json:"blockNumber,omitempty"` + TransactionIndex hexutil.Uint `json:"transactionIndex"` + L1GasPrice *hexutil.Big `json:"l1GasPrice,omitempty"` + L1BlobBaseFee *hexutil.Big `json:"l1BlobBaseFee,omitempty"` + L1GasUsed *hexutil.Big `json:"l1GasUsed,omitempty"` + L1Fee *hexutil.Big `json:"l1Fee,omitempty"` + FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` + L1BaseFeeScalar *hexutil.Uint64 `json:"l1BaseFeeScalar,omitempty"` + L1BlobBaseFeeScalar *hexutil.Uint64 `json:"l1BlobBaseFeeScalar,omitempty"` } var enc Receipt enc.Type = hexutil.Uint64(r.Type) @@ -45,30 +54,48 @@ enc.GasUsed = hexutil.Uint64(r.GasUsed) enc.EffectiveGasPrice = (*hexutil.Big)(r.EffectiveGasPrice) enc.BlobGasUsed = hexutil.Uint64(r.BlobGasUsed) enc.BlobGasPrice = (*hexutil.Big)(r.BlobGasPrice) + enc.DepositNonce = (*hexutil.Uint64)(r.DepositNonce) + enc.DepositReceiptVersion = (*hexutil.Uint64)(r.DepositReceiptVersion) enc.BlockHash = r.BlockHash enc.BlockNumber = (*hexutil.Big)(r.BlockNumber) enc.TransactionIndex = hexutil.Uint(r.TransactionIndex) + enc.L1GasPrice = (*hexutil.Big)(r.L1GasPrice) + enc.L1BlobBaseFee = (*hexutil.Big)(r.L1BlobBaseFee) + enc.L1GasUsed = (*hexutil.Big)(r.L1GasUsed) + enc.L1Fee = (*hexutil.Big)(r.L1Fee) + enc.FeeScalar = r.FeeScalar + enc.L1BaseFeeScalar = (*hexutil.Uint64)(r.L1BaseFeeScalar) + enc.L1BlobBaseFeeScalar = (*hexutil.Uint64)(r.L1BlobBaseFeeScalar) return json.Marshal(&enc) }   // UnmarshalJSON unmarshals from JSON. func (r *Receipt) UnmarshalJSON(input []byte) error { type Receipt struct { - Type *hexutil.Uint64 `json:"type,omitempty"` - PostState *hexutil.Bytes `json:"root"` - Status *hexutil.Uint64 `json:"status"` - CumulativeGasUsed *hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"` - Bloom *Bloom `json:"logsBloom" gencodec:"required"` - Logs []*Log `json:"logs" gencodec:"required"` - TxHash *common.Hash `json:"transactionHash" gencodec:"required"` - ContractAddress *common.Address `json:"contractAddress"` - GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` - EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"` - BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed,omitempty"` - BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"` - BlockHash *common.Hash `json:"blockHash,omitempty"` - BlockNumber *hexutil.Big `json:"blockNumber,omitempty"` - TransactionIndex *hexutil.Uint `json:"transactionIndex"` + Type *hexutil.Uint64 `json:"type,omitempty"` + PostState *hexutil.Bytes `json:"root"` + Status *hexutil.Uint64 `json:"status"` + CumulativeGasUsed *hexutil.Uint64 `json:"cumulativeGasUsed" gencodec:"required"` + Bloom *Bloom `json:"logsBloom" gencodec:"required"` + Logs []*Log `json:"logs" gencodec:"required"` + TxHash *common.Hash `json:"transactionHash" gencodec:"required"` + ContractAddress *common.Address `json:"contractAddress"` + GasUsed *hexutil.Uint64 `json:"gasUsed" gencodec:"required"` + EffectiveGasPrice *hexutil.Big `json:"effectiveGasPrice"` + BlobGasUsed *hexutil.Uint64 `json:"blobGasUsed,omitempty"` + BlobGasPrice *hexutil.Big `json:"blobGasPrice,omitempty"` + DepositNonce *hexutil.Uint64 `json:"depositNonce,omitempty"` + DepositReceiptVersion *hexutil.Uint64 `json:"depositReceiptVersion,omitempty"` + BlockHash *common.Hash `json:"blockHash,omitempty"` + BlockNumber *hexutil.Big `json:"blockNumber,omitempty"` + TransactionIndex *hexutil.Uint `json:"transactionIndex"` + L1GasPrice *hexutil.Big `json:"l1GasPrice,omitempty"` + L1BlobBaseFee *hexutil.Big `json:"l1BlobBaseFee,omitempty"` + L1GasUsed *hexutil.Big `json:"l1GasUsed,omitempty"` + L1Fee *hexutil.Big `json:"l1Fee,omitempty"` + FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` + L1BaseFeeScalar *hexutil.Uint64 `json:"l1BaseFeeScalar,omitempty"` + L1BlobBaseFeeScalar *hexutil.Uint64 `json:"l1BlobBaseFeeScalar,omitempty"` } var dec Receipt if err := json.Unmarshal(input, &dec); err != nil { @@ -115,6 +142,12 @@ } if dec.BlobGasPrice != nil { r.BlobGasPrice = (*big.Int)(dec.BlobGasPrice) } + if dec.DepositNonce != nil { + r.DepositNonce = (*uint64)(dec.DepositNonce) + } + if dec.DepositReceiptVersion != nil { + r.DepositReceiptVersion = (*uint64)(dec.DepositReceiptVersion) + } if dec.BlockHash != nil { r.BlockHash = *dec.BlockHash } @@ -123,6 +156,27 @@ r.BlockNumber = (*big.Int)(dec.BlockNumber) } if dec.TransactionIndex != nil { r.TransactionIndex = uint(*dec.TransactionIndex) + } + if dec.L1GasPrice != nil { + r.L1GasPrice = (*big.Int)(dec.L1GasPrice) + } + if dec.L1BlobBaseFee != nil { + r.L1BlobBaseFee = (*big.Int)(dec.L1BlobBaseFee) + } + if dec.L1GasUsed != nil { + r.L1GasUsed = (*big.Int)(dec.L1GasUsed) + } + if dec.L1Fee != nil { + r.L1Fee = (*big.Int)(dec.L1Fee) + } + if dec.FeeScalar != nil { + r.FeeScalar = dec.FeeScalar + } + if dec.L1BaseFeeScalar != nil { + r.L1BaseFeeScalar = (*uint64)(dec.L1BaseFeeScalar) + } + if dec.L1BlobBaseFeeScalar != nil { + r.L1BlobBaseFeeScalar = (*uint64)(dec.L1BlobBaseFeeScalar) } return nil }
diff --git go-ethereum/core/types/receipt.go op-geth/core/types/receipt.go index 4f96fde59c44278beee06e9846e158c2501644e6..4bc83bf988f84dbe37f3fc0a04577c2838f9363a 100644 --- go-ethereum/core/types/receipt.go +++ op-geth/core/types/receipt.go @@ -46,6 +46,9 @@ ReceiptStatusFailed = uint64(0)   // ReceiptStatusSuccessful is the status code of a transaction if execution succeeded. ReceiptStatusSuccessful = uint64(1) + + // The version number for post-canyon deposit receipts. + CanyonDepositReceiptVersion = uint64(1) )   // Receipt represents the results of a transaction. @@ -58,7 +61,8 @@ CumulativeGasUsed uint64 `json:"cumulativeGasUsed" gencodec:"required"` Bloom Bloom `json:"logsBloom" gencodec:"required"` Logs []*Log `json:"logs" gencodec:"required"`   - // Implementation fields: These fields are added by geth when processing a transaction. + // Implementation fields: These fields are added by geth when processing a transaction or retrieving a receipt. + // gencodec annotated fields: these are stored in the chain database. TxHash common.Hash `json:"transactionHash" gencodec:"required"` ContractAddress common.Address `json:"contractAddress"` GasUsed uint64 `json:"gasUsed" gencodec:"required"` @@ -66,11 +70,28 @@ EffectiveGasPrice *big.Int `json:"effectiveGasPrice"` // required, but tag omitted for backwards compatibility BlobGasUsed uint64 `json:"blobGasUsed,omitempty"` BlobGasPrice *big.Int `json:"blobGasPrice,omitempty"`   + // DepositNonce was introduced in Regolith to store the actual nonce used by deposit transactions + // The state transition process ensures this is only set for Regolith deposit transactions. + DepositNonce *uint64 `json:"depositNonce,omitempty"` + // DepositReceiptVersion was introduced in Canyon to indicate an update to how receipt hashes + // should be computed when set. The state transition process ensures this is only set for + // post-Canyon deposit transactions. + DepositReceiptVersion *uint64 `json:"depositReceiptVersion,omitempty"` + // Inclusion information: These fields provide information about the inclusion of the // transaction corresponding to this receipt. BlockHash common.Hash `json:"blockHash,omitempty"` BlockNumber *big.Int `json:"blockNumber,omitempty"` TransactionIndex uint `json:"transactionIndex"` + + // Optimism: extend receipts with L1 fee info + L1GasPrice *big.Int `json:"l1GasPrice,omitempty"` // Present from pre-bedrock. L1 Basefee after Bedrock + L1BlobBaseFee *big.Int `json:"l1BlobBaseFee,omitempty"` // Always nil prior to the Ecotone hardfork + L1GasUsed *big.Int `json:"l1GasUsed,omitempty"` // Present from pre-bedrock, deprecated as of Fjord + L1Fee *big.Int `json:"l1Fee,omitempty"` // Present from pre-bedrock + FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` // Present from pre-bedrock to Ecotone. Nil after Ecotone + L1BaseFeeScalar *uint64 `json:"l1BaseFeeScalar,omitempty"` // Always nil prior to the Ecotone hardfork + L1BlobBaseFeeScalar *uint64 `json:"l1BlobBaseFeeScalar,omitempty"` // Always nil prior to the Ecotone hardfork }   type receiptMarshaling struct { @@ -84,6 +105,17 @@ BlobGasUsed hexutil.Uint64 BlobGasPrice *hexutil.Big BlockNumber *hexutil.Big TransactionIndex hexutil.Uint + + // Optimism + L1GasPrice *hexutil.Big + L1BlobBaseFee *hexutil.Big + L1GasUsed *hexutil.Big + L1Fee *hexutil.Big + FeeScalar *big.Float + L1BaseFeeScalar *hexutil.Uint64 + L1BlobBaseFeeScalar *hexutil.Uint64 + DepositNonce *hexutil.Uint64 + DepositReceiptVersion *hexutil.Uint64 }   // receiptRLP is the consensus encoding of a receipt. @@ -94,11 +126,98 @@ Bloom Bloom Logs []*Log }   +type depositReceiptRLP struct { + PostStateOrStatus []byte + CumulativeGasUsed uint64 + Bloom Bloom + Logs []*Log + // DepositNonce was introduced in Regolith to store the actual nonce used by deposit transactions. + // Must be nil for any transactions prior to Regolith or that aren't deposit transactions. + DepositNonce *uint64 `rlp:"optional"` + // Receipt hash post-Regolith but pre-Canyon inadvertently did not include the above + // DepositNonce. Post Canyon, receipts will have a non-empty DepositReceiptVersion indicating + // which post-Canyon receipt hash function to invoke. + DepositReceiptVersion *uint64 `rlp:"optional"` +} + // storedReceiptRLP is the storage encoding of a receipt. type storedReceiptRLP struct { PostStateOrStatus []byte CumulativeGasUsed uint64 Logs []*Log + // DepositNonce was introduced in Regolith to store the actual nonce used by deposit transactions. + // Must be nil for any transactions prior to Regolith or that aren't deposit transactions. + DepositNonce *uint64 `rlp:"optional"` + // Receipt hash post-Regolith but pre-Canyon inadvertently did not include the above + // DepositNonce. Post Canyon, receipts will have a non-empty DepositReceiptVersion indicating + // which post-Canyon receipt hash function to invoke. + DepositReceiptVersion *uint64 `rlp:"optional"` +} + +// LegacyOptimismStoredReceiptRLP is the pre bedrock storage encoding of a +// receipt. It will only exist in the database if it was migrated using the +// migration tool. Nodes that sync using snap-sync will not have any of these +// entries. +type LegacyOptimismStoredReceiptRLP struct { + PostStateOrStatus []byte + CumulativeGasUsed uint64 + Logs []*LogForStorage + L1GasUsed *big.Int + L1GasPrice *big.Int + L1Fee *big.Int + FeeScalar string +} + +// LogForStorage is a wrapper around a Log that handles +// backward compatibility with prior storage formats. +type LogForStorage Log + +// EncodeRLP implements rlp.Encoder. +func (l *LogForStorage) EncodeRLP(w io.Writer) error { + rl := Log{Address: l.Address, Topics: l.Topics, Data: l.Data} + return rlp.Encode(w, &rl) +} + +type legacyRlpStorageLog struct { + Address common.Address + Topics []common.Hash + Data []byte + BlockNumber uint64 + TxHash common.Hash + TxIndex uint + BlockHash common.Hash + Index uint +} + +// DecodeRLP implements rlp.Decoder. +// +// Note some redundant fields(e.g. block number, tx hash etc) will be assembled later. +func (l *LogForStorage) DecodeRLP(s *rlp.Stream) error { + blob, err := s.Raw() + if err != nil { + return err + } + var dec Log + err = rlp.DecodeBytes(blob, &dec) + if err == nil { + *l = LogForStorage{ + Address: dec.Address, + Topics: dec.Topics, + Data: dec.Data, + } + } else { + // Try to decode log with previous definition. + var dec legacyRlpStorageLog + err = rlp.DecodeBytes(blob, &dec) + if err == nil { + *l = LogForStorage{ + Address: dec.Address, + Topics: dec.Topics, + Data: dec.Data, + } + } + } + return err }   // NewReceipt creates a barebone transaction receipt, copying the init fields. @@ -136,7 +255,13 @@ // encodeTyped writes the canonical encoding of a typed receipt to w. func (r *Receipt) encodeTyped(data *receiptRLP, w *bytes.Buffer) error { w.WriteByte(r.Type) - return rlp.Encode(w, data) + switch r.Type { + case DepositTxType: + withNonce := &depositReceiptRLP{data.PostStateOrStatus, data.CumulativeGasUsed, data.Bloom, data.Logs, r.DepositNonce, r.DepositReceiptVersion} + return rlp.Encode(w, withNonce) + default: + return rlp.Encode(w, data) + } }   // MarshalBinary returns the consensus encoding of the receipt. @@ -212,6 +337,16 @@ return err } r.Type = b[0] return r.setFromRLP(data) + case DepositTxType: + var data depositReceiptRLP + err := rlp.DecodeBytes(b[1:], &data) + if err != nil { + return err + } + r.Type = b[0] + r.DepositNonce = data.DepositNonce + r.DepositReceiptVersion = data.DepositReceiptVersion + return r.setFromRLP(receiptRLP{data.PostStateOrStatus, data.CumulativeGasUsed, data.Bloom, data.Logs}) default: return ErrTxTypeNotSupported } @@ -275,6 +410,12 @@ return err } } w.ListEnd(logList) + if r.DepositNonce != nil { + w.WriteUint64(*r.DepositNonce) + if r.DepositReceiptVersion != nil { + w.WriteUint64(*r.DepositReceiptVersion) + } + } w.ListEnd(outerList) return w.Flush() } @@ -282,8 +423,51 @@ // DecodeRLP implements rlp.Decoder, and loads both consensus and implementation // fields of a receipt from an RLP stream. func (r *ReceiptForStorage) DecodeRLP(s *rlp.Stream) error { + // Retrieve the entire receipt blob as we need to try multiple decoders + blob, err := s.Raw() + if err != nil { + return err + } + // First try to decode the latest receipt database format, try the pre-bedrock Optimism legacy format otherwise. + if err := decodeStoredReceiptRLP(r, blob); err == nil { + return nil + } + return decodeLegacyOptimismReceiptRLP(r, blob) +} + +func decodeLegacyOptimismReceiptRLP(r *ReceiptForStorage, blob []byte) error { + var stored LegacyOptimismStoredReceiptRLP + if err := rlp.DecodeBytes(blob, &stored); err != nil { + return err + } + if err := (*Receipt)(r).setStatus(stored.PostStateOrStatus); err != nil { + return err + } + r.CumulativeGasUsed = stored.CumulativeGasUsed + r.Logs = make([]*Log, len(stored.Logs)) + for i, log := range stored.Logs { + r.Logs[i] = (*Log)(log) + } + r.Bloom = CreateBloom(Receipts{(*Receipt)(r)}) + // UsingOVM + scalar := new(big.Float) + if stored.FeeScalar != "" { + var ok bool + scalar, ok = scalar.SetString(stored.FeeScalar) + if !ok { + return errors.New("cannot parse fee scalar") + } + } + r.L1GasUsed = stored.L1GasUsed + r.L1GasPrice = stored.L1GasPrice + r.L1Fee = stored.L1Fee + r.FeeScalar = scalar + return nil +} + +func decodeStoredReceiptRLP(r *ReceiptForStorage, blob []byte) error { var stored storedReceiptRLP - if err := s.Decode(&stored); err != nil { + if err := rlp.DecodeBytes(blob, &stored); err != nil { return err } if err := (*Receipt)(r).setStatus(stored.PostStateOrStatus); err != nil { @@ -292,7 +476,10 @@ } r.CumulativeGasUsed = stored.CumulativeGasUsed r.Logs = stored.Logs r.Bloom = CreateBloom(Receipts{(*Receipt)(r)}) - + if stored.DepositNonce != nil { + r.DepositNonce = stored.DepositNonce + r.DepositReceiptVersion = stored.DepositReceiptVersion + } return nil }   @@ -302,7 +489,10 @@ // Len returns the number of receipts in this list. func (rs Receipts) Len() int { return len(rs) }   -// EncodeIndex encodes the i'th receipt to w. +// EncodeIndex encodes the i'th receipt to w. For DepositTxType receipts with non-nil DepositNonce +// but nil DepositReceiptVersion, the output will differ than calling r.MarshalBinary(); this +// behavior difference should not be changed to preserve backwards compatibility of receipt-root +// hash computation. func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { r := rs[i] data := &receiptRLP{r.statusEncoding(), r.CumulativeGasUsed, r.Bloom, r.Logs} @@ -314,6 +504,14 @@ w.WriteByte(r.Type) switch r.Type { case AccessListTxType, DynamicFeeTxType, BlobTxType: rlp.Encode(w, data) + case DepositTxType: + if r.DepositReceiptVersion != nil { + // post-canyon receipt hash computation update + depositData := &depositReceiptRLP{data.PostStateOrStatus, data.CumulativeGasUsed, r.Bloom, r.Logs, r.DepositNonce, r.DepositReceiptVersion} + rlp.Encode(w, depositData) + } else { + rlp.Encode(w, data) + } default: // For unsupported types, write nothing. Since this is for // DeriveSha, the error will be caught matching the derived hash @@ -351,7 +549,11 @@ // The contract address can be derived from the transaction itself if txs[i].To() == nil { // Deriving the signer is expensive, only do if it's actually needed from, _ := Sender(signer, txs[i]) - rs[i].ContractAddress = crypto.CreateAddress(from, txs[i].Nonce()) + nonce := txs[i].Nonce() + if rs[i].DepositNonce != nil { + nonce = *rs[i].DepositNonce + } + rs[i].ContractAddress = crypto.CreateAddress(from, nonce) } else { rs[i].ContractAddress = common.Address{} } @@ -373,5 +575,30 @@ rs[i].Logs[j].Index = logIndex logIndex++ } } + if config.Optimism != nil && len(txs) >= 2 && config.IsBedrock(new(big.Int).SetUint64(number)) { // need at least an info tx and a non-info tx + gasParams, err := extractL1GasParams(config, time, txs[0].Data()) + if err != nil { + return err + } + for i := 0; i < len(rs); i++ { + if txs[i].IsDepositTx() { + continue + } + rs[i].L1GasPrice = gasParams.l1BaseFee + rs[i].L1BlobBaseFee = gasParams.l1BlobBaseFee + rs[i].L1Fee, rs[i].L1GasUsed = gasParams.costFunc(txs[i].RollupCostData()) + rs[i].FeeScalar = gasParams.feeScalar + rs[i].L1BaseFeeScalar = u32ptrTou64ptr(gasParams.l1BaseFeeScalar) + rs[i].L1BlobBaseFeeScalar = u32ptrTou64ptr(gasParams.l1BlobBaseFeeScalar) + } + } return nil } + +func u32ptrTou64ptr(a *uint32) *uint64 { + if a == nil { + return nil + } + b := uint64(*a) + return &b +}

Forward transactions to the sequencer if configured.

diff --git go-ethereum/eth/api_backend.go op-geth/eth/api_backend.go index 8a9898b956f38a6584f98a7ca4a067b27d03ba7e..bd1b623c367f5e25d66f92bc0c96790bf59e19ca 100644 --- go-ethereum/eth/api_backend.go +++ op-geth/eth/api_backend.go @@ -19,12 +19,14 @@ import ( "context" "errors" + "fmt" "math/big" "time"   "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/misc/eip4844" "github.com/ethereum/go-ethereum/core" @@ -38,6 +40,7 @@ "github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" ) @@ -46,6 +49,7 @@ // EthAPIBackend implements ethapi.Backend and tracers.Backend for full nodes type EthAPIBackend struct { extRPCEnabled bool allowUnprotectedTxs bool + disableTxPool bool eth *Ethereum gpo *gasprice.Oracle } @@ -190,10 +194,11 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) { // Pending state is only known by the miner if number == rpc.PendingBlockNumber { block, _, state := b.eth.miner.Pending() - if block == nil || state == nil { - return nil, nil, errors.New("pending state is not available") + if block != nil && state != nil { + return state, block.Header(), nil + } else { + number = rpc.LatestBlockNumber // fall back to latest state } - return state, block.Header(), nil } // Otherwise resolve the block number and return its state header, err := b.HeaderByNumber(ctx, number) @@ -201,7 +206,7 @@ if err != nil { return nil, nil, err } if header == nil { - return nil, nil, errors.New("header not found") + return nil, nil, fmt.Errorf("header %w", ethereum.NotFound) } stateDb, err := b.eth.BlockChain().StateAt(header.Root) if err != nil { @@ -220,7 +225,7 @@ if err != nil { return nil, nil, err } if header == nil { - return nil, nil, errors.New("header for hash not found") + return nil, nil, fmt.Errorf("header for hash %w", ethereum.NotFound) } if blockNrOrHash.RequireCanonical && b.eth.blockchain.GetCanonicalHash(header.Number.Uint64()) != hash { return nil, nil, errors.New("hash is not currently canonical") @@ -258,7 +263,7 @@ var context vm.BlockContext if blockCtx != nil { context = *blockCtx } else { - context = core.NewEVMBlockContext(header, b.eth.BlockChain(), nil) + context = core.NewEVMBlockContext(header, b.eth.BlockChain(), nil, b.eth.blockchain.Config(), state) } return vm.NewEVM(context, txContext, state, b.ChainConfig(), *vmConfig) } @@ -284,6 +289,29 @@ return b.eth.BlockChain().SubscribeLogsEvent(ch) }   func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { + if b.ChainConfig().IsOptimism() && signedTx.Type() == types.BlobTxType { + return types.ErrTxTypeNotSupported + } + if b.eth.seqRPCService != nil { + data, err := signedTx.MarshalBinary() + if err != nil { + return err + } + if err := b.eth.seqRPCService.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data)); err != nil { + return err + } + if b.disableTxPool { + return nil + } + // Retain tx in local tx pool after forwarding, for local RPC usage. + if err := b.eth.txPool.Add([]*types.Transaction{signedTx}, true, false)[0]; err != nil { + log.Warn("successfully sent tx to sequencer, but failed to persist in local tx pool", "err", err, "tx", signedTx.Hash()) + } + return nil + } + if b.disableTxPool { + return nil + } return b.eth.txPool.Add([]*types.Transaction{signedTx}, true, false)[0] }   @@ -431,3 +459,11 @@ func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) } + +func (b *EthAPIBackend) HistoricalRPCService() *rpc.Client { + return b.eth.historicalRPCService +} + +func (b *EthAPIBackend) Genesis() *types.Block { + return b.eth.blockchain.Genesis() +}
diff --git go-ethereum/eth/backend.go op-geth/eth/backend.go index f10d99c3a70bb93b41c24358d46508a34de66cc4..79453719c642055fb1cf5c28865a1f9bb18ba12c 100644 --- go-ethereum/eth/backend.go +++ op-geth/eth/backend.go @@ -18,11 +18,13 @@ // Package eth implements the Ethereum protocol. package eth   import ( + "context" "encoding/json" "fmt" "math/big" "runtime" "sync" + "time"   "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" @@ -40,12 +42,14 @@ "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/downloader" "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/eth/gasprice" + "github.com/ethereum/go-ethereum/eth/interop" "github.com/ethereum/go-ethereum/eth/protocols/eth" "github.com/ethereum/go-ethereum/eth/protocols/snap" "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/internal/sequencerapi" "github.com/ethereum/go-ethereum/internal/shutdowncheck" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/miner" @@ -56,6 +60,7 @@ "github.com/ethereum/go-ethereum/p2p/enode" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" + "golang.org/x/time/rate" )   // Config contains the configuration options of the ETH protocol. @@ -72,6 +77,11 @@ handler *handler discmix *enode.FairMix   + seqRPCService *rpc.Client + historicalRPCService *rpc.Client + + interopRPC *interop.InteropClient + // DB interfaces chainDb ethdb.Database // Block chain database   @@ -96,6 +106,8 @@ lock sync.RWMutex // Protects the variadic fields (e.g. gas price and etherbase)   shutdownTracker *shutdowncheck.ShutdownTracker // Tracks if and when the node has shutdown ungracefully + + nodeCloser func() error }   // New creates a new Ethereum object (including the initialisation of the common Ethereum object), @@ -162,13 +174,13 @@ bloomIndexer: core.NewBloomIndexer(chainDb, params.BloomBitsBlocks, params.BloomConfirms), p2pServer: stack.Server(), discmix: enode.NewFairMix(0), shutdownTracker: shutdowncheck.NewShutdownTracker(chainDb), + nodeCloser: stack.Close, } bcVersion := rawdb.ReadDatabaseVersion(chainDb) - var dbVer = "<nil>" + dbVer := "<nil>" if bcVersion != nil { dbVer = fmt.Sprintf("%d", *bcVersion) } - log.Info("Initialising Ethereum protocol", "network", networkID, "dbversion", dbVer)   if !config.SkipBcVersionCheck { if bcVersion != nil && *bcVersion > core.BlockChainVersion { @@ -215,23 +227,59 @@ } if config.OverrideVerkle != nil { overrides.OverrideVerkle = config.OverrideVerkle } + if config.OverrideOptimismCanyon != nil { + overrides.OverrideOptimismCanyon = config.OverrideOptimismCanyon + } + if config.OverrideOptimismEcotone != nil { + overrides.OverrideOptimismEcotone = config.OverrideOptimismEcotone + } + if config.OverrideOptimismFjord != nil { + overrides.OverrideOptimismFjord = config.OverrideOptimismFjord + } + if config.OverrideOptimismGranite != nil { + overrides.OverrideOptimismGranite = config.OverrideOptimismGranite + } + if config.OverrideOptimismHolocene != nil { + overrides.OverrideOptimismHolocene = config.OverrideOptimismHolocene + } + if config.OverrideOptimismInterop != nil { + overrides.OverrideOptimismInterop = config.OverrideOptimismInterop + } + overrides.ApplySuperchainUpgrades = config.ApplySuperchainUpgrades + eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, config.Genesis, &overrides, eth.engine, vmConfig, &config.TransactionHistory) if err != nil { return nil, err } + if chainConfig := eth.blockchain.Config(); chainConfig.Optimism != nil { // config.Genesis.Config.ChainID cannot be used because it's based on CLI flags only, thus default to mainnet L1 + config.NetworkId = chainConfig.ChainID.Uint64() // optimism defaults eth network ID to chain ID + eth.networkID = config.NetworkId + } + log.Info("Initialising Ethereum protocol", "network", config.NetworkId, "dbversion", dbVer) + eth.bloomIndexer.Start(eth.blockchain)   if config.BlobPool.Datadir != "" { config.BlobPool.Datadir = stack.ResolvePath(config.BlobPool.Datadir) } - blobPool := blobpool.New(config.BlobPool, eth.blockchain)   if config.TxPool.Journal != "" { config.TxPool.Journal = stack.ResolvePath(config.TxPool.Journal) } legacyPool := legacypool.New(config.TxPool, eth.blockchain)   - eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, []txpool.SubPool{legacyPool, blobPool}) + txPools := []txpool.SubPool{legacyPool} + if !eth.BlockChain().Config().IsOptimism() { + blobPool := blobpool.New(config.BlobPool, eth.blockchain) + txPools = append(txPools, blobPool) + } + // 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 } @@ -247,6 +295,7 @@ Sync: config.SyncMode, BloomCache: uint64(cacheLimit), EventMux: eth.eventMux, RequiredBlocks: config.RequiredBlocks, + NoTxGossip: config.RollupDisableTxPoolGossip, }); err != nil { return nil, err } @@ -254,12 +303,36 @@ eth.miner = miner.New(eth, config.Miner, eth.engine) eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData))   - eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, eth, nil} + eth.APIBackend = &EthAPIBackend{stack.Config().ExtRPCEnabled(), stack.Config().AllowUnprotectedTxs, config.RollupDisableTxPoolAdmission, eth, nil} if eth.APIBackend.allowUnprotectedTxs { log.Info("Unprotected transactions allowed") } eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, config.GPO, config.Miner.GasPrice)   + if config.RollupSequencerHTTP != "" { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + client, err := rpc.DialContext(ctx, config.RollupSequencerHTTP) + cancel() + if err != nil { + return nil, err + } + eth.seqRPCService = client + } + + if config.RollupHistoricalRPC != "" { + ctx, cancel := context.WithTimeout(context.Background(), config.RollupHistoricalRPCTimeout) + client, err := rpc.DialContext(ctx, config.RollupHistoricalRPC) + cancel() + if err != nil { + return nil, err + } + eth.historicalRPCService = client + } + + if config.InteropMessageRPC != "" { + eth.interopRPC = interop.NewInteropClient(config.InteropMessageRPC) + } + // Start the RPC service eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, networkID)   @@ -278,7 +351,7 @@ func makeExtraData(extra []byte) []byte { if len(extra) == 0 { // create default extradata extra, _ = rlp.EncodeToBytes([]interface{}{ - uint(params.VersionMajor<<16 | params.VersionMinor<<8 | params.VersionPatch), + uint(params.OPVersionMajor<<16 | params.OPVersionMinor<<8 | params.OPVersionPatch), "geth", runtime.Version(), runtime.GOOS, @@ -298,6 +371,13 @@ apis := ethapi.GetAPIs(s.APIBackend)   // Append any APIs exposed explicitly by the consensus engine apis = append(apis, s.engine.APIs(s.BlockChain())...) + + // Append any Sequencer APIs as enabled + if s.config.RollupSequencerTxConditionalEnabled { + log.Info("Enabling eth_sendRawTransactionConditional endpoint support") + costRateLimit := rate.Limit(s.config.RollupSequencerTxConditionalCostRateLimit) + apis = append(apis, sequencerapi.GetSendRawTxConditionalAPI(s.APIBackend, s.seqRPCService, costRateLimit)) + }   // Append all the local APIs and return return append(apis, []rpc.API{ @@ -410,6 +490,18 @@ close(s.closeBloomHandler) s.txPool.Close() s.blockchain.Stop() s.engine.Close() + if s.seqRPCService != nil { + s.seqRPCService.Close() + } + 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() @@ -445,3 +537,33 @@ } // Nope, we're really full syncing return downloader.FullSync } + +// HandleRequiredProtocolVersion handles the protocol version signal. This implements opt-in halting, +// the protocol version data is already logged and metered when signaled through the Engine API. +func (s *Ethereum) HandleRequiredProtocolVersion(required params.ProtocolVersion) error { + var needLevel int + switch s.config.RollupHaltOnIncompatibleProtocolVersion { + case "major": + needLevel = 3 + case "minor": + needLevel = 2 + case "patch": + needLevel = 1 + default: + return nil // do not consider halting if not configured to + } + haveLevel := 0 + switch params.OPStackSupport.Compare(required) { + case params.OutdatedMajor: + haveLevel = 3 + case params.OutdatedMinor: + haveLevel = 2 + case params.OutdatedPatch: + haveLevel = 1 + } + if haveLevel >= needLevel { // halt if we opted in to do so at this granularity + log.Error("Opted to halt, unprepared for protocol change", "required", required, "local", params.OPStackSupport) + return s.nodeCloser() + } + return nil +}
diff --git go-ethereum/internal/ethapi/backend.go op-geth/internal/ethapi/backend.go index 0e991592b4b3c528e0b97c06223ecc2834cad55a..4f5849a86984952673b61abb26edb50558357bb3 100644 --- go-ethereum/internal/ethapi/backend.go +++ op-geth/internal/ethapi/backend.go @@ -86,6 +86,8 @@ SubscribeNewTxsEvent(chan<- core.NewTxsEvent) event.Subscription   ChainConfig() *params.ChainConfig Engine() consensus.Engine + HistoricalRPCService() *rpc.Client + Genesis() *types.Block   // This is copied from filters.Backend // eth/filters needs to be initialized from this backend type, so methods needed by
diff --git go-ethereum/eth/state_accessor.go op-geth/eth/state_accessor.go index cb5a233a83badfe93413d3d0171928d5b4267506..5941e0a15c46a807abba293eec365e8eb0a91b29 100644 --- go-ethereum/eth/state_accessor.go +++ op-geth/eth/state_accessor.go @@ -236,13 +236,13 @@ return nil, vm.BlockContext{}, nil, nil, err } // Insert parent beacon block root in the state as per EIP-4788. if beaconRoot := block.BeaconRoot(); beaconRoot != nil { - context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) + context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil, eth.blockchain.Config(), statedb) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, eth.blockchain.Config(), vm.Config{}) core.ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) } // If prague hardfork, insert parent block hash in the state as per EIP-2935. if eth.blockchain.Config().IsPrague(block.Number(), block.Time()) { - context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) + context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil, eth.blockchain.Config(), statedb) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, eth.blockchain.Config(), vm.Config{}) core.ProcessParentBlockHash(block.ParentHash(), vmenv, statedb) } @@ -255,7 +255,7 @@ for idx, tx := range block.Transactions() { // Assemble the transaction call message and return if the requested offset msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil) + context := core.NewEVMBlockContext(block.Header(), eth.blockchain, nil, eth.blockchain.Config(), statedb) if idx == txIndex { return tx, context, statedb, release, nil }

Format deposit and L1-cost data in transaction responses. Add debug_chainConfig API.

diff --git go-ethereum/internal/ethapi/api.go op-geth/internal/ethapi/api.go index a50295289340d876823270b7fd12714dc8066515..7712d71273f3c2521f8f5944fbf58ecf6dc08cc1 100644 --- go-ethereum/internal/ethapi/api.go +++ op-geth/internal/ethapi/api.go @@ -27,6 +27,7 @@ "strings" "time"   "github.com/davecgh/go-spew/spew" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/scwallet" @@ -665,6 +666,24 @@ // GetBalance returns the amount of wei for the given address in the state of the // given block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta // block numbers are also allowed. func (api *BlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) { + header, err := headerByNumberOrHash(ctx, api.b, blockNrOrHash) + if err != nil { + return nil, err + } + + if api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res hexutil.Big + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_getBalance", address, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return &res, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil, err @@ -705,6 +724,22 @@ }   // GetProof returns the Merkle-proof for a given account and optionally some storage keys. func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) { + header, err := headerByNumberOrHash(ctx, api.b, blockNrOrHash) + if err != nil { + return nil, err + } + if api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res AccountResult + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_getProof", address, storageKeys, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return &res, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } var ( keys = make([]common.Hash, len(storageKeys)) keyLengths = make([]int, len(storageKeys)) @@ -808,7 +843,7 @@ func (api *BlockChainAPI) GetHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (map[string]interface{}, error) { header, err := api.b.HeaderByNumber(ctx, number) if header != nil && err == nil { response := RPCMarshalHeader(header) - if number == rpc.PendingBlockNumber { + if number == rpc.PendingBlockNumber && api.b.ChainConfig().Optimism == nil { // Pending header need to nil out a few fields for _, field := range []string{"hash", "nonce", "miner"} { response[field] = nil @@ -838,14 +873,14 @@ // only the transaction hash is returned. func (api *BlockChainAPI) GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { block, err := api.b.BlockByNumber(ctx, number) if block != nil && err == nil { - response := RPCMarshalBlock(block, true, fullTx, api.b.ChainConfig()) - if number == rpc.PendingBlockNumber { + response, err := RPCMarshalBlock(ctx, block, true, fullTx, api.b.ChainConfig(), api.b) + if err == nil && number == rpc.PendingBlockNumber && api.b.ChainConfig().Optimism == nil { // Pending blocks need to nil out a few fields for _, field := range []string{"hash", "nonce", "miner"} { response[field] = nil } } - return response, nil + return response, err } return nil, err } @@ -855,7 +890,7 @@ // detail, otherwise only the transaction hash is returned. func (api *BlockChainAPI) GetBlockByHash(ctx context.Context, hash common.Hash, fullTx bool) (map[string]interface{}, error) { block, err := api.b.BlockByHash(ctx, hash) if block != nil { - return RPCMarshalBlock(block, true, fullTx, api.b.ChainConfig()), nil + return RPCMarshalBlock(ctx, block, true, fullTx, api.b.ChainConfig(), api.b) } return nil, err } @@ -870,7 +905,7 @@ log.Debug("Requested uncle not found", "number", blockNr, "hash", block.Hash(), "index", index) return nil, nil } block = types.NewBlockWithHeader(uncles[index]) - return RPCMarshalBlock(block, false, false, api.b.ChainConfig()), nil + return RPCMarshalBlock(ctx, block, false, false, api.b.ChainConfig(), api.b) } return nil, err } @@ -885,7 +920,7 @@ log.Debug("Requested uncle not found", "number", block.Number(), "hash", blockHash, "index", index) return nil, nil } block = types.NewBlockWithHeader(uncles[index]) - return RPCMarshalBlock(block, false, false, api.b.ChainConfig()), nil + return RPCMarshalBlock(ctx, block, false, false, api.b.ChainConfig(), api.b) } return nil, err } @@ -910,10 +945,29 @@ }   // GetCode returns the code stored at the given address in the state for the given block number. func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { + header, err := headerByNumberOrHash(ctx, api.b, blockNrOrHash) + if err != nil { + return nil, err + } + + if api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res hexutil.Bytes + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_getCode", address, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return res, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil, err } + code := state.GetCode(address) return code, state.Error() } @@ -922,16 +976,47 @@ // GetStorageAt returns the storage from the state at the given address, key and // block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block // numbers are also allowed. func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Address, hexKey string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { + header, err := headerByNumberOrHash(ctx, api.b, blockNrOrHash) + if err != nil { + return nil, err + } + + if api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res hexutil.Bytes + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_getStorageAt", address, hexKey, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return res, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil, err } + key, _, err := decodeHash(hexKey) if err != nil { return nil, fmt.Errorf("unable to decode storage key: %s", err) } res := state.GetState(address, key) return res[:], state.Error() +} + +// The HeaderByNumberOrHash method returns a nil error and nil header +// if the header is not found, but only for nonexistent block numbers. This is +// different from StateAndHeaderByNumberOrHash. To account for this discrepancy, +// headerOrNumberByHash will properly convert the error into an ethereum.NotFound. +func headerByNumberOrHash(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) { + header, err := b.HeaderByNumberOrHash(ctx, blockNrOrHash) + if header == nil { + return nil, fmt.Errorf("header %w", ethereum.NotFound) + } + return header, err }   // GetBlockReceipts returns the block receipts for the given block hash or number or tag. @@ -956,7 +1041,7 @@ signer := types.MakeSigner(api.b.ChainConfig(), block.Number(), block.Time())   result := make([]map[string]interface{}, len(receipts)) for i, receipt := range receipts { - result[i] = marshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i) + result[i] = marshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i, api.b.ChainConfig()) }   return result, nil @@ -1160,7 +1245,7 @@ return header }   func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *StateOverride, blockOverrides *BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { - blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil) + blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil, b.ChainConfig(), state) if blockOverrides != nil { blockOverrides.Apply(&blockCtx) } @@ -1257,6 +1342,25 @@ if blockNrOrHash == nil { latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) blockNrOrHash = &latest } + + header, err := headerByNumberOrHash(ctx, api.b, *blockNrOrHash) + if err != nil { + return nil, err + } + + if api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res hexutil.Bytes + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_call", args, blockNrOrHash, overrides) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return res, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + result, err := DoCall(ctx, api.b, args, *blockNrOrHash, overrides, blockOverrides, api.b.RPCEVMTimeout(), api.b.RPCGasCap()) if err != nil { return nil, err @@ -1360,6 +1464,25 @@ bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) if blockNrOrHash != nil { bNrOrHash = *blockNrOrHash } + + header, err := headerByNumberOrHash(ctx, api.b, bNrOrHash) + if err != nil { + return 0, err + } + + if api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res hexutil.Uint64 + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_estimateGas", args, blockNrOrHash) + if err != nil { + return 0, fmt.Errorf("historical backend error: %w", err) + } + return res, nil + } else { + return 0, rpc.ErrNoHistoricalFallback + } + } + return DoEstimateGas(ctx, api.b, args, bNrOrHash, overrides, api.b.RPCGasCap()) }   @@ -1407,7 +1530,7 @@ // RPCMarshalBlock converts the given block to the RPC output which depends on fullTx. If inclTx is true transactions are // returned. When fullTx is true the returned block contains full transaction details, otherwise it will only contain // transaction hashes. -func RPCMarshalBlock(block *types.Block, inclTx bool, fullTx bool, config *params.ChainConfig) map[string]interface{} { +func RPCMarshalBlock(ctx context.Context, block *types.Block, inclTx bool, fullTx bool, config *params.ChainConfig, backend Backend) (map[string]interface{}, error) { fields := RPCMarshalHeader(block.Header()) fields["size"] = hexutil.Uint64(block.Size())   @@ -1417,7 +1540,7 @@ return tx.Hash() } if fullTx { formatTx = func(idx int, tx *types.Transaction) interface{} { - return newRPCTransactionFromBlockIndex(block, uint64(idx), config) + return newRPCTransactionFromBlockIndex(ctx, block, uint64(idx), config, backend) } } txs := block.Transactions() @@ -1439,7 +1562,7 @@ } if block.Header().RequestsHash != nil { fields["requests"] = block.Requests() } - return fields + return fields, nil }   // RPCTransaction represents a transaction that will serialize to the RPC representation of a transaction @@ -1466,11 +1589,18 @@ V *hexutil.Big `json:"v"` R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` YParity *hexutil.Uint64 `json:"yParity,omitempty"` + + // deposit-tx only + SourceHash *common.Hash `json:"sourceHash,omitempty"` + Mint *hexutil.Big `json:"mint,omitempty"` + IsSystemTx *bool `json:"isSystemTx,omitempty"` + // deposit-tx post-Canyon only + DepositReceiptVersion *hexutil.Uint64 `json:"depositReceiptVersion,omitempty"` }   // newRPCTransaction returns a transaction that will serialize to the RPC // representation, with the given location metadata set (if available). -func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, blockTime uint64, index uint64, baseFee *big.Int, config *params.ChainConfig) *RPCTransaction { +func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, blockTime uint64, index uint64, baseFee *big.Int, config *params.ChainConfig, receipt *types.Receipt) *RPCTransaction { signer := types.MakeSigner(config, new(big.Int).SetUint64(blockNumber), blockTime) from, _ := types.Sender(signer, tx) v, r, s := tx.RawSignatureValues() @@ -1495,7 +1625,27 @@ result.TransactionIndex = (*hexutil.Uint64)(&index) }   switch tx.Type() { + case types.DepositTxType: + srcHash := tx.SourceHash() + isSystemTx := tx.IsSystemTx() + result.SourceHash = &srcHash + if isSystemTx { + // Only include IsSystemTx when true + result.IsSystemTx = &isSystemTx + } + result.Mint = (*hexutil.Big)(tx.Mint()) + if receipt != nil && receipt.DepositNonce != nil { + result.Nonce = hexutil.Uint64(*receipt.DepositNonce) + if receipt.DepositReceiptVersion != nil { + result.DepositReceiptVersion = new(hexutil.Uint64) + *result.DepositReceiptVersion = hexutil.Uint64(*receipt.DepositReceiptVersion) + } + } case types.LegacyTxType: + if v.Sign() == 0 && r.Sign() == 0 && s.Sign() == 0 { // pre-bedrock relayed tx does not have a signature + result.ChainID = (*hexutil.Big)(new(big.Int).Set(config.ChainID)) + break + } // if a legacy transaction has an EIP-155 chain id, include it explicitly if id := tx.ChainId(); id.Sign() != 0 { result.ChainID = (*hexutil.Big)(id) @@ -1564,20 +1714,36 @@ blockNumber = uint64(0) blockTime = uint64(0) ) if current != nil { - baseFee = eip1559.CalcBaseFee(config, current) + baseFee = eip1559.CalcBaseFee(config, current, current.Time+1) blockNumber = current.Number.Uint64() blockTime = current.Time } - return newRPCTransaction(tx, common.Hash{}, blockNumber, blockTime, 0, baseFee, config) + return newRPCTransaction(tx, common.Hash{}, blockNumber, blockTime, 0, baseFee, config, nil) }   // newRPCTransactionFromBlockIndex returns a transaction that will serialize to the RPC representation. -func newRPCTransactionFromBlockIndex(b *types.Block, index uint64, config *params.ChainConfig) *RPCTransaction { +func newRPCTransactionFromBlockIndex(ctx context.Context, b *types.Block, index uint64, config *params.ChainConfig, backend Backend) *RPCTransaction { txs := b.Transactions() if index >= uint64(len(txs)) { return nil } - return newRPCTransaction(txs[index], b.Hash(), b.NumberU64(), b.Time(), index, b.BaseFee(), config) + tx := txs[index] + rcpt := depositTxReceipt(ctx, b.Hash(), index, backend, tx) + return newRPCTransaction(tx, b.Hash(), b.NumberU64(), b.Time(), index, b.BaseFee(), config, rcpt) +} + +func depositTxReceipt(ctx context.Context, blockHash common.Hash, index uint64, backend Backend, tx *types.Transaction) *types.Receipt { + if tx.Type() != types.DepositTxType { + return nil + } + receipts, err := backend.GetReceipts(ctx, blockHash) + if err != nil { + return nil + } + if index >= uint64(len(receipts)) { + return nil + } + return receipts[index] }   // newRPCRawTransactionFromBlockIndex returns the bytes of a transaction given a block and a transaction index. @@ -1606,6 +1772,21 @@ bNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) if blockNrOrHash != nil { bNrOrHash = *blockNrOrHash } + + header, err := headerByNumberOrHash(ctx, api.b, bNrOrHash) + if err == nil && header != nil && api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res accessListResult + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_createAccessList", args, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return &res, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + acl, gasUsed, vmerr, err := AccessList(ctx, api.b, bNrOrHash, args) if err != nil { return nil, err @@ -1719,7 +1900,7 @@ // GetTransactionByBlockNumberAndIndex returns the transaction for the given block number and index. func (api *TransactionAPI) GetTransactionByBlockNumberAndIndex(ctx context.Context, blockNr rpc.BlockNumber, index hexutil.Uint) *RPCTransaction { if block, _ := api.b.BlockByNumber(ctx, blockNr); block != nil { - return newRPCTransactionFromBlockIndex(block, uint64(index), api.b.ChainConfig()) + return newRPCTransactionFromBlockIndex(ctx, block, uint64(index), api.b.ChainConfig(), api.b) } return nil } @@ -1727,7 +1908,7 @@ // GetTransactionByBlockHashAndIndex returns the transaction for the given block hash and index. func (api *TransactionAPI) GetTransactionByBlockHashAndIndex(ctx context.Context, blockHash common.Hash, index hexutil.Uint) *RPCTransaction { if block, _ := api.b.BlockByHash(ctx, blockHash); block != nil { - return newRPCTransactionFromBlockIndex(block, uint64(index), api.b.ChainConfig()) + return newRPCTransactionFromBlockIndex(ctx, block, uint64(index), api.b.ChainConfig(), api.b) } return nil } @@ -1759,10 +1940,29 @@ } return (*hexutil.Uint64)(&nonce), nil } // Resolve block number and use its state to ask for the nonce + header, err := headerByNumberOrHash(ctx, api.b, blockNrOrHash) + if err != nil { + return nil, err + } + + if api.b.ChainConfig().IsOptimismPreBedrock(header.Number) { + if api.b.HistoricalRPCService() != nil { + var res hexutil.Uint64 + err := api.b.HistoricalRPCService().CallContext(ctx, &res, "eth_getTransactionCount", address, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return &res, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil, err } + nonce := state.GetNonce(address) return (*hexutil.Uint64)(&nonce), state.Error() } @@ -1785,7 +1985,8 @@ header, err := api.b.HeaderByHash(ctx, blockHash) if err != nil { return nil, err } - return newRPCTransaction(tx, blockHash, blockNumber, header.Time, index, header.BaseFee, api.b.ChainConfig()), nil + rcpt := depositTxReceipt(ctx, blockHash, index, api.b, tx) + return newRPCTransaction(tx, blockHash, blockNumber, header.Time, index, header.BaseFee, api.b.ChainConfig(), rcpt), nil }   // GetRawTransactionByHash returns the bytes of the transaction for the given hash. @@ -1828,11 +2029,11 @@ receipt := receipts[index]   // Derive the sender. signer := types.MakeSigner(api.b.ChainConfig(), header.Number, header.Time) - return marshalReceipt(receipt, blockHash, blockNumber, signer, tx, int(index)), nil + return marshalReceipt(receipt, blockHash, blockNumber, signer, tx, int(index), api.b.ChainConfig()), nil }   // marshalReceipt marshals a transaction receipt into a JSON object. -func marshalReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber uint64, signer types.Signer, tx *types.Transaction, txIndex int) map[string]interface{} { +func marshalReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber uint64, signer types.Signer, tx *types.Transaction, txIndex int, chainConfig *params.ChainConfig) map[string]interface{} { from, _ := types.Sender(signer, tx)   fields := map[string]interface{}{ @@ -1851,6 +2052,32 @@ "type": hexutil.Uint(tx.Type()), "effectiveGasPrice": (*hexutil.Big)(receipt.EffectiveGasPrice), }   + if chainConfig.Optimism != nil && !tx.IsDepositTx() { + fields["l1GasPrice"] = (*hexutil.Big)(receipt.L1GasPrice) + fields["l1GasUsed"] = (*hexutil.Big)(receipt.L1GasUsed) + fields["l1Fee"] = (*hexutil.Big)(receipt.L1Fee) + // Fields removed with Ecotone + if receipt.FeeScalar != nil { + fields["l1FeeScalar"] = receipt.FeeScalar.String() + } + // Fields added in Ecotone + if receipt.L1BlobBaseFee != nil { + fields["l1BlobBaseFee"] = (*hexutil.Big)(receipt.L1BlobBaseFee) + } + if receipt.L1BaseFeeScalar != nil { + fields["l1BaseFeeScalar"] = hexutil.Uint64(*receipt.L1BaseFeeScalar) + } + if receipt.L1BlobBaseFeeScalar != nil { + fields["l1BlobBaseFeeScalar"] = hexutil.Uint64(*receipt.L1BlobBaseFeeScalar) + } + } + if chainConfig.Optimism != nil && tx.IsDepositTx() && receipt.DepositNonce != nil { + fields["depositNonce"] = hexutil.Uint64(*receipt.DepositNonce) + if receipt.DepositReceiptVersion != nil { + fields["depositReceiptVersion"] = hexutil.Uint64(*receipt.DepositReceiptVersion) + } + } + // Assign receipt status or post state. if len(receipt.PostState) > 0 { fields["root"] = hexutil.Bytes(receipt.PostState) @@ -2023,6 +2250,9 @@ return nil, errors.New("gas not specified") } if args.GasPrice == nil && (args.MaxPriorityFeePerGas == nil || args.MaxFeePerGas == nil) { return nil, errors.New("missing gasPrice or maxFeePerGas/maxPriorityFeePerGas") + } + if args.IsEIP4844() { + return nil, errBlobTxNotSupported } if args.Nonce == nil { return nil, errors.New("nonce not specified") @@ -2264,6 +2494,10 @@ func (api *DebugAPI) SetHead(number hexutil.Uint64) { api.b.SetHead(uint64(number)) }   +func (api *DebugAPI) ChainConfig() *params.ChainConfig { + return api.b.ChainConfig() +} + // NetAPI offers network related RPC methods type NetAPI struct { net *p2p.Server @@ -2304,3 +2538,8 @@ return fmt.Errorf("tx fee (%.2f ether) exceeds the configured cap (%.2f ether)", feeFloat, cap) } return nil } + +// CheckTxFee exports a helper function used to check whether the fee is reasonable +func CheckTxFee(gasPrice *big.Int, gas uint64, cap float64) error { + return checkTxFee(gasPrice, gas, cap) +}
diff --git go-ethereum/rpc/errors.go op-geth/rpc/errors.go index 438aff218c2e306d322160d8be8dcac16dd0dfa2..67c523d11a36294e4448988dc4c906f38541ec04 100644 --- go-ethereum/rpc/errors.go +++ op-geth/rpc/errors.go @@ -73,6 +73,16 @@ errMsgResponseTooLarge = "response too large" errMsgBatchTooLarge = "batch too large" )   +var ErrNoHistoricalFallback = NoHistoricalFallbackError{} + +type NoHistoricalFallbackError struct{} + +func (e NoHistoricalFallbackError) ErrorCode() int { return -32801 } + +func (e NoHistoricalFallbackError) Error() string { + return "no historical RPC is available for this historical (pre-bedrock) execution request" +} + type methodNotFoundError struct{ method string }   func (e *methodNotFoundError) ErrorCode() int { return -32601 }

Forward pre-bedrock tracing calls to legacy node.

diff --git go-ethereum/eth/tracers/api.go op-geth/eth/tracers/api.go index a8289512069b3dd1d367d422114dcd40231895c3..d77b4886980e586092e9d0d65a3de85e6c0366c3 100644 --- go-ethereum/eth/tracers/api.go +++ op-geth/eth/tracers/api.go @@ -68,8 +68,6 @@ // trace states exceed this limit. maximumPendingTraceStates = 128 )   -var errTxNotFound = errors.New("transaction not found") - // StateReleaseFunc is used to deallocate resources held by constructing a // historical state for tracing purposes. type StateReleaseFunc func() @@ -88,6 +86,7 @@ Engine() consensus.Engine ChainDb() ethdb.Database StateAtBlock(ctx context.Context, block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (*state.StateDB, StateReleaseFunc, error) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) + HistoricalRPCService() *rpc.Client }   // API is the collection of tracing APIs exposed over the private debugging endpoint. @@ -209,6 +208,7 @@ // TraceChain returns the structured logs created during the execution of EVM // between two blocks (excluding start) and returns them as a JSON object. func (api *API) TraceChain(ctx context.Context, start, end rpc.BlockNumber, config *TraceConfig) (*rpc.Subscription, error) { // Fetch the block interval that we want to trace + // TODO: Need to implement a fallback for this from, err := api.blockByNumber(ctx, start) if err != nil { return nil, err @@ -267,7 +267,7 @@ // Fetch and execute the block trace taskCh for task := range taskCh { var ( signer = types.MakeSigner(api.backend.ChainConfig(), task.block.Number(), task.block.Time()) - blockCtx = core.NewEVMBlockContext(task.block.Header(), api.chainContext(ctx), nil) + blockCtx = core.NewEVMBlockContext(task.block.Header(), api.chainContext(ctx), nil, api.backend.ChainConfig(), task.statedb) ) // Trace all the transactions contained within for i, tx := range task.block.Transactions() { @@ -379,13 +379,13 @@ } // Insert block's parent beacon block root in the state // as per EIP-4788. if beaconRoot := next.BeaconRoot(); beaconRoot != nil { - context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil) + context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil, api.backend.ChainConfig(), statedb) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, api.backend.ChainConfig(), vm.Config{}) core.ProcessBeaconBlockRoot(*beaconRoot, vmenv, statedb) } // Insert parent hash in history contract. if api.backend.ChainConfig().IsPrague(next.Number(), next.Time()) { - context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil) + context := core.NewEVMBlockContext(next.Header(), api.chainContext(ctx), nil, api.backend.ChainConfig(), statedb) vmenv := vm.NewEVM(context, vm.TxContext{}, statedb, api.backend.ChainConfig(), vm.Config{}) core.ProcessParentBlockHash(next.ParentHash(), vmenv, statedb) } @@ -448,6 +448,20 @@ block, err := api.blockByNumber(ctx, number) if err != nil { return nil, err } + + if api.backend.ChainConfig().IsOptimismPreBedrock(block.Number()) { + if api.backend.HistoricalRPCService() != nil { + var histResult []*txTraceResult + err = api.backend.HistoricalRPCService().CallContext(ctx, &histResult, "debug_traceBlockByNumber", number, config) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return histResult, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + return api.traceBlock(ctx, block, config) }   @@ -458,6 +472,20 @@ block, err := api.blockByHash(ctx, hash) if err != nil { return nil, err } + + if api.backend.ChainConfig().IsOptimismPreBedrock(block.Number()) { + if api.backend.HistoricalRPCService() != nil { + var histResult []*txTraceResult + err = api.backend.HistoricalRPCService().CallContext(ctx, &histResult, "debug_traceBlockByHash", hash, config) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return histResult, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } + } + return api.traceBlock(ctx, block, config) }   @@ -507,6 +535,7 @@ // IntermediateRoots executes a block (bad- or canon- or side-), and returns a list // of intermediate roots: the stateroot after each transaction. func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config *TraceConfig) ([]common.Hash, error) { block, _ := api.blockByHash(ctx, hash) + // TODO: Cannot get intermediate roots for pre-bedrock block without daisy chain if block == nil { // Check in the bad blocks block = rawdb.ReadBadBlock(api.backend.ChainDb(), hash) @@ -534,7 +563,7 @@ var ( roots []common.Hash signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) chainConfig = api.backend.ChainConfig() - vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, chainConfig, statedb) deleteEmptyObjects = chainConfig.IsEIP158(block.Number()) ) if beaconRoot := block.BeaconRoot(); beaconRoot != nil { @@ -615,7 +644,7 @@ // Native tracers have low overhead var ( txs = block.Transactions() blockHash = block.Hash() - blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, api.backend.ChainConfig(), statedb) signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) results = make([]*txTraceResult, len(txs)) ) @@ -679,7 +708,7 @@ // Reconstruct the block context for each transaction // as the GetHash function of BlockContext is not safe for // concurrent use. // See: https://github.com/ethereum/go-ethereum/issues/29114 - blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, api.backend.ChainConfig(), task.statedb) res, err := api.traceTx(ctx, txs[task.index], msg, txctx, blockCtx, task.statedb, config) if err != nil { results[task.index] = &txTraceResult{TxHash: txs[task.index].Hash(), Error: err.Error()} @@ -692,7 +721,7 @@ }   // Feed the transactions into the tracers and return var failed error - blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, api.backend.ChainConfig(), statedb) txloop: for i, tx := range txs { // Send the trace task over for execution @@ -769,7 +798,7 @@ var ( dumps []string signer = types.MakeSigner(api.backend.ChainConfig(), block.Number(), block.Time()) chainConfig = api.backend.ChainConfig() - vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, chainConfig, statedb) canon = true ) // Check if there are any overrides: the caller may wish to enable a future @@ -865,13 +894,22 @@ // TraceTransaction returns the structured logs created during the execution of EVM // and returns them as a JSON object. func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *TraceConfig) (interface{}, error) { - found, _, blockHash, blockNumber, index, err := api.backend.GetTransaction(ctx, hash) + _, _, blockHash, blockNumber, index, err := api.backend.GetTransaction(ctx, hash) if err != nil { return nil, ethapi.NewTxIndexingError() } - // Only mined txes are supported - if !found { - return nil, errTxNotFound + + if api.backend.ChainConfig().IsOptimismPreBedrock(new(big.Int).SetUint64(blockNumber)) { + if api.backend.HistoricalRPCService() != nil { + var histResult json.RawMessage + err := api.backend.HistoricalRPCService().CallContext(ctx, &histResult, "debug_traceTransaction", hash, config) + if err != nil { + return nil, fmt.Errorf("historical backend error: %w", err) + } + return histResult, nil + } else { + return nil, rpc.ErrNoHistoricalFallback + } } // It shouldn't happen in practice. if blockNumber == 0 { @@ -937,6 +975,11 @@ } if err != nil { return nil, err } + + if api.backend.ChainConfig().IsOptimismPreBedrock(block.Number()) { + return nil, errors.New("l2geth does not have a debug_traceCall method") + } + // try to recompute the state reexec := defaultTraceReexec if config != nil && config.Reexec != nil { @@ -953,7 +996,7 @@ return nil, err } defer release()   - vmctx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + vmctx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil, api.backend.ChainConfig(), statedb) // Apply the customization rules if required. if config != nil { config.BlockOverrides.Apply(&vmctx)
diff --git go-ethereum/eth/tracers/api_test.go op-geth/eth/tracers/api_test.go index 47e36934953bb4339e9ceb52f97c6907e5ab3731..2ea4f196f0f77966cf549cdc2a01526dddd4da02 100644 --- go-ethereum/eth/tracers/api_test.go +++ op-geth/eth/tracers/api_test.go @@ -23,12 +23,15 @@ "encoding/json" "errors" "fmt" "math/big" + "net" + "net/http" "reflect" "slices" "sync/atomic" "testing" "time"   + "github.com/davecgh/go-spew/spew" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" @@ -43,8 +46,10 @@ "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth/tracers/logger" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" + "github.com/stretchr/testify/mock" )   var ( @@ -52,6 +57,66 @@ errStateNotFound = errors.New("state not found") errBlockNotFound = errors.New("block not found") )   +type mockHistoricalBackend struct { + mock.Mock +} + +// mockHistoricalBackend does not have a TraceCall, because pre-bedrock there is no debug_traceCall available + +func (m *mockHistoricalBackend) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *TraceConfig) ([]*txTraceResult, error) { + ret := m.Mock.MethodCalled("TraceBlockByNumber", number, config) + return ret[0].([]*txTraceResult), *ret[1].(*error) +} + +func (m *mockHistoricalBackend) ExpectTraceBlockByNumber(number rpc.BlockNumber, config *TraceConfig, out []*txTraceResult, err error) { + m.Mock.On("TraceBlockByNumber", number, config).Once().Return(out, &err) +} + +func (m *mockHistoricalBackend) TraceTransaction(ctx context.Context, hash common.Hash, config *TraceConfig) (interface{}, error) { + ret := m.Mock.MethodCalled("TraceTransaction", hash, config) + return ret[0], *ret[1].(*error) +} + +func (m *mockHistoricalBackend) ExpectTraceTransaction(hash common.Hash, config *TraceConfig, out interface{}, err error) { + jsonOut, _ := json.Marshal(out) + m.Mock.On("TraceTransaction", hash, config).Once().Return(json.RawMessage(jsonOut), &err) +} + +func newMockHistoricalBackend(t *testing.T, backend *mockHistoricalBackend) string { + s := rpc.NewServer() + err := node.RegisterApis([]rpc.API{ + { + Namespace: "debug", + Service: backend, + Public: true, + Authenticated: false, + }, + }, nil, s) + if err != nil { + t.Fatalf("error creating mock historical backend: %v", err) + } + + hdlr := node.NewHTTPHandlerStack(s, []string{"*"}, []string{"*"}, nil) + mux := http.NewServeMux() + mux.Handle("/", hdlr) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("error creating mock historical backend listener: %v", err) + } + + go func() { + httpS := &http.Server{Handler: mux} + httpS.Serve(listener) + + t.Cleanup(func() { + httpS.Shutdown(context.Background()) + }) + }() + + return fmt.Sprintf("http://%s", listener.Addr().String()) +} + type testBackend struct { chainConfig *params.ChainConfig engine consensus.Engine @@ -60,15 +125,28 @@ chain *core.BlockChain   refHook func() // Hook is invoked when the requested state is referenced relHook func() // Hook is invoked when the requested state is released + + historical *rpc.Client + mockHistorical *mockHistoricalBackend }   // newTestBackend creates a new test backend. OBS: After test is done, teardown must be // invoked in order to release associated resources. func newTestBackend(t *testing.T, n int, gspec *core.Genesis, generator func(i int, b *core.BlockGen)) *testBackend { + mock := new(mockHistoricalBackend) + historicalAddr := newMockHistoricalBackend(t, mock) + + historicalClient, err := rpc.Dial(historicalAddr) + if err != nil { + t.Fatalf("error making historical client: %v", err) + } + backend := &testBackend{ - chainConfig: gspec.Config, - engine: ethash.NewFaker(), - chaindb: rawdb.NewMemoryDatabase(), + chainConfig: gspec.Config, + engine: ethash.NewFaker(), + chaindb: rawdb.NewMemoryDatabase(), + historical: historicalClient, + mockHistorical: mock, } // Generate blocks for testing _, blocks, _ := core.GenerateChainWithGenesis(gspec, backend.engine, n, generator) @@ -173,7 +251,7 @@ signer := types.MakeSigner(b.chainConfig, block.Number(), block.Time()) for idx, tx := range block.Transactions() { msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) txContext := core.NewEVMTxContext(msg) - context := core.NewEVMBlockContext(block.Header(), b.chain, nil) + context := core.NewEVMBlockContext(block.Header(), b.chain, nil, b.chainConfig, statedb) if idx == txIndex { return tx, context, statedb, release, nil } @@ -184,6 +262,10 @@ } statedb.Finalise(vmenv.ChainConfig().IsEIP158(block.Number())) } return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction index %d out of range for block %#x", txIndex, block.Hash()) +} + +func (b *testBackend) HistoricalRPCService() *rpc.Client { + return b.historical }   func TestTraceCall(t *testing.T) { @@ -451,11 +533,58 @@ StructLogs: []logger.StructLogRes{}, }) { t.Error("Transaction tracing result is different") } +} +func TestTraceTransactionHistorical(t *testing.T) { + t.Parallel()   - // Test non-existent transaction - _, err = api.TraceTransaction(context.Background(), common.Hash{42}, nil) - if !errors.Is(err, errTxNotFound) { - t.Fatalf("want %v, have %v", errTxNotFound, err) + // Initialize test accounts + accounts := newAccounts(2) + genesis := &core.Genesis{ + Config: params.OptimismTestConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + target := common.Hash{} + signer := types.HomesteadSigner{} + backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + // value: 1000 wei + // fee: 0 wei + tx, _ := types.SignTx(types.NewTransaction(uint64(i), accounts[1].addr, big.NewInt(1000), params.TxGas, b.BaseFee(), nil), signer, accounts[0].key) + b.AddTx(tx) + target = tx.Hash() + }) + defer backend.mockHistorical.AssertExpectations(t) + defer backend.chain.Stop() + backend.mockHistorical.ExpectTraceTransaction( + target, + nil, + logger.ExecutionResult{ + Gas: params.TxGas, + Failed: false, + ReturnValue: "", + StructLogs: []logger.StructLogRes{}, + }, + nil) + api := NewAPI(backend) + result, err := api.TraceTransaction(context.Background(), target, nil) + if err != nil { + t.Errorf("Failed to trace transaction %v", err) + } + var have *logger.ExecutionResult + spew.Dump(result) + if err := json.Unmarshal(result.(json.RawMessage), &have); err != nil { + t.Errorf("failed to unmarshal result %v", err) + } + if !reflect.DeepEqual(have, &logger.ExecutionResult{ + Gas: params.TxGas, + Failed: false, + ReturnValue: "", + StructLogs: []logger.StructLogRes{}, + }) { + t.Error("Transaction tracing result is different") } }   @@ -546,6 +675,59 @@ want := tc.want if string(have) != want { t.Errorf("test %d, result mismatch, have\n%v\n, want\n%v\n", i, string(have), want) } + } +} + +func TestTraceBlockHistorical(t *testing.T) { + t.Parallel() + + // Initialize test accounts + accounts := newAccounts(3) + genesis := &core.Genesis{ + Config: params.OptimismTestConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + accounts[2].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + genBlocks := 10 + signer := types.HomesteadSigner{} + backend := newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + // value: 1000 wei + // fee: 0 wei + tx, _ := types.SignTx(types.NewTransaction(uint64(i), accounts[1].addr, big.NewInt(1000), params.TxGas, b.BaseFee(), nil), signer, accounts[0].key) + b.AddTx(tx) + }) + defer backend.mockHistorical.AssertExpectations(t) + defer backend.chain.Stop() + api := NewAPI(backend) + + var expectErr error + var config *TraceConfig + blockNumber := rpc.BlockNumber(3) + want := `[{"txHash":"0x0000000000000000000000000000000000000000000000000000000000000000","result":{"failed":false,"gas":21000,"returnValue":"","structLogs":[]}}]` + var ret []*txTraceResult + _ = json.Unmarshal([]byte(want), &ret) + + backend.mockHistorical.ExpectTraceBlockByNumber(blockNumber, config, ret, nil) + + result, err := api.TraceBlockByNumber(context.Background(), blockNumber, config) + if expectErr != nil { + if err == nil { + t.Errorf("want error %v", expectErr) + } + if !reflect.DeepEqual(err, expectErr) { + t.Errorf("error mismatch, want %v, get %v", expectErr, err) + } + } + if err != nil { + t.Errorf("want no error, have %v", err) + } + have, _ := json.Marshal(result) + if string(have) != want { + t.Errorf("result mismatch, have\n%v\n, want\n%v\n", string(have), want) } }
diff --git go-ethereum/ethclient/ethclient_test.go op-geth/ethclient/ethclient_test.go index 1b7e26fb74f50fd0ca37ae05aba5ba588f78a60a..8b627d3845883d20a3174ccd6279b8361023c24a 100644 --- go-ethereum/ethclient/ethclient_test.go +++ op-geth/ethclient/ethclient_test.go @@ -20,10 +20,18 @@ import ( "bytes" "context" "errors" + "fmt" "math/big" + "net" + "net/http" "reflect" "testing" "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/consensus/beacon" + "github.com/ethereum/go-ethereum/internal/ethapi"   "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" @@ -193,6 +201,21 @@ Timestamp: 9000, BaseFee: big.NewInt(params.InitialBaseFee), }   +var genesisForHistorical = &core.Genesis{ + Config: params.OptimismTestConfig, + Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}}, + ExtraData: []byte("test genesis"), + Timestamp: 9000, + BaseFee: big.NewInt(params.InitialBaseFee), +} + +var depositTx = types.NewTx(&types.DepositTx{ + Value: big.NewInt(12), + Gas: params.TxGas + 2000, + To: &common.Address{2}, + Data: make([]byte, 500), +}) + var testTx1 = types.MustSignNewTx(testKey, types.LatestSigner(genesis.Config), &types.LegacyTx{ Nonce: 0, Value: big.NewInt(12), @@ -209,9 +232,77 @@ Gas: params.TxGas, To: &common.Address{2}, })   -func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { - // Generate test chain. - blocks := generateTestChain() +type mockHistoricalBackend struct{} + +func (m *mockHistoricalBackend) Call(ctx context.Context, args ethapi.TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *ethapi.StateOverride) (hexutil.Bytes, error) { + num, ok := blockNrOrHash.Number() + if ok && num == 1 { + return hexutil.Bytes("test"), nil + } + return nil, ethereum.NotFound +} + +func (m *mockHistoricalBackend) EstimateGas(ctx context.Context, args ethapi.TransactionArgs, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Uint64, error) { + num, ok := blockNrOrHash.Number() + if ok && num == 1 { + return hexutil.Uint64(12345), nil + } + return 0, ethereum.NotFound +} + +func newMockHistoricalBackend(t *testing.T) string { + s := rpc.NewServer() + err := node.RegisterApis([]rpc.API{ + { + Namespace: "eth", + Service: new(mockHistoricalBackend), + Public: true, + Authenticated: false, + }, + }, nil, s) + if err != nil { + t.Fatalf("error creating mock historical backend: %v", err) + } + + hdlr := node.NewHTTPHandlerStack(s, []string{"*"}, []string{"*"}, nil) + mux := http.NewServeMux() + mux.Handle("/", hdlr) + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("error creating mock historical backend listener: %v", err) + } + + go func() { + httpS := &http.Server{Handler: mux} + httpS.Serve(listener) + + t.Cleanup(func() { + httpS.Shutdown(context.Background()) + }) + }() + + return fmt.Sprintf("http://%s", listener.Addr().String()) +} + +func newTestBackend(t *testing.T, enableHistoricalState bool) (*node.Node, []*types.Block) { + histAddr := newMockHistoricalBackend(t) + + var consensusEngine consensus.Engine + var actualGenesis *core.Genesis + var chainLength int + if enableHistoricalState { + actualGenesis = genesisForHistorical + consensusEngine = beacon.New(ethash.NewFaker()) + chainLength = 10 + } else { + actualGenesis = genesis + consensusEngine = ethash.NewFaker() + chainLength = 2 + } + + // Generate test chain + blocks := generateTestChain(consensusEngine, actualGenesis, chainLength)   // Create node n, err := node.New(&node.Config{}) @@ -219,11 +310,18 @@ if err != nil { t.Fatalf("can't create new node: %v", err) } // Create Ethereum Service - config := &ethconfig.Config{Genesis: genesis, RPCGasCap: 1000000} + config := &ethconfig.Config{Genesis: actualGenesis, RPCGasCap: 1000000} + if enableHistoricalState { + config.RollupHistoricalRPC = histAddr + config.RollupHistoricalRPCTimeout = time.Second * 5 + } ethservice, err := eth.New(n, config) if err != nil { t.Fatalf("can't create new ethereum service: %v", err) } + if enableHistoricalState { // swap to the pre-bedrock consensus-engine that we used to generate the historical blocks + ethservice.BlockChain().Engine().(*beacon.Beacon).SwapInner(ethash.NewFaker()) + } // Import the test chain. if err := n.Start(); err != nil { t.Fatalf("can't start test node: %v", err) @@ -231,6 +329,7 @@ } if _, err := ethservice.BlockChain().InsertChain(blocks[1:]); err != nil { t.Fatalf("can't import test blocks: %v", err) } + // Ensure the tx indexing is fully generated for ; ; time.Sleep(time.Millisecond * 100) { progress, err := ethservice.BlockChain().TxIndexProgress() @@ -238,25 +337,44 @@ if err == nil && progress.Done() { break } } + + if enableHistoricalState { + // Now that we have a filled DB, swap the pre-Bedrock consensus to OpLegacy, + // which does not support re-processing of pre-bedrock data. + ethservice.Engine().(*beacon.Beacon).SwapInner(&beacon.OpLegacy{}) + } + return n, blocks }   -func generateTestChain() []*types.Block { +func generateTestChain(consensusEngine consensus.Engine, genesis *core.Genesis, length int) []*types.Block { generate := func(i int, g *core.BlockGen) { g.OffsetTime(5) g.SetExtra([]byte("test")) if i == 1 { // Test transactions are included in block #2. + if genesis.Config.Optimism != nil && genesis.Config.IsBedrock(big.NewInt(1)) { + g.AddTx(depositTx) + } g.AddTx(testTx1) g.AddTx(testTx2) } } - _, blocks, _ := core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), 2, generate) + _, blocks, _ := core.GenerateChainWithGenesis(genesis, consensusEngine, length, generate) return append([]*types.Block{genesis.ToBlock()}, blocks...) }   +func TestEthClientHistoricalBackend(t *testing.T) { + backend, _ := newTestBackend(t, true) + client := backend.Attach() + defer backend.Close() + defer client.Close() + + testHistoricalRPC(t, client) +} + func TestEthClient(t *testing.T) { - backend, chain := newTestBackend(t) + backend, chain := newTestBackend(t, false) client := backend.Attach() defer backend.Close() defer client.Close() @@ -293,6 +411,9 @@ func(t *testing.T) { testAtFunctions(t, client) }, }, "TransactionSender": { func(t *testing.T) { testTransactionSender(t, client) }, + }, + "EstimateGas": { + func(t *testing.T) { testEstimateGas(t, client) }, }, }   @@ -734,6 +855,54 @@ t.Fatal(err) } if sender2 != testAddr { t.Fatal("wrong sender:", sender2) + } +} + +func testEstimateGas(t *testing.T, client *rpc.Client) { + ec := NewClient(client) + + // EstimateGas + msg := ethereum.CallMsg{ + From: testAddr, + To: &common.Address{}, + Gas: 21000, + Value: big.NewInt(1), + } + gas, err := ec.EstimateGas(context.Background(), msg) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gas != 21000 { + t.Fatalf("unexpected gas price: %v", gas) + } +} + +func testHistoricalRPC(t *testing.T, client *rpc.Client) { + ec := NewClient(client) + + // Estimate Gas RPC + msg := ethereum.CallMsg{ + From: testAddr, + To: &common.Address{}, + Gas: 21000, + Value: big.NewInt(1), + } + var res hexutil.Uint64 + err := client.CallContext(context.Background(), &res, "eth_estimateGas", toCallArg(msg), rpc.BlockNumberOrHashWithNumber(1)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != 12345 { + t.Fatalf("invalid result: %d", res) + } + + // Call Contract RPC + histVal, err := ec.CallContract(context.Background(), msg, big.NewInt(1)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(histVal) != "test" { + t.Fatalf("expected %s to equal test", string(histVal)) } }
diff --git go-ethereum/internal/ethapi/transaction_args_test.go op-geth/internal/ethapi/transaction_args_test.go index 531782817328c0704bd8aff42745ffba80d12018..8b9b7fb66a8127aaea9d7bf0f322ee421c92ad92 100644 --- go-ethereum/internal/ethapi/transaction_args_test.go +++ op-geth/internal/ethapi/transaction_args_test.go @@ -404,4 +404,6 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription { return nil }   -func (b *backendMock) Engine() consensus.Engine { return nil } +func (b *backendMock) Engine() consensus.Engine { return nil } +func (b *backendMock) HistoricalRPCService() *rpc.Client { return nil } +func (b *backendMock) Genesis() *types.Block { return nil }

Fix Debug API block marshaling to include deposits

diff --git go-ethereum/eth/api_debug.go op-geth/eth/api_debug.go index d5e4dda1401c9ae49a754077f837d0ebed760310..203764a00eb35145fa1577f7afdc2cfbf25a8818 100644 --- go-ethereum/eth/api_debug.go +++ op-geth/eth/api_debug.go @@ -24,8 +24,10 @@ "time"   "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/stateless" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/internal/ethapi" @@ -119,7 +121,10 @@ blockRlp = err.Error() // Hacky, but hey, it works } else { blockRlp = fmt.Sprintf("%#x", rlpBytes) } - blockJSON = ethapi.RPCMarshalBlock(block, true, true, api.eth.APIBackend.ChainConfig()) + var err error + if blockJSON, err = ethapi.RPCMarshalBlock(ctx, block, true, true, api.eth.APIBackend.ChainConfig(), api.eth.APIBackend); err != nil { + blockJSON = map[string]interface{}{"error": err.Error()} + } results = append(results, &BadBlockArgs{ Hash: block.Hash(), RLP: blockRlp, @@ -443,3 +448,46 @@ return "", errors.New("trie flush interval is undefined for path-based scheme") } return api.eth.blockchain.GetTrieFlushInterval().String(), nil } + +func (api *DebugAPI) ExecutionWitness(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*stateless.ExecutionWitness, error) { + block, err := api.eth.APIBackend.BlockByNumberOrHash(ctx, blockNrOrHash) + if err != nil { + return nil, fmt.Errorf("failed to retrieve block: %w", err) + } + if block == nil { + return nil, fmt.Errorf("block not found: %s", blockNrOrHash.String()) + } + + witness, err := generateWitness(api.eth.blockchain, block) + return witness.ToExecutionWitness(), err +} + +func generateWitness(blockchain *core.BlockChain, block *types.Block) (*stateless.Witness, error) { + witness, err := stateless.NewWitness(block.Header(), blockchain) + if err != nil { + return nil, fmt.Errorf("failed to create witness: %w", err) + } + + parentHeader := witness.Headers[0] + statedb, err := blockchain.StateAt(parentHeader.Root) + if err != nil { + return nil, fmt.Errorf("failed to retrieve parent state: %w", err) + } + + statedb.StartPrefetcher("debug_execution_witness", witness) + defer statedb.StopPrefetcher() + + res, err := blockchain.Processor().Process(block, statedb, *blockchain.GetVMConfig()) + if err != nil { + return nil, fmt.Errorf("failed to process block %d: %w", block.Number(), err) + } + + // OP-Stack warning: below has the side-effect of including the withdrawals storage-root + // into the execution witness through the storage lookup by ValidateState, triggering the pre-fetcher. + // The Process function only runs through Finalize steps, not through FinalizeAndAssemble, missing merkleization. + if err := blockchain.Validator().ValidateState(block, statedb, res, false); err != nil { + return nil, fmt.Errorf("failed to validate block %d: %w", block.Number(), err) + } + + return witness, nil +}

gasprice suggestion adjustments to accommodate faster L2 blocks and lower fees.

diff --git go-ethereum/eth/gasprice/gasprice.go op-geth/eth/gasprice/gasprice.go index 19a6c0010a6081e23793006aec7aaff0c758e0e8..79d49cf0e41703f9465a30c5c0b45897629d648e 100644 --- go-ethereum/eth/gasprice/gasprice.go +++ op-geth/eth/gasprice/gasprice.go @@ -38,6 +38,8 @@ var ( DefaultMaxPrice = big.NewInt(500 * params.GWei) DefaultIgnorePrice = big.NewInt(2 * params.Wei) + + DefaultMinSuggestedPriorityFee = big.NewInt(1e6 * params.Wei) // 0.001 gwei, for Optimism fee suggestion )   type Config struct { @@ -47,6 +49,8 @@ MaxHeaderHistory uint64 MaxBlockHistory uint64 MaxPrice *big.Int `toml:",omitempty"` IgnorePrice *big.Int `toml:",omitempty"` + + MinSuggestedPriorityFee *big.Int `toml:",omitempty"` // for Optimism fee suggestion }   // OracleBackend includes all necessary background APIs for oracle. @@ -74,6 +78,8 @@ checkBlocks, percentile int maxHeaderHistory, maxBlockHistory uint64   historyCache *lru.Cache[cacheKey, processedFees] + + minSuggestedPriorityFee *big.Int // for Optimism fee suggestion }   // NewOracle returns a new gasprice oracle which can recommend suitable @@ -131,7 +137,7 @@ lastHead = ev.Block.Hash() } }()   - return &Oracle{ + r := &Oracle{ backend: backend, lastPrice: startPrice, maxPrice: maxPrice, @@ -142,6 +148,17 @@ maxHeaderHistory: maxHeaderHistory, maxBlockHistory: maxBlockHistory, historyCache: cache, } + + if backend.ChainConfig().IsOptimism() { + r.minSuggestedPriorityFee = params.MinSuggestedPriorityFee + if r.minSuggestedPriorityFee == nil || r.minSuggestedPriorityFee.Int64() <= 0 { + r.minSuggestedPriorityFee = DefaultMinSuggestedPriorityFee + log.Warn("Sanitizing invalid optimism gasprice oracle min priority fee suggestion", + "provided", params.MinSuggestedPriorityFee, + "updated", r.minSuggestedPriorityFee) + } + } + return r }   // SuggestTipCap returns a tip cap so that newly created transaction can have a @@ -171,6 +188,11 @@ oracle.cacheLock.RUnlock() if headHash == lastHead { return new(big.Int).Set(lastPrice), nil } + + if oracle.backend.ChainConfig().IsOptimism() { + return oracle.SuggestOptimismPriorityFee(ctx, head, headHash), nil + } + var ( sent, exp int number = head.Number.Uint64() @@ -277,3 +299,9 @@ case result <- results{prices, nil}: case <-quit: } } + +type bigIntArray []*big.Int + +func (s bigIntArray) Len() int { return len(s) } +func (s bigIntArray) Less(i, j int) bool { return s[i].Cmp(s[j]) < 0 } +func (s bigIntArray) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
diff --git go-ethereum/eth/gasprice/optimism-gasprice.go op-geth/eth/gasprice/optimism-gasprice.go new file mode 100644 index 0000000000000000000000000000000000000000..71cd021f637f92e4b6d99ec5fc337d27edf547e5 --- /dev/null +++ op-geth/eth/gasprice/optimism-gasprice.go @@ -0,0 +1,110 @@ +package gasprice + +import ( + "context" + "math/big" + "sort" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/rpc" +) + +// SuggestOptimismPriorityFee returns a max priority fee value that can be used such that newly +// created transactions have a very high chance to be included in the following blocks, using a +// simplified and more predictable algorithm appropriate for chains like Optimism with a single +// known block builder. +// +// In the typical case, which results whenever the last block had room for more transactions, this +// function returns a minimum suggested priority fee value. Otherwise it returns the higher of this +// minimum suggestion or 10% over the median effective priority fee from the last block. +// +// Rationale: For a chain such as Optimism where there is a single block builder whose behavior is +// known, we know priority fee (as long as it is non-zero) has no impact on the probability for tx +// inclusion as long as there is capacity for it in the block. In this case then, there's no reason +// to return any value higher than some fixed minimum. Blocks typically reach capacity only under +// extreme events such as airdrops, meaning predicting whether the next block is going to be at +// capacity is difficult *except* in the case where we're already experiencing the increased demand +// from such an event. We therefore expect whether the last known block is at capacity to be one of +// the best predictors of whether the next block is likely to be at capacity. (An even better +// predictor is to look at the state of the transaction pool, but we want an algorithm that works +// even if the txpool is private or unavailable.) +// +// In the event the next block may be at capacity, the algorithm should allow for average fees to +// rise in order to reach a market price that appropriately reflects demand. We accomplish this by +// returning a suggestion that is a significant amount (10%) higher than the median effective +// priority fee from the previous block. +func (oracle *Oracle) SuggestOptimismPriorityFee(ctx context.Context, h *types.Header, headHash common.Hash) *big.Int { + suggestion := new(big.Int).Set(oracle.minSuggestedPriorityFee) + + // find the maximum gas used by any of the transactions in the block to use as the capacity + // margin + receipts, err := oracle.backend.GetReceipts(ctx, headHash) + if receipts == nil || err != nil { + log.Error("failed to get block receipts", "err", err) + return suggestion + } + var maxTxGasUsed uint64 + for i := range receipts { + gu := receipts[i].GasUsed + if gu > maxTxGasUsed { + maxTxGasUsed = gu + } + } + + // sanity check the max gas used value + if maxTxGasUsed > h.GasLimit { + log.Error("found tx consuming more gas than the block limit", "gas", maxTxGasUsed) + return suggestion + } + + if h.GasUsed+maxTxGasUsed > h.GasLimit { + // A block is "at capacity" if, when it is built, there is a pending tx in the txpool that + // could not be included because the block's gas limit would be exceeded. Since we don't + // have access to the txpool, we instead adopt the following heuristic: consider a block as + // at capacity if the total gas consumed by its transactions is within max-tx-gas-used of + // the block limit, where max-tx-gas-used is the most gas used by any one transaction + // within the block. This heuristic is almost perfectly accurate when transactions always + // consume the same amount of gas, but becomes less accurate as tx gas consumption begins + // to vary. The typical error is we assume a block is at capacity when it was not because + // max-tx-gas-used will in most cases over-estimate the "capacity margin". But it's better + // to err on the side of returning a higher-than-needed suggestion than a lower-than-needed + // one in order to satisfy our desire for high chance of inclusion and rising fees under + // high demand. + block, err := oracle.backend.BlockByNumber(ctx, rpc.BlockNumber(h.Number.Int64())) + if block == nil || err != nil { + log.Error("failed to get last block", "err", err) + return suggestion + } + baseFee := block.BaseFee() + txs := block.Transactions() + if len(txs) == 0 { + log.Error("block was at capacity but doesn't have transactions") + return suggestion + } + tips := bigIntArray(make([]*big.Int, len(txs))) + for i := range txs { + tips[i] = txs[i].EffectiveGasTipValue(baseFee) + } + sort.Sort(tips) + median := tips[len(tips)/2] + newSuggestion := new(big.Int).Add(median, new(big.Int).Div(median, big.NewInt(10))) + // use the new suggestion only if it's bigger than the minimum + if newSuggestion.Cmp(suggestion) > 0 { + suggestion = newSuggestion + } + } + + // the suggestion should be capped by oracle.maxPrice + if suggestion.Cmp(oracle.maxPrice) > 0 { + suggestion.Set(oracle.maxPrice) + } + + oracle.cacheLock.Lock() + oracle.lastHead = headHash + oracle.lastPrice = suggestion + oracle.cacheLock.Unlock() + + return new(big.Int).Set(suggestion) +}

Upstream test of broken behavior; in Optimism, a zero signature is valid (pre-bedrock for deposit-txs), and the chain ID formula on signature data must not be used, or an underflow happens.

diff --git go-ethereum/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json op-geth/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json index 60f18bc1144e31ed88550eab56e9656f04e2c54f..56de19c2971d02547f218bed9b6057422358b790 100644 --- go-ethereum/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json +++ op-geth/internal/ethapi/testdata/eth_getBlockByNumber-tag-pending-fullTx.json @@ -29,7 +29,7 @@ "to": "0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e", "transactionIndex": "0x0", "value": "0x6f", "type": "0x0", - "chainId": "0x7fffffffffffffee", + "chainId": "0x1", "v": "0x0", "r": "0x0", "s": "0x0"

sequencer api for conditional transaction inclusion enforced out of protocol

diff --git go-ethereum/core/state/statedb.go op-geth/core/state/statedb.go index b2b4f8fb97b1fffa978561afb72866af81fc97ad..566344a4c7d11591fbaeb0519cf079fc55f086df 100644 --- go-ethereum/core/state/statedb.go +++ op-geth/core/state/statedb.go @@ -41,7 +41,6 @@ "github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/triestate" "github.com/ethereum/go-ethereum/trie/utils" "github.com/holiman/uint256" - "golang.org/x/sync/errgroup" )   // TriesInMemory represents the number of layers that are kept in RAM. @@ -158,6 +157,9 @@ AccountDeleted int // Number of accounts deleted during the state transition StorageLoaded int // Number of storage slots retrieved from the database during the state transition StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition + + // singlethreaded avoids creation of additional threads when set to true for compatibility with cannon. + singlethreaded bool }   // New creates a new state from a given trie. @@ -188,6 +190,10 @@ if db.TrieDB().IsVerkle() { sdb.accessEvents = NewAccessEvents(db.PointCache()) } return sdb, nil +} + +func (s *StateDB) MakeSinglethreaded() { + s.singlethreaded = true }   // SetLogger sets the logger for account update hooks. @@ -405,6 +411,40 @@ } return false }   +// CheckTransactionConditional validates the account preconditions against the statedb. +// +// NOTE: A lock is not held on the db while the conditional is checked. The caller must +// ensure no state changes occur while this check is executed. +func (s *StateDB) CheckTransactionConditional(cond *types.TransactionConditional) error { + cost := cond.Cost() + + // The max cost is an inclusive limit. + if cost > params.TransactionConditionalMaxCost { + return fmt.Errorf("conditional cost, %d, exceeded max: %d", cost, params.TransactionConditionalMaxCost) + } + + for addr, acct := range cond.KnownAccounts { + if root, isRoot := acct.Root(); isRoot { + storageRoot := s.GetStorageRoot(addr) + if storageRoot == (common.Hash{}) { // if the root is not found, replace with the empty root hash + storageRoot = types.EmptyRootHash + } + if root != storageRoot { + return fmt.Errorf("failed account storage root constraint. Got %s, Expected %s", storageRoot, root) + } + } + if slots, isSlots := acct.Slots(); isSlots { + for key, state := range slots { + accState := s.GetState(addr, key) + if state != accState { + return fmt.Errorf("failed account storage slot key %s constraint. Got %s, Expected %s", key, accState, state) + } + } + } + } + return nil +} + /* * SETTERS */ @@ -785,9 +825,9 @@ // Process all storage updates concurrently. The state object update root // method will internally call a blocking trie fetch from the prefetcher, // so there's no need to explicitly wait for the prefetchers to finish. var ( - start = time.Now() - workers errgroup.Group + start = time.Now() ) + workers := newWorkerGroup(s.singlethreaded) if s.db.TrieDB().IsVerkle() { // Whilst MPT storage tries are independent, Verkle has one single trie // for all the accounts and all the storage slots merged together. The @@ -1159,10 +1199,10 @@ // Handle all state updates afterwards, concurrently to one another to shave // off some milliseconds from the commit operation. Also accumulate the code // writes to run in parallel with the computations. var ( - start = time.Now() - root common.Hash - workers errgroup.Group + start = time.Now() + root common.Hash ) + workers := newWorkerGroup(s.singlethreaded) // Schedule the account trie first since that will be the biggest, so give // it the most time to crunch. // @@ -1384,6 +1424,12 @@ // SlotInAccessList returns true if the given (address, slot)-tuple is in the access list. func (s *StateDB) SlotInAccessList(addr common.Address, slot common.Hash) (addressPresent bool, slotPresent bool) { return s.accessList.Contains(addr, slot) +} + +// OpenStorageTrie opens the storage trie for the storage root of the provided address. +func (s *StateDB) OpenStorageTrie(addr common.Address) (Trie, error) { + storageRoot := s.GetStorageRoot(addr) + return s.db.OpenStorageTrie(s.originalRoot, addr, storageRoot, s.trie) }   // markDelete is invoked when an account is deleted but the deletion is
diff --git go-ethereum/core/state/statedb_test.go op-geth/core/state/statedb_test.go index 9441834c6a24e48092f5faa6fa7c83fd610b76ad..bb06e226583bc208038f189837f2ed4193da18eb 100644 --- go-ethereum/core/state/statedb_test.go +++ op-geth/core/state/statedb_test.go @@ -1375,3 +1375,221 @@ // the storage change is reverted, dirty value should be set back state.RevertToSnapshot(snap) checkDirty(common.Hash{0x1}, common.Hash{0x1}, true) } + +func TestCheckTransactionConditional(t *testing.T) { + type preAction struct { + Account common.Address + Slots map[common.Hash]common.Hash + } + + tests := []struct { + name string + preActions []preAction + cond types.TransactionConditional + valid bool + }{ + { + // Clean Prestate, no defined cond + name: "clean prestate", + preActions: []preAction{}, + cond: types.TransactionConditional{}, + valid: true, + }, + { + // Prestate: + // - address(1) + // - bytes32(0): bytes32(1) + // cond: + // - address(1) + // - bytes32(0): bytes32(1) + name: "matching storage slots", + preActions: []preAction{ + { + Account: common.Address{19: 1}, + Slots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + }, + cond: types.TransactionConditional{ + KnownAccounts: map[common.Address]types.KnownAccount{ + common.Address{19: 1}: types.KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + }, + }, + valid: true, + }, + { + // Prestate: + // - address(1) + // - bytes32(0): bytes32(1) + // cond: + // - address(1) + // - bytes32(0): bytes32(2) + name: "mismatched storage slots", + preActions: []preAction{ + { + Account: common.Address{19: 1}, + Slots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + }, + cond: types.TransactionConditional{ + KnownAccounts: map[common.Address]types.KnownAccount{ + common.Address{19: 1}: types.KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 2}, + }, + }, + }, + }, + valid: false, + }, + { + // Clean Prestate + // cond: + // - address(1) + // - emptyRoot + name: "matching storage root", + preActions: []preAction{}, + cond: types.TransactionConditional{ + KnownAccounts: map[common.Address]types.KnownAccount{ + common.Address{19: 1}: types.KnownAccount{ + StorageRoot: &types.EmptyRootHash, + }, + }, + }, + valid: true, + }, + { + // Prestate: + // - address(1) + // - bytes32(0): bytes32(1) + // cond: + // - address(1) + // - emptyRoot + name: "mismatched storage root", + preActions: []preAction{ + { + Account: common.Address{19: 1}, + Slots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + }, + cond: types.TransactionConditional{ + KnownAccounts: map[common.Address]types.KnownAccount{ + common.Address{19: 1}: types.KnownAccount{ + StorageRoot: &types.EmptyRootHash, + }, + }, + }, + valid: false, + }, + { + // Prestate: + // - address(1) + // - bytes32(0): bytes32(1) + // - address(2) + // - bytes32(0): bytes32(2) + // cond: + // - address(1) + // - bytes32(0): bytes32(1) + // - address(2) + // - bytes32(0): bytes32(2) + name: "multiple matching", + preActions: []preAction{ + { + Account: common.Address{19: 1}, + Slots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + { + Account: common.Address{19: 2}, + Slots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 2}, + }, + }, + }, + cond: types.TransactionConditional{ + KnownAccounts: map[common.Address]types.KnownAccount{ + common.Address{19: 1}: types.KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + common.Address{19: 2}: types.KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 2}, + }, + }, + }, + }, + valid: true, + }, + { + // Prestate: + // - address(1) + // - bytes32(0): bytes32(1) + // - address(2) + // - bytes32(0): bytes32(3) + // cond: + // - address(1) + // - bytes32(0): bytes32(1) + // - address(2) + // - bytes32(0): bytes32(2) + name: "multiple mismatch single", + preActions: []preAction{ + { + Account: common.Address{19: 1}, + Slots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + { + Account: common.Address{19: 2}, + Slots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 3}, + }, + }, + }, + cond: types.TransactionConditional{ + KnownAccounts: map[common.Address]types.KnownAccount{ + common.Address{19: 1}: types.KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + }, + }, + common.Address{19: 2}: types.KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 2}, + }, + }, + }, + }, + valid: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + state, _ := New(types.EmptyRootHash, NewDatabaseForTesting()) + for _, action := range test.preActions { + for key, value := range action.Slots { + state.SetState(action.Account, key, value) + } + } + + // write modifications to the trie + state.IntermediateRoot(false) + if err := state.CheckTransactionConditional(&test.cond); err == nil && !test.valid { + t.Errorf("Test %s got unvalid value: want %v, got err %v", test.name, test.valid, err) + } + }) + } +}
diff --git go-ethereum/core/types/block.go op-geth/core/types/block.go index 1c00658d5b38f489efa66f07eead6d3c5f5b9489..54197d063ef901158d7762a66d9f04daf735a427 100644 --- go-ethereum/core/types/block.go +++ op-geth/core/types/block.go @@ -178,6 +178,23 @@ func (h *Header) EmptyReceipts() bool { return h.ReceiptHash == EmptyReceiptsHash }   +// CheckTransactionConditional validates the block preconditions against the header +func (h *Header) CheckTransactionConditional(cond *TransactionConditional) error { + if cond.BlockNumberMin != nil && cond.BlockNumberMin.Cmp(h.Number) > 0 { + return fmt.Errorf("failed block number minimum constraint") + } + if cond.BlockNumberMax != nil && cond.BlockNumberMax.Cmp(h.Number) < 0 { + return fmt.Errorf("failed block number maximmum constraint") + } + if cond.TimestampMin != nil && *cond.TimestampMin > h.Time { + return fmt.Errorf("failed timestamp minimum constraint") + } + if cond.TimestampMax != nil && *cond.TimestampMax < h.Time { + return fmt.Errorf("failed timestamp maximum constraint") + } + return nil +} + // Body is a simple (mutable, non-safe) data container for storing and moving // a block's data contents (transactions and uncles) together. type Body struct { @@ -235,12 +252,16 @@ Withdrawals []*Withdrawal `rlp:"optional"` Requests []*Request `rlp:"optional"` }   +type BlockType interface { + HasOptimismWithdrawalsRoot(blkTime uint64) bool +} + // NewBlock creates a new block. The input data is copied, changes to header and to the // field values will not affect the block. // // The body elements and the receipts are used to recompute and overwrite the // relevant portions of the header. -func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher) *Block { +func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher, bType BlockType) *Block { if body == nil { body = &Body{} } @@ -277,7 +298,14 @@ b.uncles[i] = CopyHeader(uncles[i]) } }   - if withdrawals == nil { + if bType.HasOptimismWithdrawalsRoot(b.header.Time) { + if withdrawals == nil || len(withdrawals) > 0 { + panic(fmt.Sprintf("expected non-nil empty withdrawals operation list in Isthmus, but got: %v", body.Withdrawals)) + } + b.header.WithdrawalsHash = header.WithdrawalsHash + b.withdrawals = make(Withdrawals, 0) + } else if withdrawals == nil { + // pre-Canyon b.header.WithdrawalsHash = nil } else if len(withdrawals) == 0 { b.header.WithdrawalsHash = &EmptyWithdrawalsHash @@ -411,6 +439,14 @@ func (b *Block) TxHash() common.Hash { return b.header.TxHash } func (b *Block) ReceiptHash() common.Hash { return b.header.ReceiptHash } func (b *Block) UncleHash() common.Hash { return b.header.UncleHash } func (b *Block) Extra() []byte { return common.CopyBytes(b.header.Extra) } + +func (b *Block) WithdrawalsRoot() *common.Hash { + if b.header.WithdrawalsHash == nil { + return nil + } + h := *b.header.WithdrawalsHash + return &h +}   func (b *Block) BaseFee() *big.Int { if b.header.BaseFee == nil {
diff --git go-ethereum/core/types/block_test.go op-geth/core/types/block_test.go index 1af5b9d7bf2c99e06d96c5fe226e3ebdbe200b5d..40e8af81b7d13f5b2c6c5e447be61dd5435f135d 100644 --- go-ethereum/core/types/block_test.go +++ op-geth/core/types/block_test.go @@ -254,7 +254,8 @@ Time: 9876543, Extra: []byte("benchmark uncle"), } } - return NewBlock(header, &Body{Transactions: txs, Uncles: uncles}, receipts, blocktest.NewHasher()) + withdrawals := make([]*Withdrawal, 0) + return NewBlock(header, &Body{Transactions: txs, Uncles: uncles, Withdrawals: withdrawals}, receipts, blocktest.NewHasher(), IsthmusBlockConfig) }   func TestRlpDecodeParentHash(t *testing.T) { @@ -317,3 +318,109 @@ } } } } + +func TestCheckTransactionConditional(t *testing.T) { + u64Ptr := func(n uint64) *uint64 { + return &n + } + + tests := []struct { + name string + header Header + cond TransactionConditional + valid bool + }{ + { + "BlockNumberMaxFails", + Header{Number: big.NewInt(2)}, + TransactionConditional{BlockNumberMax: big.NewInt(1)}, + false, + }, + { + "BlockNumberMaxEqualSucceeds", + Header{Number: big.NewInt(2)}, + TransactionConditional{BlockNumberMax: big.NewInt(2)}, + true, + }, + { + "BlockNumberMaxSucceeds", + Header{Number: big.NewInt(1)}, + TransactionConditional{BlockNumberMax: big.NewInt(2)}, + true, + }, + { + "BlockNumberMinFails", + Header{Number: big.NewInt(1)}, + TransactionConditional{BlockNumberMin: big.NewInt(2)}, + false, + }, + { + "BlockNumberMinEqualSuccess", + Header{Number: big.NewInt(2)}, + TransactionConditional{BlockNumberMin: big.NewInt(2)}, + true, + }, + { + "BlockNumberMinSuccess", + Header{Number: big.NewInt(4)}, + TransactionConditional{BlockNumberMin: big.NewInt(3)}, + true, + }, + { + "BlockNumberRangeSucceeds", + Header{Number: big.NewInt(5)}, + TransactionConditional{BlockNumberMin: big.NewInt(1), BlockNumberMax: big.NewInt(10)}, + true, + }, + { + "BlockNumberRangeFails", + Header{Number: big.NewInt(15)}, + TransactionConditional{BlockNumberMin: big.NewInt(1), BlockNumberMax: big.NewInt(10)}, + false, + }, + { + "BlockTimestampMinFails", + Header{Time: 1}, + TransactionConditional{TimestampMin: u64Ptr(2)}, + false, + }, + { + "BlockTimestampMinSucceeds", + Header{Time: 2}, + TransactionConditional{TimestampMin: u64Ptr(1)}, + true, + }, + { + "BlockTimestampMinEqualSucceeds", + Header{Time: 1}, + TransactionConditional{TimestampMin: u64Ptr(1)}, + true, + }, + { + "BlockTimestampMaxFails", + Header{Time: 2}, + TransactionConditional{TimestampMax: u64Ptr(1)}, + false, + }, + { + "BlockTimestampMaxSucceeds", + Header{Time: 1}, + TransactionConditional{TimestampMax: u64Ptr(2)}, + true, + }, + { + "BlockTimestampMaxEqualSucceeds", + Header{Time: 2}, + TransactionConditional{TimestampMax: u64Ptr(2)}, + true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.header.CheckTransactionConditional(&test.cond); err == nil && !test.valid { + t.Errorf("Test %s got unvalid value, want %v, got err %v", test.name, test.valid, err) + } + }) + } +}
diff --git go-ethereum/core/types/gen_transaction_conditional_json.go op-geth/core/types/gen_transaction_conditional_json.go new file mode 100644 index 0000000000000000000000000000000000000000..b2027353166e04c8cd676ca0bdf3d23f5d29c793 --- /dev/null +++ op-geth/core/types/gen_transaction_conditional_json.go @@ -0,0 +1,61 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package types + +import ( + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common/math" +) + +var _ = (*transactionConditionalMarshalling)(nil) + +// MarshalJSON marshals as JSON. +func (t TransactionConditional) MarshalJSON() ([]byte, error) { + type TransactionConditional struct { + 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 = (*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 *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 + if err := json.Unmarshal(input, &dec); err != nil { + return err + } + if dec.KnownAccounts != nil { + t.KnownAccounts = *dec.KnownAccounts + } + if dec.BlockNumberMin != nil { + t.BlockNumberMin = (*big.Int)(dec.BlockNumberMin) + } + if dec.BlockNumberMax != nil { + t.BlockNumberMax = (*big.Int)(dec.BlockNumberMax) + } + if dec.TimestampMin != nil { + t.TimestampMin = (*uint64)(dec.TimestampMin) + } + if dec.TimestampMax != nil { + t.TimestampMax = (*uint64)(dec.TimestampMax) + } + return nil +}
diff --git go-ethereum/core/types/transaction_conditional.go op-geth/core/types/transaction_conditional.go new file mode 100644 index 0000000000000000000000000000000000000000..479b6f9fc5b284552e22129f5f306ddd2954dc57 --- /dev/null +++ op-geth/core/types/transaction_conditional.go @@ -0,0 +1,126 @@ +package types + +import ( + "encoding/json" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" +) + +// KnownAccounts represents a set of KnownAccounts +type KnownAccounts map[common.Address]KnownAccount + +// KnownAccount allows for a user to express their preference of a known +// prestate at a particular account. Only one of the storage root or +// storage slots is allowed to be set. If the storage root is set, then +// the user prefers their transaction to only be included in a block if +// the account's storage root matches. If the storage slots are set, +// then the user prefers their transaction to only be included if the +// particular storage slot values from state match. +type KnownAccount struct { + StorageRoot *common.Hash + StorageSlots map[common.Hash]common.Hash +} + +// UnmarshalJSON will parse the JSON bytes into a KnownAccount struct. +func (ka *KnownAccount) UnmarshalJSON(data []byte) error { + var hash common.Hash + if err := json.Unmarshal(data, &hash); err == nil { + ka.StorageRoot = &hash + ka.StorageSlots = make(map[common.Hash]common.Hash) + return nil + } + + var mapping map[common.Hash]common.Hash + if err := json.Unmarshal(data, &mapping); err != nil { + return err + } + ka.StorageSlots = mapping + return nil +} + +// MarshalJSON will serialize the KnownAccount into JSON bytes. +func (ka KnownAccount) MarshalJSON() ([]byte, error) { + if ka.StorageRoot != nil { + return json.Marshal(ka.StorageRoot) + } + return json.Marshal(ka.StorageSlots) +} + +// Root will return the storage root and true when the user prefers +// execution against an account's storage root, otherwise it will +// return false. +func (ka *KnownAccount) Root() (common.Hash, bool) { + if ka.StorageRoot == nil { + return common.Hash{}, false + } + return *ka.StorageRoot, true +} + +// Slots will return the storage slots and true when the user prefers +// execution against an account's particular storage slots, StorageRoot == nil, +// otherwise it will return false. +func (ka *KnownAccount) Slots() (map[common.Hash]common.Hash, bool) { + if ka.StorageRoot != nil { + return ka.StorageSlots, false + } + return ka.StorageSlots, true +} + +//go:generate go run github.com/fjl/gencodec -type TransactionConditional -field-override transactionConditionalMarshalling -out gen_transaction_conditional_json.go + +// TransactionConditional represents the preconditions that determine the +// inclusion of the transaction, enforced out-of-protocol by the sequencer. +type TransactionConditional struct { + // KnownAccounts represents account prestate conditions + KnownAccounts KnownAccounts `json:"knownAccounts"` + + // Header state conditionals (inclusive ranges) + BlockNumberMin *big.Int `json:"blockNumberMin,omitempty"` + BlockNumberMax *big.Int `json:"blockNumberMax,omitempty"` + TimestampMin *uint64 `json:"timestampMin,omitempty"` + TimestampMax *uint64 `json:"timestampMax,omitempty"` +} + +// field type overrides for gencodec +type transactionConditionalMarshalling struct { + 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. +func (cond *TransactionConditional) Validate() error { + if cond.BlockNumberMin != nil && cond.BlockNumberMax != nil && cond.BlockNumberMin.Cmp(cond.BlockNumberMax) > 0 { + return fmt.Errorf("block number minimum constraint must be less than the maximum") + } + if cond.TimestampMin != nil && cond.TimestampMax != nil && *cond.TimestampMin > *cond.TimestampMax { + return fmt.Errorf("timestamp minimum constraint must be less than the maximum") + } + return nil +} + +// Cost computes the aggregate cost of the preconditions; total number of storage lookups required +func (cond *TransactionConditional) Cost() int { + cost := 0 + for _, account := range cond.KnownAccounts { + // default cost to handle empty accounts + cost += 1 + if _, isRoot := account.Root(); isRoot { + cost += 1 + } + if slots, isSlots := account.Slots(); isSlots { + cost += len(slots) + } + } + if cond.BlockNumberMin != nil || cond.BlockNumberMax != nil { + cost += 1 + } + if cond.TimestampMin != nil || cond.TimestampMax != nil { + cost += 1 + } + return cost +}
diff --git go-ethereum/core/types/transaction_conditional_test.go op-geth/core/types/transaction_conditional_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6abfe946d0ca6a905aa0bf836f4c14f0cff6076f --- /dev/null +++ op-geth/core/types/transaction_conditional_test.go @@ -0,0 +1,264 @@ +package types + +import ( + "encoding/json" + "math/big" + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +func TestTransactionConditionalCost(t *testing.T) { + uint64Ptr := func(num uint64) *uint64 { + return &num + } + + tests := []struct { + name string + cond TransactionConditional + cost int + }{ + { + name: "empty conditional", + cond: TransactionConditional{}, + cost: 0, + }, + { + name: "block number lookup counts once", + cond: TransactionConditional{BlockNumberMin: big.NewInt(1), BlockNumberMax: big.NewInt(2)}, + cost: 1, + }, + { + name: "timestamp lookup counts once", + cond: TransactionConditional{TimestampMin: uint64Ptr(0), TimestampMax: uint64Ptr(5)}, + cost: 1, + }, + { + name: "default cost per account", + cond: TransactionConditional{KnownAccounts: map[common.Address]KnownAccount{ + common.Address{19: 1}: KnownAccount{}, + common.Address{19: 2}: KnownAccount{}, + }}, + cost: 2, + }, + { + name: "storage root lookup", + cond: TransactionConditional{KnownAccounts: map[common.Address]KnownAccount{ + common.Address{19: 1}: KnownAccount{ + StorageRoot: &EmptyRootHash, + }}}, + cost: 2, + }, + { + name: "cost per storage slot lookup", + cond: TransactionConditional{KnownAccounts: map[common.Address]KnownAccount{ + common.Address{19: 1}: KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + common.Hash{31: 1}: common.Hash{31: 1}, + }, + }}}, + cost: 3, + }, + { + name: "cost summed together", + cond: TransactionConditional{ + BlockNumberMin: big.NewInt(1), + TimestampMin: uint64Ptr(1), + KnownAccounts: map[common.Address]KnownAccount{ + common.Address{19: 1}: KnownAccount{StorageRoot: &EmptyRootHash}, + common.Address{19: 2}: KnownAccount{ + StorageSlots: map[common.Hash]common.Hash{ + common.Hash{}: common.Hash{31: 1}, + common.Hash{31: 1}: common.Hash{31: 1}, + }, + }}}, + cost: 7, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cost := test.cond.Cost() + if cost != test.cost { + t.Errorf("Test %s mismatch in TransactionConditional cost. Got %d, Expected: %d", test.name, cost, test.cost) + } + }) + } +} + +func TestTransactionConditionalValidation(t *testing.T) { + uint64Ptr := func(num uint64) *uint64 { + return &num + } + + tests := []struct { + name string + cond TransactionConditional + mustFail bool + }{ + { + name: "empty conditional", + cond: TransactionConditional{}, + mustFail: false, + }, + { + name: "equal block constraint", + cond: TransactionConditional{BlockNumberMin: big.NewInt(1), BlockNumberMax: big.NewInt(1)}, + mustFail: false, + }, + { + name: "block min greater than max", + cond: TransactionConditional{BlockNumberMin: big.NewInt(2), BlockNumberMax: big.NewInt(1)}, + mustFail: true, + }, + { + name: "equal timestamp constraint", + cond: TransactionConditional{TimestampMin: uint64Ptr(1), TimestampMax: uint64Ptr(1)}, + mustFail: false, + }, + { + name: "timestamp min greater than max", + cond: TransactionConditional{TimestampMin: uint64Ptr(2), TimestampMax: uint64Ptr(1)}, + mustFail: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + err := test.cond.Validate() + if test.mustFail && err == nil { + t.Errorf("Test %s should fail", test.name) + } + if !test.mustFail && err != nil { + t.Errorf("Test %s should pass but got err: %v", test.name, err) + } + }) + } +} + +func TestTransactionConditionalSerDeser(t *testing.T) { + uint64Ptr := func(num uint64) *uint64 { + return &num + } + hashPtr := func(hash common.Hash) *common.Hash { + return &hash + } + + tests := []struct { + name string + input string + mustFail bool + expected TransactionConditional + }{ + { + name: "StateRoot", + input: `{"knownAccounts":{"0x6b3A8798E5Fb9fC5603F3aB5eA2e8136694e55d0":"0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563"}}`, + mustFail: false, + expected: TransactionConditional{ + KnownAccounts: map[common.Address]KnownAccount{ + common.HexToAddress("0x6b3A8798E5Fb9fC5603F3aB5eA2e8136694e55d0"): KnownAccount{ + StorageRoot: hashPtr(common.HexToHash("0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563")), + StorageSlots: make(map[common.Hash]common.Hash), + }, + }, + }, + }, + { + name: "StorageSlots", + input: `{"knownAccounts":{"0x6b3A8798E5Fb9fC5603F3aB5eA2e8136694e55d0":{"0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2a8":"0x0000000000000000000000000000000000000000000000000000000000000000"}}}`, + mustFail: false, + expected: TransactionConditional{ + KnownAccounts: map[common.Address]KnownAccount{ + common.HexToAddress("0x6b3A8798E5Fb9fC5603F3aB5eA2e8136694e55d0"): KnownAccount{ + StorageRoot: nil, + StorageSlots: map[common.Hash]common.Hash{ + common.HexToHash("0xc65a7bb8d6351c1cf70c95a316cc6a92839c986682d98bc35f958f4883f9d2a8"): common.HexToHash("0x"), + }, + }, + }, + }, + }, + { + name: "EmptyObject", + input: `{"knownAccounts":{}}`, + mustFail: false, + expected: TransactionConditional{ + KnownAccounts: make(map[common.Address]KnownAccount), + }, + }, + { + name: "EmptyStrings", + input: `{"knownAccounts":{"":""}}`, + mustFail: true, + expected: TransactionConditional{KnownAccounts: nil}, + }, + { + name: "BlockNumberMin", + input: `{"blockNumberMin":"0x1"}`, + mustFail: false, + expected: TransactionConditional{ + BlockNumberMin: big.NewInt(1), + }, + }, + { + name: "BlockNumberMax", + input: `{"blockNumberMax":"0x2"}`, + mustFail: false, + expected: TransactionConditional{ + 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"}`, + mustFail: false, + expected: TransactionConditional{ + TimestampMin: uint64Ptr(uint64(0xffff)), + }, + }, + { + name: "TimestampMax", + input: `{"timestampMax":"0xffffff"}`, + mustFail: false, + expected: TransactionConditional{ + TimestampMax: uint64Ptr(uint64(0xffffff)), + }, + }, + { + name: "Timestamp (decimal)", + input: `{"timestampMin": 0, "timestampMax": 1}`, + mustFail: false, + expected: TransactionConditional{ + TimestampMin: uint64Ptr(0), + TimestampMax: uint64Ptr(1), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var cond TransactionConditional + err := json.Unmarshal([]byte(test.input), &cond) + if test.mustFail && err == nil { + t.Errorf("Test %s should fail", test.name) + } + if !test.mustFail && err != nil { + t.Errorf("Test %s should pass but got err: %v", test.name, err) + } + if !reflect.DeepEqual(cond, test.expected) { + t.Errorf("Test %s got unexpected value, want %#v, got %#v", test.name, test.expected, cond) + } + }) + } +}
diff --git go-ethereum/eth/protocols/eth/broadcast.go op-geth/eth/protocols/eth/broadcast.go index f0ed1d6bc9a0ee013488d9e2775deee6d7f173ea..e41e0aaf74fda9af0a6de5dcd4a396aa603c4dd0 100644 --- go-ethereum/eth/protocols/eth/broadcast.go +++ op-geth/eth/protocols/eth/broadcast.go @@ -47,7 +47,10 @@ txs []*types.Transaction size common.StorageSize ) for i := 0; i < len(queue) && size < maxTxPacketSize; i++ { - if tx := p.txpool.Get(queue[i]); tx != nil { + // Transaction conditionals are tied to the block builder it was + // submitted to. Thus we do not broadcast transactions to peers that + // have a conditional attached to them. + if tx := p.txpool.Get(queue[i]); tx != nil && tx.Conditional() == nil { txs = append(txs, tx) size += common.StorageSize(tx.Size()) }
diff --git go-ethereum/internal/sequencerapi/api.go op-geth/internal/sequencerapi/api.go new file mode 100644 index 0000000000000000000000000000000000000000..9a64258948ebe1e9620ad794fad58618f08cbaf5 --- /dev/null +++ op-geth/internal/sequencerapi/api.go @@ -0,0 +1,129 @@ +package sequencerapi + +import ( + "context" + "errors" + "fmt" + "time" + + "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/internal/ethapi" + "github.com/ethereum/go-ethereum/metrics" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" + "golang.org/x/time/rate" +) + +var ( + sendRawTxConditionalCostMeter = metrics.NewRegisteredMeter("sequencer/sendRawTransactionConditional/cost", nil) + sendRawTxConditionalRequestsCounter = metrics.NewRegisteredCounter("sequencer/sendRawTransactionConditional/requests", nil) + sendRawTxConditionalAcceptedCounter = metrics.NewRegisteredCounter("sequencer/sendRawTransactionConditional/accepted", nil) +) + +type sendRawTxCond struct { + b ethapi.Backend + seqRPC *rpc.Client + costLimiter *rate.Limiter +} + +func GetSendRawTxConditionalAPI(b ethapi.Backend, seqRPC *rpc.Client, costRateLimit rate.Limit) rpc.API { + // Applying a manual bump to the burst to allow conditional txs to queue. Metrics will + // will inform of adjustments that may need to be made here. + costLimiter := rate.NewLimiter(costRateLimit, 3*params.TransactionConditionalMaxCost) + return rpc.API{ + Namespace: "eth", + Service: &sendRawTxCond{b, seqRPC, costLimiter}, + } +} + +func (s *sendRawTxCond) SendRawTransactionConditional(ctx context.Context, txBytes hexutil.Bytes, cond types.TransactionConditional) (common.Hash, error) { + sendRawTxConditionalRequestsCounter.Inc(1) + + cost := cond.Cost() + sendRawTxConditionalCostMeter.Mark(int64(cost)) + if cost > params.TransactionConditionalMaxCost { + return common.Hash{}, &rpc.JsonError{ + Message: fmt.Sprintf("conditional cost, %d, exceeded max: %d", cost, params.TransactionConditionalMaxCost), + Code: params.TransactionConditionalCostExceededMaxErrCode, + } + } + + // Perform sanity validation prior to state lookups + if err := cond.Validate(); err != nil { + return common.Hash{}, &rpc.JsonError{ + Message: fmt.Sprintf("failed conditional validation: %s", err), + Code: params.TransactionConditionalRejectedErrCode, + } + } + + state, header, err := s.b.StateAndHeaderByNumber(ctx, rpc.LatestBlockNumber) + if err != nil { + return common.Hash{}, err + } + if err := header.CheckTransactionConditional(&cond); err != nil { + return common.Hash{}, &rpc.JsonError{ + Message: fmt.Sprintf("failed header check: %s", err), + Code: params.TransactionConditionalRejectedErrCode, + } + } + if err := state.CheckTransactionConditional(&cond); err != nil { + return common.Hash{}, &rpc.JsonError{ + Message: fmt.Sprintf("failed state check: %s", err), + Code: params.TransactionConditionalRejectedErrCode, + } + } + + // State is checked against an older block to remove the MEV incentive for this endpoint compared with sendRawTransaction + parentBlock := rpc.BlockNumberOrHash{BlockHash: &header.ParentHash} + parentState, _, err := s.b.StateAndHeaderByNumberOrHash(ctx, parentBlock) + if err != nil { + return common.Hash{}, err + } + if err := parentState.CheckTransactionConditional(&cond); err != nil { + return common.Hash{}, &rpc.JsonError{ + Message: fmt.Sprintf("failed parent block %s state check: %s", header.ParentHash, err), + Code: params.TransactionConditionalRejectedErrCode, + } + } + + // enforce rate limit on the cost to be observed + if err := s.costLimiter.WaitN(ctx, cost); err != nil { + return common.Hash{}, &rpc.JsonError{ + Message: fmt.Sprintf("cost %d rate limited", cost), + Code: params.TransactionConditionalCostExceededMaxErrCode, + } + } + + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(txBytes); err != nil { + return common.Hash{}, err + } + + // forward if seqRPC is set, otherwise submit the tx + if s.seqRPC != nil { + // Some precondition checks done by `ethapi.SubmitTransaction` that are good to also check here + if err := ethapi.CheckTxFee(tx.GasPrice(), tx.Gas(), s.b.RPCTxFeeCap()); err != nil { + return common.Hash{}, err + } + if !s.b.UnprotectedAllowed() && !tx.Protected() { + // Ensure only eip155 signed transactions are submitted if EIP155Required is set. + return common.Hash{}, errors.New("only replay-protected (EIP-155) transactions allowed over RPC") + } + + var hash common.Hash + err := s.seqRPC.CallContext(ctx, &hash, "eth_sendRawTransactionConditional", txBytes, cond) + return hash, err + } else { + // Set out-of-consensus internal tx fields + tx.SetTime(time.Now()) + tx.SetConditional(&cond) + + // `SubmitTransaction` which forwards to `b.SendTx` also checks if its internal `seqRPC` client is + // set. Since both of these client are constructed when `RollupSequencerHTTP` is supplied, the above + // block ensures that we're only adding to the txpool for this node. + sendRawTxConditionalAcceptedCounter.Inc(1) + return ethapi.SubmitTransaction(ctx, s.b, tx) + } +}
diff --git go-ethereum/params/conditional_tx_params.go op-geth/params/conditional_tx_params.go new file mode 100644 index 0000000000000000000000000000000000000000..a3d27a98379e715e42036752611f0df96b7b3df9 --- /dev/null +++ op-geth/params/conditional_tx_params.go @@ -0,0 +1,9 @@ +package params + +const ( + // An inclusive limit on the max cost for the conditional attached to a tx + TransactionConditionalMaxCost = 1000 + + TransactionConditionalRejectedErrCode = -32003 + TransactionConditionalCostExceededMaxErrCode = -32005 +)
diff --git go-ethereum/rpc/json.go op-geth/rpc/json.go index e932389d17c7bc63aecbe2deebdcb40073a797ec..bc8d69f143bcd25285cbe5f02679528397aa6266 100644 --- go-ethereum/rpc/json.go +++ op-geth/rpc/json.go @@ -41,6 +41,8 @@ )   var null = json.RawMessage("null")   +type JsonError = jsonError + type subscriptionResult struct { ID string `json:"subscription"` Result json.RawMessage `json:"result,omitempty"`

Extend the tools available in geth to improve external testing and tooling.

diff --git go-ethereum/accounts/abi/bind/backends/simulated.go op-geth/accounts/abi/bind/backends/simulated.go index dfd929695286568a72fcb10a8c671e7b00b8cf22..38ac9d60d091204326f9fd8ec932fff4f4721bc9 100644 --- go-ethereum/accounts/abi/bind/backends/simulated.go +++ op-geth/accounts/abi/bind/backends/simulated.go @@ -21,6 +21,7 @@ "context"   "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/ethclient/simulated" )   @@ -50,3 +51,11 @@ Backend: b, Client: b.Client(), } } + +func NewSimulatedBackendFromConfig(cfg ethconfig.Config) *SimulatedBackend { + b := simulated.NewBackendFromConfig(cfg) + return &SimulatedBackend{ + Backend: b, + Client: b.Client(), + } +}
diff --git go-ethereum/ethclient/simulated/backend.go op-geth/ethclient/simulated/backend.go index 6e07aa68d071d88a5bf393193a11ae51120e8826..03bb56da66d546219801343973d43ca94d55bd34 100644 --- go-ethereum/ethclient/simulated/backend.go +++ op-geth/ethclient/simulated/backend.go @@ -103,6 +103,27 @@ } return sim }   +func NewBackendFromConfig(conf ethconfig.Config) *Backend { + // Setup the node object + nodeConf := node.DefaultConfig + nodeConf.DataDir = "" + nodeConf.P2P = p2p.Config{NoDiscovery: true} + stack, err := node.New(&nodeConf) + if err != nil { + // This should never happen, if it does, please open an issue + panic(err) + } + + conf.SyncMode = downloader.FullSync + conf.TxPool.NoLocals = true + sim, err := newWithNode(stack, &conf, 0) + if err != nil { + // This should never happen, if it does, please open an issue + panic(err) + } + return sim +} + // newWithNode sets up a simulated backend on an existing node. The provided node // must not be started and will be started by this method. func newWithNode(stack *node.Node, conf *eth.Config, blockPeriod uint64) (*Backend, error) {

Track L1-deposited native currency that is coming into the L2 supply. The balance delta is considered to be a “withdrawal” from L1, similar to a withdrawal of the Beacon-chain into the Ethereum L1 execution chain.

diff --git go-ethereum/eth/tracers/live/supply.go op-geth/eth/tracers/live/supply.go index 96f70594548cb4936ea58fa4813f11c7f39b5edb..cfa4843bfe1faada945a8d4416642e6b3a02588d 100644 --- go-ethereum/eth/tracers/live/supply.go +++ op-geth/eth/tracers/live/supply.go @@ -188,6 +188,9 @@ case tracing.BalanceDecreaseSelfdestructBurn: // BalanceDecreaseSelfdestructBurn is non-reversible as it happens // at the end of the transaction. s.delta.Burn.Misc.Sub(s.delta.Burn.Misc, diff) + // Technically an OP-Stack deposit mint is a "withdrawal" from L1, taking funds into L2. + case tracing.BalanceMint: + s.delta.Issuance.Withdrawals.Add(s.delta.Issuance.Withdrawals, diff) default: return }

The EVM t8ntool has not been updated with most op-stack features and does not use the same sealer logic as used in Geth consensus. Isthumus hard fork adds a withdrawalsRoot field in the block header. We note that the t8ntool is not updated to handle the newly added withdrawalsRoot field in the block header.

Extend Ledger wallet support for newer devices on Macos

diff --git go-ethereum/accounts/usbwallet/hub.go op-geth/accounts/usbwallet/hub.go index e22dffe9718e6aba0ad50357f22a146babcf18e4..4603b392358288cd10eae62e6c9bb68693b4fe40 100644 --- go-ethereum/accounts/usbwallet/hub.go +++ op-geth/accounts/usbwallet/hub.go @@ -19,6 +19,7 @@ import ( "errors" "runtime" + "slices" "sync" "sync/atomic" "time" @@ -48,7 +49,7 @@ type Hub struct { scheme string // Protocol scheme prefixing account and wallet URLs. vendorID uint16 // USB vendor identifier used for device discovery product