op-geth

diff: ignored:
+6999
-451
+9227
-175

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..a970bdcd31b3bed1ff75e6e2647c16a3cee024ad 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 }   +// DecodeHolocene1599Params 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..c69dc80f93ac9e040e6a10e420ceaffb5a892724 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) + } + } + } +} + +// TestCalcBaseFeeHolocene 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..d03d01e7d41d265c3a25f5a650c7276f6916a10c 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 { @@ -479,6 +495,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 +514,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..41c475ed57d04a336ccd35a128ec8f71c795a70c 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 @@ -110,6 +126,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 { @@ -333,7 +351,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..5200dae2fc0d24e3ac80556232458fe969a527b7 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,6 +841,14 @@ // 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()   @@ -1108,6 +1146,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..e038b02f61b9f120c439f71241a5e132cecfa99c 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,6 +75,7 @@ func (bc *testBlockChain) CurrentBlock() *types.Header { return &types.Header{ Number: new(big.Int), GasLimit: bc.gasLimit, + BaseFee: big.NewInt(params.InitialBaseFee), } }   @@ -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..2aecb3f2e7ddf3e80f4e898e00f8e67d19c9453d 100644 --- go-ethereum/core/txpool/legacypool/legacypool_test.go +++ op-geth/core/txpool/legacypool/legacypool_test.go @@ -689,6 +689,39 @@ 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) + } +} + // Tests that if a transaction is dropped from the current pending pool (e.g. out // of fund), all consecutive (still valid, but not executable) transactions are // postponed back into the future queue to prevent broadcasting them. @@ -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..f8032507337e24a6fcd32d8b0eba8c9edf3f6485 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,6 +116,12 @@ 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 } @@ -237,6 +250,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 +283,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 +348,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() } @@ -344,7 +406,11 @@ 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 } @@ -411,8 +477,15 @@ }   // 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 root 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)) + } + root = *g.StateHash + } else if root, err = hashAlloc(&g.Alloc, g.IsVerkle()); err != nil { panic(err) } return g.toBlockWithRoot(root) @@ -496,12 +569,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 stateHash common.Hash + if len(g.Alloc) == 0 { + if g.StateHash == nil { + log.Warn("Empty genesis alloc, and no 'stateHash' override was set") + stateHash = types.EmptyRootHash // default to the hash of the empty state. Some unit-tests rely on this. + } else { + stateHash = *g.StateHash + } + } else { + // flush the data to disk and compute the state root + root, err := flushAlloc(&g.Alloc, triedb) + if err != nil { + return nil, err + } + stateHash = root } - block := g.toBlockWithRoot(root) + block := g.toBlockWithRoot(stateHash)   // 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..04cc43a8403775a9e9ba9ddf7c537535c437ced2 100644 --- go-ethereum/params/config.go +++ op-geth/params/config.go @@ -31,6 +31,12 @@ HoleskyGenesisHash = common.HexToHash("0xb5f7f912443c940f21fd611f12828d75b534364ed9e95ca4e307729a4661bde4") SepoliaGenesisHash = common.HexToHash("0x25a5cc106eea7138acab33231d7160d69cb777ee0c2c553fcddf5138993e6dd9") )   +const ( + OPMainnetChainID = 10 + BaseMainnetChainID = 8453 + baseSepoliaChainID = 84532 +) + func newUint64(val uint64) *uint64 { return &val }   var ( @@ -282,6 +288,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 +343,17 @@ 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) + + 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 +370,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 +394,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 +417,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 +497,27 @@ } 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.InteropTime != nil { + banner += fmt.Sprintf(" - Interop: @%-10v\n", *c.InteropTime) + } return banner }   @@ -561,9 +626,81 @@ 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) 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) +} + +// 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 +708,7 @@ ) // Iterate checkCompatible to find the lowest conflict. var lasterr *ConfigCompatError for { - err := c.checkCompatible(newcfg, bhead, btime) + err := c.checkCompatible(newcfg, bhead, btime, genesisTimestamp) if err == nil || (lasterr != nil && err.RewindToBlock == lasterr.RewindToBlock && err.RewindToTime == lasterr.RewindToTime) { break } @@ -654,7 +791,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 +847,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 +927,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 +954,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 +1075,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 +1108,12 @@ 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), } }
diff --git go-ethereum/params/protocol_params.go op-geth/params/protocol_params.go index 638f58a33992f5048396ee306d95b5b6e3c70677..107a9fecfcb245c0c5f223ccc8ef0f9cb2e93ff8 100644 --- go-ethereum/params/protocol_params.go +++ op-geth/params/protocol_params.go @@ -22,6 +22,13 @@ "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") +) + 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 +158,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 +168,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..bab6bec17e0be89f7d179e80560470230d5ee169 --- /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: 1}.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..bbf57c3ccb7a8ede9f5615b5da316c1226da1b1b 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,
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..f5313715b6496609302eab8e539c819baa0af791 100644 --- go-ethereum/eth/downloader/downloader.go +++ op-geth/eth/downloader/downloader.go @@ -139,6 +139,9 @@ // 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. @@ -194,7 +197,7 @@ 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, @@ -207,6 +210,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 +986,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..ab8dc7420fbbc52cfc8892ce2cd3c47c2fdaf9db 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,43 @@ 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) + } + + 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..621891e4b4850c0be9e66c152fd6ab8c9e876b78 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 {
diff --git go-ethereum/core/types/block_test.go op-geth/core/types/block_test.go index 1af5b9d7bf2c99e06d96c5fe226e3ebdbe200b5d..16cb64fa783b67af88e1cd4a20276140a086455a 100644 --- go-ethereum/core/types/block_test.go +++ op-geth/core/types/block_test.go @@ -317,3 +317,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 }

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 productIDs []uint16 // USB product identifiers used for device discovery - usageID uint16 // USB usage page identifier used for macOS device discovery + usageIDs []uint16 // USB usage page identifier used for macOS device discovery endpointID int // USB endpoint identifier used for non-macOS device discovery makeDriver func(log.Logger) driver // Factory method to construct a vendor specific driver   @@ -93,22 +94,22 @@ 0x1011, /* HID + WebUSB Ledger Nano S */ 0x4011, /* HID + WebUSB Ledger Nano X */ 0x5011, /* HID + WebUSB Ledger Nano S Plus */ 0x6011, /* HID + WebUSB Ledger Nano FTS */ - }, 0xffa0, 0, newLedgerDriver) + }, []uint16{0xffa0, 0}, 2, newLedgerDriver) }   // NewTrezorHubWithHID creates a new hardware wallet manager for Trezor devices. func NewTrezorHubWithHID() (*Hub, error) { - return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor HID */}, 0xff00, 0, newTrezorDriver) + return newHub(TrezorScheme, 0x534c, []uint16{0x0001 /* Trezor HID */}, []uint16{0xff00}, 0, newTrezorDriver) }   // NewTrezorHubWithWebUSB creates a new hardware wallet manager for Trezor devices with // firmware version > 1.8.0 func NewTrezorHubWithWebUSB() (*Hub, error) { - return newHub(TrezorScheme, 0x1209, []uint16{0x53c1 /* Trezor WebUSB */}, 0xffff /* No usage id on webusb, don't match unset (0) */, 0, newTrezorDriver) + return newHub(TrezorScheme, 0x1209, []uint16{0x53c1 /* Trezor WebUSB */}, []uint16{0xffff} /* No usage id on webusb, don't match unset (0) */, 0, newTrezorDriver) }   // newHub creates a new hardware wallet manager for generic USB devices. -func newHub(scheme string, vendorID uint16, productIDs []uint16, usageID uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) { +func newHub(scheme string, vendorID uint16, productIDs []uint16, usageIDs []uint16, endpointID int, makeDriver func(log.Logger) driver) (*Hub, error) { if !hid.Supported() { return nil, errors.New("unsupported platform") } @@ -116,7 +117,7 @@ hub := &Hub{ scheme: scheme, vendorID: vendorID, productIDs: productIDs, - usageID: usageID, + usageIDs: usageIDs, endpointID: endpointID, makeDriver: makeDriver, quit: make(chan chan error), @@ -186,7 +187,9 @@ for _, info := range infos { for _, id := range hub.productIDs { // Windows and Macos use UsageID matching, Linux uses Interface matching - if info.ProductID == id && (info.UsagePage == hub.usageID || info.Interface == hub.endpointID) { + if info.ProductID == id && + info.Path != "" && + (slices.Contains(hub.usageIDs, info.UsagePage) || info.Interface == hub.endpointID) { devices = append(devices, info) break }

Additional or modified tests, not already captured by the above diff

diff --git go-ethereum/accounts/abi/packing_test.go op-geth/accounts/abi/packing_test.go index eae3b0df20562372b4e739ba0e72559c0d19bd96..9ed52a475eb900f5be20dfe9441ba25ac330defd 100644 --- go-ethereum/accounts/abi/packing_test.go +++ op-geth/accounts/abi/packing_test.go @@ -375,7 +375,7 @@ { def: `[{"type": "bytes"}]`, packed: "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000020" + - "0100000000000000000000000000000000000000000000000000000000000000", + "0100000000000000000000000000000000000000000000000000000000000000", //nolint:all unpacked: common.Hex2Bytes("0100000000000000000000000000000000000000000000000000000000000000"), }, { @@ -476,7 +476,7 @@ { def: `[{"type": "int256[3]"}]`, packed: "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000002" + - "0000000000000000000000000000000000000000000000000000000000000003", + "0000000000000000000000000000000000000000000000000000000000000003", //nolint:all unpacked: [3]*big.Int{big.NewInt(1), big.NewInt(2), big.NewInt(3)}, }, // multi dimensional, if these pass, all types that don't require length prefix should pass @@ -536,7 +536,7 @@ { def: `[{"type": "uint8[][2]"}]`, packed: "0000000000000000000000000000000000000000000000000000000000000020" + "0000000000000000000000000000000000000000000000000000000000000040" + - "0000000000000000000000000000000000000000000000000000000000000080" + + "0000000000000000000000000000000000000000000000000000000000000080" + //nolint:all "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000001" + "0000000000000000000000000000000000000000000000000000000000000001" + @@ -801,7 +801,7 @@ "0000000000000000000000000000000000000000000000000000000000000002" + // len(array[0]) = 2 "0100000000000000000000000000000000000000000000000000000000000000" + // array[0][0] "0200000000000000000000000000000000000000000000000000000000000000" + // array[0][1] "0000000000000000000000000000000000000000000000000000000000000003" + // len(array[1]) = 3 - "0300000000000000000000000000000000000000000000000000000000000000" + // array[1][0] + "0300000000000000000000000000000000000000000000000000000000000000" + //nolint:all // array[1][0] "0400000000000000000000000000000000000000000000000000000000000000" + // array[1][1] "0500000000000000000000000000000000000000000000000000000000000000", // array[1][2] }, @@ -845,7 +845,7 @@ E [2][3][32]byte }{1, big.NewInt(1), big.NewInt(-1), true, [2][3][32]byte{{{1}, {2}, {3}}, {{3}, {4}, {5}}}}, packed: "0000000000000000000000000000000000000000000000000000000000000001" + // struct[a] "0000000000000000000000000000000000000000000000000000000000000001" + // struct[b] - "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + // struct[c] + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + //nolint:all // struct[c] "0000000000000000000000000000000000000000000000000000000000000001" + // struct[d] "0100000000000000000000000000000000000000000000000000000000000000" + // struct[e] array[0][0] "0200000000000000000000000000000000000000000000000000000000000000" + // struct[e] array[0][1]
diff --git go-ethereum/consensus/misc/create2deployer_test.go op-geth/consensus/misc/create2deployer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..fc0ef79fd7739d92431085d515481be8621e8fcb --- /dev/null +++ op-geth/consensus/misc/create2deployer_test.go @@ -0,0 +1,95 @@ +package misc + +import ( + "math/big" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +func TestEnsureCreate2Deployer(t *testing.T) { + canyonTime := uint64(1000) + var tests = []struct { + name string + override func(cfg *params.ChainConfig) + timestamp uint64 + codeExists bool + applied bool + }{ + { + name: "at hardfork", + timestamp: canyonTime, + applied: true, + }, + { + name: "another chain ID", + override: func(cfg *params.ChainConfig) { + cfg.ChainID = big.NewInt(params.OPMainnetChainID) + }, + timestamp: canyonTime, + applied: true, + }, + { + name: "code already exists", + timestamp: canyonTime, + codeExists: true, + applied: true, + }, + { + name: "pre canyon", + timestamp: canyonTime - 1, + applied: false, + }, + { + name: "post hardfork", + timestamp: canyonTime + 1, + applied: false, + }, + { + name: "canyon not configured", + override: func(cfg *params.ChainConfig) { + cfg.CanyonTime = nil + }, + timestamp: canyonTime, + applied: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := params.ChainConfig{ + ChainID: big.NewInt(params.BaseMainnetChainID), + Optimism: &params.OptimismConfig{}, + CanyonTime: &canyonTime, + } + if tt.override != nil { + tt.override(&cfg) + } + state := &stateDb{ + codeExists: tt.codeExists, + } + EnsureCreate2Deployer(&cfg, tt.timestamp, state) + assert.Equal(t, tt.applied, state.codeSet) + }) + } +} + +type stateDb struct { + vm.StateDB + codeExists bool + codeSet bool +} + +func (s *stateDb) GetCodeSize(_ common.Address) int { + if s.codeExists { + return 1 + } + return 0 +} + +func (s *stateDb) SetCode(_ common.Address, _ []byte) { + s.codeSet = true +}
diff --git go-ethereum/core/rawdb/accessors_chain_test.go op-geth/core/rawdb/accessors_chain_test.go index 2d30af4b3d20d43f9b8b011cb5de251347fe3a1b..cf6cac6d075add43ca657be445001dd80a10204b 100644 --- go-ethereum/core/rawdb/accessors_chain_test.go +++ op-geth/core/rawdb/accessors_chain_test.go @@ -27,10 +27,12 @@ "reflect" "testing"   "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" + "github.com/stretchr/testify/require" "golang.org/x/crypto/sha3" )   @@ -347,6 +349,9 @@ // Create a live block since we need metadata to reconstruct the receipt tx1 := types.NewTransaction(1, common.HexToAddress("0x1"), big.NewInt(1), 1, big.NewInt(1), nil) tx2 := types.NewTransaction(2, common.HexToAddress("0x2"), big.NewInt(2), 2, big.NewInt(2), nil)   + header := &types.Header{ + Number: big.NewInt(0), + } body := &types.Body{Transactions: types.Transactions{tx1, tx2}}   // Create the two receipts to manage afterwards @@ -378,13 +383,16 @@ receipt2.Bloom = types.CreateBloom(types.Receipts{receipt2}) receipts := []*types.Receipt{receipt1, receipt2}   // Check that no receipt entries are in a pristine database - hash := common.BytesToHash([]byte{0x03, 0x14}) + hash := header.Hash() if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) != 0 { t.Fatalf("non existent receipts returned: %v", rs) } // Insert the body that corresponds to the receipts WriteBody(db, hash, 0, body)   + // Insert the header that corresponds to the receipts + WriteHeader(db, header) + // Insert the receipt slice into the database and check presence WriteReceipts(db, hash, 0, receipts) if rs := ReadReceipts(db, hash, 0, 0, params.TestChainConfig); len(rs) == 0 { @@ -694,36 +702,56 @@ // Create a live block since we need metadata to reconstruct the receipt tx1 := types.NewTransaction(1, common.HexToAddress("0x1"), big.NewInt(1), 1, big.NewInt(1), nil) tx2 := types.NewTransaction(2, common.HexToAddress("0x2"), big.NewInt(2), 2, big.NewInt(2), nil) + tx3 := types.NewTransaction(3, common.HexToAddress("0x3"), big.NewInt(3), 3, big.NewInt(3), nil)   - body := &types.Body{Transactions: types.Transactions{tx1, tx2}} + body := &types.Body{Transactions: types.Transactions{tx1, tx2, tx3}}   - // Create the two receipts to manage afterwards - receipt1 := &types.Receipt{ + // Create the three receipts to manage afterwards + depositNonce := uint64(math.MaxUint64) + depositReceipt := types.Receipt{ Status: types.ReceiptStatusFailed, CumulativeGasUsed: 1, Logs: []*types.Log{ {Address: common.BytesToAddress([]byte{0x11})}, {Address: common.BytesToAddress([]byte{0x01, 0x11})}, }, - TxHash: tx1.Hash(), - ContractAddress: common.BytesToAddress([]byte{0x01, 0x11, 0x11}), - GasUsed: 111111, + TxHash: tx1.Hash(), + ContractAddress: common.BytesToAddress([]byte{0x01, 0x11, 0x11}), + GasUsed: 111111, + DepositNonce: &depositNonce, + DepositReceiptVersion: nil, } - receipt1.Bloom = types.CreateBloom(types.Receipts{receipt1}) + depositReceipt.Bloom = types.CreateBloom(types.Receipts{&depositReceipt})   - receipt2 := &types.Receipt{ - PostState: common.Hash{2}.Bytes(), + receiptVersion := types.CanyonDepositReceiptVersion + versionedDepositReceipt := types.Receipt{ + Status: types.ReceiptStatusFailed, CumulativeGasUsed: 2, Logs: []*types.Log{ {Address: common.BytesToAddress([]byte{0x22})}, - {Address: common.BytesToAddress([]byte{0x02, 0x22})}, + {Address: common.BytesToAddress([]byte{0x01, 0x11})}, + }, + TxHash: tx2.Hash(), + ContractAddress: common.BytesToAddress([]byte{0x02, 0x22, 0x22}), + GasUsed: 222222, + DepositNonce: &depositNonce, + DepositReceiptVersion: &receiptVersion, + } + versionedDepositReceipt.Bloom = types.CreateBloom(types.Receipts{&versionedDepositReceipt}) + + receipt := types.Receipt{ + PostState: common.Hash{3}.Bytes(), + CumulativeGasUsed: 3, + Logs: []*types.Log{ + {Address: common.BytesToAddress([]byte{0x33})}, + {Address: common.BytesToAddress([]byte{0x03, 0x33})}, }, - TxHash: tx2.Hash(), - ContractAddress: common.BytesToAddress([]byte{0x02, 0x22, 0x22}), - GasUsed: 222222, + TxHash: tx3.Hash(), + ContractAddress: common.BytesToAddress([]byte{0x03, 0x33, 0x33}), + GasUsed: 333333, } - receipt2.Bloom = types.CreateBloom(types.Receipts{receipt2}) - receipts := []*types.Receipt{receipt1, receipt2} + receipt.Bloom = types.CreateBloom(types.Receipts{&receipt}) + receipts := []*types.Receipt{&receipt, &depositReceipt, &versionedDepositReceipt}   hash := common.BytesToHash([]byte{0x03, 0x14}) // Check that no receipt entries are in a pristine database