diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..1e98f5c395 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,71 @@ +version: 2.1 + +jobs: + build-geth: + docker: + - image: cimg/go:1.18 + resource_class: medium + steps: + - checkout + - run: + command: go run build/ci.go install + unit-test: + resource_class: medium + docker: + - image: cimg/go:1.18 + steps: + - checkout + - run: + command: go run build/ci.go test + lint-geth: + resource_class: medium + docker: + - image: cimg/go:1.18 + steps: + - checkout + - run: + command: go run build/ci.go lint + + push-geth: + docker: + - image: cimg/base:2022.04 + steps: + - when: + condition: + or: + - equal: [ optimism, <> ] + - equal: [ optimism-history, <> ] + steps: + - checkout + - setup_remote_docker: + version: 20.10.12 + - run: + name: Build and push + command: | + echo "$DOCKER_PASS" | docker login -u "$DOCKER_USERNAME" --password-stdin + docker build -t "ethereumoptimism/op-geth:$CIRCLE_SHA1" -f Dockerfile . + docker tag "ethereumoptimism/op-geth:$CIRCLE_SHA1" "ethereumoptimism/op-geth:$CIRCLE_BRANCH" + docker push "ethereumoptimism/op-geth:$CIRCLE_SHA1" + docker push "ethereumoptimism/op-geth:$CIRCLE_BRANCH" + + if [ $CIRCLE_BRANCH = "optimism" ] + then + docker tag "ethereumoptimism/op-geth:$CIRCLE_SHA1" "ethereumoptimism/op-geth:latest" + docker push "ethereumoptimism/op-geth:latest" + pwd + fi + # Below step is required to prevent CircleCI from barfing on a + # job with no steps + - run: echo 0 + +workflows: + main: + jobs: + - build-geth: + name: Build geth + - unit-test: + name: Run unit tests for geth + - lint-geth: + name: Run linter over geth + - push-geth: + name: Push geth diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 008c71feaa..c83eb18d05 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/ethash" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/bloombits" @@ -63,6 +64,8 @@ type SimulatedBackend struct { database ethdb.Database // In memory database to store our testing data blockchain *core.BlockChain // Ethereum blockchain to handle the consensus + consensus consensus.Engine + mu sync.Mutex pendingBlock *types.Block // Currently pending block that will be imported on request pendingState *state.StateDB // Currently pending state that will be the active on request @@ -78,20 +81,93 @@ type SimulatedBackend struct { // and uses a simulated blockchain for testing purposes. // A simulated backend always uses chainID 1337. func NewSimulatedBackendWithDatabase(database ethdb.Database, alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend { - genesis := core.Genesis{ - Config: params.AllEthashProtocolChanges, - GasLimit: gasLimit, - Alloc: alloc, + return NewSimulatedBackendWithOpts(WithDatabase(database), WithAlloc(alloc), WithGasLimit(gasLimit)) +} + +// NewSimulatedBackend creates a new binding backend using a simulated blockchain +// for testing purposes. +// A simulated backend always uses chainID 1337. +func NewSimulatedBackend(alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend { + return NewSimulatedBackendWithOpts(WithGasLimit(gasLimit), WithAlloc(alloc)) +} + +type simulatedBackendConfig struct { + genesis core.Genesis + cacheConfig *core.CacheConfig + database ethdb.Database + vmConfig vm.Config + consensus consensus.Engine +} + +type SimulatedBackendOpt func(s *simulatedBackendConfig) + +func WithDatabase(database ethdb.Database) SimulatedBackendOpt { + return func(s *simulatedBackendConfig) { + s.database = database + } +} + +func WithGasLimit(gasLimit uint64) SimulatedBackendOpt { + return func(s *simulatedBackendConfig) { + s.genesis.GasLimit = gasLimit + } +} + +func WithAlloc(alloc core.GenesisAlloc) SimulatedBackendOpt { + return func(s *simulatedBackendConfig) { + s.genesis.Alloc = alloc + } +} + +func WithCacheConfig(cacheConfig *core.CacheConfig) SimulatedBackendOpt { + return func(s *simulatedBackendConfig) { + s.cacheConfig = cacheConfig } - blockchain, _ := core.NewBlockChain(database, nil, &genesis, nil, ethash.NewFaker(), vm.Config{}, nil, nil) +} + +func WithGenesis(genesis core.Genesis) SimulatedBackendOpt { + return func(s *simulatedBackendConfig) { + s.genesis = genesis + } +} + +func WithVMConfig(vmConfig vm.Config) SimulatedBackendOpt { + return func(s *simulatedBackendConfig) { + s.vmConfig = vmConfig + } +} + +func WithConsensus(consensus consensus.Engine) SimulatedBackendOpt { + return func(s *simulatedBackendConfig) { + s.consensus = consensus + } +} + +// NewSimulatedBackendWithOpts creates a new binding backend based on the given database +// and uses a simulated blockchain for testing purposes. It exposes additional configuration +// options that are useful to +func NewSimulatedBackendWithOpts(opts ...SimulatedBackendOpt) *SimulatedBackend { + config := &simulatedBackendConfig{ + genesis: core.Genesis{Config: params.AllEthashProtocolChanges, GasLimit: 100000000, Alloc: make(core.GenesisAlloc)}, + database: rawdb.NewMemoryDatabase(), + consensus: ethash.NewFaker(), + } + + for _, opt := range opts { + opt(config) + } + + config.genesis.MustCommit(config.database) + blockchain, _ := core.NewBlockChain(config.database, config.cacheConfig, &config.genesis, nil, config.consensus, config.vmConfig, nil, nil) backend := &SimulatedBackend{ - database: database, + database: config.database, blockchain: blockchain, - config: genesis.Config, + config: config.genesis.Config, + consensus: config.consensus, } - filterBackend := &filterBackend{database, blockchain, backend} + filterBackend := &filterBackend{config.database, blockchain, backend} backend.filterSystem = filters.NewFilterSystem(filterBackend, filters.Config{}) backend.events = filters.NewEventSystem(backend.filterSystem, false) @@ -99,13 +175,6 @@ func NewSimulatedBackendWithDatabase(database ethdb.Database, alloc core.Genesis return backend } -// NewSimulatedBackend creates a new binding backend using a simulated blockchain -// for testing purposes. -// A simulated backend always uses chainID 1337. -func NewSimulatedBackend(alloc core.GenesisAlloc, gasLimit uint64) *SimulatedBackend { - return NewSimulatedBackendWithDatabase(rawdb.NewMemoryDatabase(), alloc, gasLimit) -} - // Close terminates the underlying blockchain's update loop. func (b *SimulatedBackend) Close() error { b.blockchain.Stop() @@ -121,6 +190,8 @@ func (b *SimulatedBackend) Commit() common.Hash { if _, err := b.blockchain.InsertChain([]*types.Block{b.pendingBlock}); err != nil { panic(err) // This cannot happen unless the simulator is wrong, fail in that case } + // Don't wait for the async tx indexing + rawdb.WriteTxLookupEntriesByBlock(b.database, b.pendingBlock) blockHash := b.pendingBlock.Hash() // Using the last inserted block here makes it possible to build on a side @@ -139,7 +210,7 @@ func (b *SimulatedBackend) Rollback() { } func (b *SimulatedBackend) rollback(parent *types.Block) { - blocks, _ := core.GenerateChain(b.config, parent, ethash.NewFaker(), b.database, 1, func(int, *core.BlockGen) {}) + blocks, _ := core.GenerateChain(b.config, parent, b.consensus, b.database, 1, func(int, *core.BlockGen) {}) b.pendingBlock = blocks[0] b.pendingState, _ = state.New(b.pendingBlock.Root(), b.blockchain.StateCache(), nil) @@ -646,6 +717,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM txContext := core.NewEVMTxContext(msg) evmContext := core.NewEVMBlockContext(block.Header(), b.blockchain, nil) + evmContext.L1CostFunc = types.NewL1CostFunc(b.config, stateDB) // Create a new environment which holds all relevant information // about the transaction and calling mechanisms. vmEnv := vm.NewEVM(evmContext, txContext, stateDB, b.config, vm.Config{NoBaseFee: true}) @@ -675,7 +747,7 @@ func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transa return fmt.Errorf("invalid transaction nonce: got %d, want %d", tx.Nonce(), nonce) } // Include tx in chain - blocks, receipts := core.GenerateChain(b.config, block, ethash.NewFaker(), b.database, 1, func(number int, block *core.BlockGen) { + blocks, receipts := core.GenerateChain(b.config, block, b.consensus, b.database, 1, func(number int, block *core.BlockGen) { for _, tx := range b.pendingBlock.Transactions() { block.AddTxWithChain(b.blockchain, tx) } @@ -799,7 +871,7 @@ func (b *SimulatedBackend) AdjustTime(adjustment time.Duration) error { return fmt.Errorf("could not find parent") } - blocks, _ := core.GenerateChain(b.config, block, ethash.NewFaker(), b.database, 1, func(number int, block *core.BlockGen) { + blocks, _ := core.GenerateChain(b.config, block, b.consensus, b.database, 1, func(number int, block *core.BlockGen) { block.OffsetTime(int64(adjustment.Seconds())) }) stateDB, _ := b.blockchain.State() @@ -831,6 +903,10 @@ func (m callMsg) Gas() uint64 { return m.CallMsg.Gas } func (m callMsg) Value() *big.Int { return m.CallMsg.Value } func (m callMsg) Data() []byte { return m.CallMsg.Data } func (m callMsg) AccessList() types.AccessList { return m.CallMsg.AccessList } +func (m callMsg) IsSystemTx() bool { return false } +func (m callMsg) IsDepositTx() bool { return false } +func (m callMsg) Mint() *big.Int { return nil } +func (m callMsg) RollupDataGas() uint64 { return 0 } // filterBackend implements filters.Backend to support filtering for logs without // taking bloom-bits acceleration structures into account. diff --git a/beacon/engine/gen_blockparams.go b/beacon/engine/gen_blockparams.go index 0dd2b52597..0a76475841 100644 --- a/beacon/engine/gen_blockparams.go +++ b/beacon/engine/gen_blockparams.go @@ -19,13 +19,24 @@ func (p PayloadAttributes) MarshalJSON() ([]byte, error) { Timestamp hexutil.Uint64 `json:"timestamp" gencodec:"required"` Random common.Hash `json:"prevRandao" gencodec:"required"` SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` + Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty" gencodec:"optional"` + Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` + NoTxPool bool `json:"noTxPool,omitempty" gencodec:"optional"` + GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` } var enc PayloadAttributes enc.Timestamp = hexutil.Uint64(p.Timestamp) enc.Random = p.Random enc.SuggestedFeeRecipient = p.SuggestedFeeRecipient enc.Withdrawals = p.Withdrawals + 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) return json.Marshal(&enc) } @@ -35,7 +46,10 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { Timestamp *hexutil.Uint64 `json:"timestamp" gencodec:"required"` Random *common.Hash `json:"prevRandao" gencodec:"required"` SuggestedFeeRecipient *common.Address `json:"suggestedFeeRecipient" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` + Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty" gencodec:"optional"` + Transactions []hexutil.Bytes `json:"transactions,omitempty" gencodec:"optional"` + NoTxPool *bool `json:"noTxPool,omitempty" gencodec:"optional"` + GasLimit *hexutil.Uint64 `json:"gasLimit,omitempty" gencodec:"optional"` } var dec PayloadAttributes if err := json.Unmarshal(input, &dec); err != nil { @@ -56,5 +70,17 @@ func (p *PayloadAttributes) UnmarshalJSON(input []byte) error { if dec.Withdrawals != nil { p.Withdrawals = dec.Withdrawals } + 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) + } return nil } diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 58f7263119..4cc8af5e06 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -34,12 +34,23 @@ type PayloadAttributes struct { Timestamp uint64 `json:"timestamp" gencodec:"required"` Random common.Hash `json:"prevRandao" gencodec:"required"` SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` - Withdrawals []*types.Withdrawal `json:"withdrawals"` + Withdrawals []*types.Withdrawal `json:"withdrawals,omitempty" gencodec:"optional"` + + // 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"` } // JSON type overrides for PayloadAttributes. type payloadAttributesMarshaling struct { Timestamp hexutil.Uint64 + + Transactions []hexutil.Bytes + GasLimit *hexutil.Uint64 } //go:generate go run github.com/fjl/gencodec -type ExecutableData -field-override executableDataMarshaling -out gen_ed.go diff --git a/build/ci.go b/build/ci.go index 2aad2ac52b..1e7bc345fa 100644 --- a/build/ci.go +++ b/build/ci.go @@ -512,7 +512,7 @@ func doDocker(cmdline []string) { 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} } // If architecture specific image builds are requested, build and push them if *image { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 5ba0702498..757526add7 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -149,6 +149,9 @@ var ( utils.GpoMaxGasPriceFlag, utils.GpoIgnoreGasPriceFlag, utils.MinerNotifyFullFlag, + utils.RollupSequencerHTTPFlag, + utils.RollupHistoricalRPCFlag, + utils.RollupDisableTxPoolGossipFlag, configFileFlag, }, utils.NetworkFlags, utils.DatabasePathFlags) diff --git a/cmd/geth/misccmd.go b/cmd/geth/misccmd.go index d8a523c632..5ca1732e07 100644 --- a/cmd/geth/misccmd.go +++ b/cmd/geth/misccmd.go @@ -137,6 +137,7 @@ func printVersion(ctx *cli.Context) error { 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 a/cmd/utils/flags.go b/cmd/utils/flags.go index 08de71ee83..9587332707 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -34,6 +34,10 @@ import ( "strings" "time" + pcsclite "github.com/gballet/go-libpcsclite" + gopsutil "github.com/shirou/gopsutil/mem" + "github.com/urfave/cli/v2" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" @@ -74,9 +78,6 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" - pcsclite "github.com/gballet/go-libpcsclite" - gopsutil "github.com/shirou/gopsutil/mem" - "github.com/urfave/cli/v2" ) // These are all the command line flags we support. @@ -900,6 +901,25 @@ var ( 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, + } + + RollupDisableTxPoolGossipFlag = &cli.BoolFlag{ + Name: "rollup.disabletxpoolgossip", + Usage: "Disable transaction pool gossip.", + Category: flags.RollupCategory, + } + // Metrics flags MetricsEnabledFlag = &cli.BoolFlag{ Name: "metrics", @@ -1857,7 +1877,14 @@ func SetEthConfig(ctx *cli.Context, stack *node.Node, cfg *ethconfig.Config) { 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) + } + cfg.RollupDisableTxPoolGossip = ctx.Bool(RollupDisableTxPoolGossipFlag.Name) // Override any default configs for hard coded networks. switch { case ctx.Bool(MainnetFlag.Name): diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index eb5aa58ca8..dca750339a 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -445,6 +445,9 @@ func IsTTDReached(chain consensus.ChainHeaderReader, parentHash common.Hash, par 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 { return false, consensus.ErrUnknownAncestor diff --git a/consensus/misc/eip1559.go b/consensus/misc/eip1559.go index 4521b47b36..0b6c8532ac 100644 --- a/consensus/misc/eip1559.go +++ b/consensus/misc/eip1559.go @@ -35,8 +35,10 @@ func VerifyEip1559Header(config *params.ChainConfig, parent, header *types.Heade if !config.IsLondon(parent.Number) { parentGasLimit = parent.GasLimit * config.ElasticityMultiplier() } - if err := VerifyGaslimit(parentGasLimit, header.GasLimit); err != nil { - return err + if config.Optimism == nil { // gasLimit can adjust instantly in optimism + if err := VerifyGaslimit(parentGasLimit, header.GasLimit); err != nil { + return err + } } // Verify the header is not malformed if header.BaseFee == nil { diff --git a/core/state_prefetcher.go b/core/state_prefetcher.go index 867b47db53..4c0a782c10 100644 --- a/core/state_prefetcher.go +++ b/core/state_prefetcher.go @@ -55,6 +55,7 @@ func (p *statePrefetcher) Prefetch(block *types.Block, statedb *state.StateDB, c evm = vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) signer = types.MakeSigner(p.config, header.Number) ) + blockContext.L1CostFunc = types.NewL1CostFunc(p.config, statedb) // Iterate over and process the individual transactions byzantium := p.config.IsByzantium(block.Number()) for i, tx := range block.Transactions() { diff --git a/core/state_processor.go b/core/state_processor.go index 163ea0a020..85b81ef95f 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -71,6 +71,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg misc.ApplyDAOHardFork(statedb) } blockContext := NewEVMBlockContext(header, p.bc, nil) + blockContext.L1CostFunc = types.NewL1CostFunc(p.config, statedb) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, cfg) // Iterate over and process the individual transactions for i, tx := range block.Transactions() { @@ -153,6 +154,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo } // Create a new context to be used in the EVM environment blockContext := NewEVMBlockContext(header, bc, author) + blockContext.L1CostFunc = types.NewL1CostFunc(config, statedb) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) return applyTransaction(msg, config, gp, statedb, header.Number, header.Hash(), tx, usedGas, vmenv) } diff --git a/core/state_transition.go b/core/state_transition.go index 653c6b1836..b85253e951 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -62,6 +62,7 @@ type StateTransition struct { gasTipCap *big.Int initialGas uint64 value *big.Int + mint *big.Int data []byte state vm.StateDB evm *vm.EVM @@ -78,6 +79,11 @@ type Message interface { Gas() uint64 Value() *big.Int + 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. + RollupDataGas() uint64 // RollupDataGas indicates the rollup cost of the message, 0 if not a rollup or no cost. + Nonce() uint64 IsFake() bool Data() []byte @@ -188,6 +194,7 @@ func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition gasFeeCap: msg.GasFeeCap(), gasTipCap: msg.GasTipCap(), value: msg.Value(), + mint: msg.Mint(), data: msg.Data(), state: evm.StateDB, } @@ -215,11 +222,21 @@ func (st *StateTransition) to() common.Address { func (st *StateTransition) buyGas() error { mgval := new(big.Int).SetUint64(st.msg.Gas()) mgval = mgval.Mul(mgval, st.gasPrice) + var l1Cost *big.Int + if st.evm.Context.L1CostFunc != nil { + l1Cost = st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.msg) + } + if l1Cost != nil { + mgval = mgval.Add(mgval, l1Cost) + } balanceCheck := mgval if st.gasFeeCap != nil { balanceCheck = new(big.Int).SetUint64(st.msg.Gas()) balanceCheck = balanceCheck.Mul(balanceCheck, st.gasFeeCap) balanceCheck.Add(balanceCheck, st.value) + if l1Cost != nil { + balanceCheck.Add(balanceCheck, l1Cost) + } } if have, want := st.state.GetBalance(st.msg.From()), balanceCheck; have.Cmp(want) < 0 { return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From().Hex(), have, want) @@ -235,6 +252,17 @@ func (st *StateTransition) buyGas() error { } 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.Gas() + st.gas += st.msg.Gas() // Add gas here in order to be able to execute calls. + // Don't touch the gas pool for system transactions + if st.msg.IsSystemTx() { + return nil + } + return st.gp.SubGas(st.msg.Gas()) // gas used by deposits may not be used by other txs + } // Only check transactions that are not fake if !st.msg.IsFake() { // Make sure this transaction's nonce is correct. @@ -293,6 +321,35 @@ func (st *StateTransition) preCheck() error { // 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 { + st.state.AddBalance(st.msg.From(), mint) + } + 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() { + 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) + gasUsed := st.msg.Gas() + if st.msg.IsSystemTx() { + gasUsed = 0 + } + result = &ExecutionResult{ + UsedGas: gasUsed, + Err: fmt.Errorf("failed deposit: %w", err), + ReturnData: nil, + } + err = nil + } + 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 // @@ -359,6 +416,20 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value) } + // if deposit: skip refunds, skip tipping coinbase + if st.msg.IsDepositTx() { + // 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.Gas() + if st.msg.IsSystemTx() { + gasUsed = 0 + } + return &ExecutionResult{ + UsedGas: gasUsed, + Err: vmerr, + ReturnData: ret, + }, nil + } if !rules.IsLondon { // Before EIP-3529: refunds were capped to gasUsed / 2 st.refundGas(params.RefundQuotient) @@ -381,6 +452,13 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { st.state.AddBalance(st.evm.Context.Coinbase, fee) } + if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil { + st.state.AddBalance(params.OptimismBaseFeeRecipient, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee)) + if cost := st.evm.Context.L1CostFunc(st.evm.Context.BlockNumber.Uint64(), st.msg); cost != nil { + st.state.AddBalance(params.OptimismL1FeeRecipient, cost) + } + } + return &ExecutionResult{ UsedGas: st.gasUsed(), Err: vmerr, diff --git a/core/txpool/list.go b/core/txpool/list.go index 062cbbf63e..4318bfb254 100644 --- a/core/txpool/list.go +++ b/core/txpool/list.go @@ -246,6 +246,15 @@ func (m *sortedMap) LastElement() *types.Transaction { 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- diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index c805201866..5c6fffa2d4 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -252,6 +252,8 @@ type TxPool struct { pendingNonces *noncer // Pending state tracking virtual nonces currentMaxGas uint64 // Current gas limit for transaction caps + l1CostFn func(message types.RollupMessage) *big.Int // Current L1 fee cost function + locals *accountSet // Set of local transaction to exempt from eviction rules journal *journal // Journal of local transaction to back up to disk @@ -588,6 +590,12 @@ func (pool *TxPool) local() map[common.Address]types.Transactions { // validateTx checks whether a transaction is valid according to the consensus // rules and adheres to some heuristic limits of the local node (price and size). func (pool *TxPool) validateTx(tx *types.Transaction, local bool) 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 + } // Accept only legacy transactions until EIP-2718/2930 activates. if !pool.eip2718 && tx.Type() != types.LegacyTxType { return core.ErrTxTypeNotSupported @@ -639,7 +647,11 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { } // Transactor should have enough funds to cover the costs // cost == V + GP * GL - if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 { + cost := tx.Cost() + if l1Cost := pool.l1CostFn(tx); l1Cost != nil { // add rollup cost + cost = cost.Add(cost, l1Cost) + } + if pool.currentState.GetBalance(from).Cmp(cost) < 0 { return core.ErrInsufficientFunds } // Ensure the transaction has more gas than the basic tx fee. @@ -1285,6 +1297,16 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { return } } + // Do not insert deposit txs back into the pool + // (validateTx would still catch it if not filtered, but no need to re-inject in the first place). + j := 0 + for _, tx := range discarded { + if tx.Type() != types.DepositTxType { + discarded[j] = tx + j++ + } + } + discarded = discarded[:j] reinject = types.TxDifference(discarded, included) } } @@ -1302,6 +1324,11 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { pool.pendingNonces = newNoncer(statedb) pool.currentMaxGas = newHead.GasLimit + costFn := types.NewL1CostFunc(pool.chainconfig, statedb) + pool.l1CostFn = func(message types.RollupMessage) *big.Int { + return costFn(newHead.Number.Uint64(), message) + } + // Inject any transactions discarded due to reorgs log.Debug("Reinjecting stale transactions", "count", len(reinject)) core.SenderCacher.Recover(pool.signer, reinject) @@ -1335,8 +1362,15 @@ func (pool *TxPool) promoteExecutables(accounts []common.Address) []*types.Trans pool.all.Remove(hash) } log.Trace("Removed old queued transactions", "count", len(forwards)) + balance := pool.currentState.GetBalance(addr) + if !list.Empty() { + // Reduce the cost-cap by L1 rollup cost of the first tx if necessary. Other txs will get filtered out afterwards. + if l1Cost := pool.l1CostFn(list.txs.FirstElement()); l1Cost != nil { + balance = new(big.Int).Sub(balance, l1Cost) // negative big int is fine + } + } // Drop all transactions that are too costly (low balance or out of gas) - drops, _ := list.Filter(pool.currentState.GetBalance(addr), pool.currentMaxGas) + drops, _ := list.Filter(balance, pool.currentMaxGas) for _, tx := range drops { hash := tx.Hash() pool.all.Remove(hash) @@ -1532,8 +1566,15 @@ func (pool *TxPool) demoteUnexecutables() { pool.all.Remove(hash) log.Trace("Removed old pending transaction", "hash", hash) } + balance := pool.currentState.GetBalance(addr) + if !list.Empty() { + // Reduce the cost-cap by L1 rollup cost of the first tx if necessary. Other txs will get filtered out afterwards. + if l1Cost := pool.l1CostFn(list.txs.FirstElement()); l1Cost != nil { + balance = new(big.Int).Sub(balance, l1Cost) // negative big int is fine + } + } // 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), pool.currentMaxGas) + drops, invalids := list.Filter(balance, pool.currentMaxGas) for _, tx := range drops { hash := tx.Hash() log.Trace("Removed unpayable pending transaction", "hash", hash) diff --git a/core/types/deposit_tx.go b/core/types/deposit_tx.go new file mode 100644 index 0000000000..698e4b327e --- /dev/null +++ b/core/types/deposit_tx.go @@ -0,0 +1,87 @@ +// 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 . + +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" +) + +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) 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 +} diff --git a/core/types/gen_receipt_json.go b/core/types/gen_receipt_json.go index bb892f85be..a506116f33 100644 --- a/core/types/gen_receipt_json.go +++ b/core/types/gen_receipt_json.go @@ -28,6 +28,10 @@ func (r Receipt) MarshalJSON() ([]byte, error) { BlockHash common.Hash `json:"blockHash,omitempty"` BlockNumber *hexutil.Big `json:"blockNumber,omitempty"` TransactionIndex hexutil.Uint `json:"transactionIndex"` + L1GasPrice *hexutil.Big `json:"l1GasPrice,omitempty"` + L1GasUsed *hexutil.Big `json:"l1GasUsed,omitempty"` + L1Fee *hexutil.Big `json:"l1Fee,omitempty"` + FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` } var enc Receipt enc.Type = hexutil.Uint64(r.Type) @@ -42,6 +46,10 @@ func (r Receipt) MarshalJSON() ([]byte, error) { enc.BlockHash = r.BlockHash enc.BlockNumber = (*hexutil.Big)(r.BlockNumber) enc.TransactionIndex = hexutil.Uint(r.TransactionIndex) + enc.L1GasPrice = (*hexutil.Big)(r.L1GasPrice) + enc.L1GasUsed = (*hexutil.Big)(r.L1GasUsed) + enc.L1Fee = (*hexutil.Big)(r.L1Fee) + enc.FeeScalar = r.FeeScalar return json.Marshal(&enc) } @@ -60,6 +68,10 @@ func (r *Receipt) UnmarshalJSON(input []byte) error { BlockHash *common.Hash `json:"blockHash,omitempty"` BlockNumber *hexutil.Big `json:"blockNumber,omitempty"` TransactionIndex *hexutil.Uint `json:"transactionIndex"` + L1GasPrice *hexutil.Big `json:"l1GasPrice,omitempty"` + L1GasUsed *hexutil.Big `json:"l1GasUsed,omitempty"` + L1Fee *hexutil.Big `json:"l1Fee,omitempty"` + FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` } var dec Receipt if err := json.Unmarshal(input, &dec); err != nil { @@ -106,5 +118,17 @@ func (r *Receipt) UnmarshalJSON(input []byte) error { if dec.TransactionIndex != nil { r.TransactionIndex = uint(*dec.TransactionIndex) } + if dec.L1GasPrice != nil { + r.L1GasPrice = (*big.Int)(dec.L1GasPrice) + } + 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 + } return nil } diff --git a/core/types/receipt.go b/core/types/receipt.go index 4404b27889..b37bb6f2c7 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -69,6 +69,12 @@ type Receipt struct { BlockHash common.Hash `json:"blockHash,omitempty"` BlockNumber *big.Int `json:"blockNumber,omitempty"` TransactionIndex uint `json:"transactionIndex"` + + // OVM legacy: extend receipts with their L1 price (if a rollup tx) + L1GasPrice *big.Int `json:"l1GasPrice,omitempty"` + L1GasUsed *big.Int `json:"l1GasUsed,omitempty"` + L1Fee *big.Int `json:"l1Fee,omitempty"` + FeeScalar *big.Float `json:"l1FeeScalar,omitempty"` } type receiptMarshaling struct { @@ -79,6 +85,12 @@ type receiptMarshaling struct { GasUsed hexutil.Uint64 BlockNumber *hexutil.Big TransactionIndex hexutil.Uint + + // Optimism: extend receipts with their L1 price (if a rollup tx) + L1GasPrice *hexutil.Big + L1GasUsed *hexutil.Big + L1Fee *hexutil.Big + FeeScalar *big.Float } // receiptRLP is the consensus encoding of a receipt. @@ -193,7 +205,7 @@ func (r *Receipt) decodeTyped(b []byte) error { return errShortTypedReceipt } switch b[0] { - case DynamicFeeTxType, AccessListTxType: + case DynamicFeeTxType, AccessListTxType, DepositTxType: var data receiptRLP err := rlp.DecodeBytes(b[1:], &data) if err != nil { @@ -304,6 +316,9 @@ func (rs Receipts) EncodeIndex(i int, w *bytes.Buffer) { case DynamicFeeTxType: w.WriteByte(DynamicFeeTxType) rlp.Encode(w, data) + case DepositTxType: + w.WriteByte(DepositTxType) + rlp.Encode(w, data) default: // For unsupported types, write nothing. Since this is for // DeriveSha, the error will be caught matching the derived hash @@ -352,5 +367,26 @@ func (rs Receipts) DeriveFields(config *params.ChainConfig, hash common.Hash, nu logIndex++ } } + if config.Optimism != nil && len(txs) >= 2 { // need at least an info tx and a non-info tx + if data := txs[0].Data(); len(data) >= 4+32*8 { // function selector + 8 arguments to setL1BlockValues + l1Basefee := new(big.Int).SetBytes(data[4+32*2 : 4+32*3]) // arg index 2 + overhead := new(big.Int).SetBytes(data[4+32*6 : 4+32*7]) // arg index 6 + scalar := new(big.Int).SetBytes(data[4+32*7 : 4+32*8]) // arg index 7 + fscalar := new(big.Float).SetInt(scalar) // legacy: format fee scalar as big Float + fdivisor := new(big.Float).SetUint64(1_000_000) // 10**6, i.e. 6 decimals + feeScalar := new(big.Float).Quo(fscalar, fdivisor) + for i := 0; i < len(rs); i++ { + if !txs[i].IsDepositTx() { + rs[i].L1GasPrice = l1Basefee + rs[i].L1GasUsed = new(big.Int).SetUint64(txs[i].RollupDataGas()) + rs[i].L1Fee = L1Cost(txs[i].RollupDataGas(), l1Basefee, overhead, scalar) + rs[i].FeeScalar = feeScalar + } + } + } else { + return fmt.Errorf("L1 info tx only has %d bytes, cannot read gas price parameters", len(data)) + } + } + return nil } diff --git a/core/types/rollup_l1_cost.go b/core/types/rollup_l1_cost.go new file mode 100644 index 0000000000..edb2138881 --- /dev/null +++ b/core/types/rollup_l1_cost.go @@ -0,0 +1,74 @@ +// 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 . + +package types + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params" +) + +type RollupMessage interface { + RollupDataGas() uint64 + IsDepositTx() bool +} + +type StateGetter interface { + GetState(common.Address, common.Hash) common.Hash +} + +// L1CostFunc is used in the state transition to determine the cost of a rollup message. +// Returns nil if there is no cost. +type L1CostFunc func(blockNum uint64, msg RollupMessage) *big.Int + +var ( + L1BaseFeeSlot = common.BigToHash(big.NewInt(1)) + OverheadSlot = common.BigToHash(big.NewInt(5)) + ScalarSlot = common.BigToHash(big.NewInt(6)) +) + +var L1BlockAddr = common.HexToAddress("0x4200000000000000000000000000000000000015") + +// NewL1CostFunc returns a function used for calculating L1 fee cost. +// This depends on the oracles because gas costs can change over time. +// It returns nil if there is no applicable cost function. +func NewL1CostFunc(config *params.ChainConfig, statedb StateGetter) L1CostFunc { + cacheBlockNum := ^uint64(0) + var l1BaseFee, overhead, scalar *big.Int + return func(blockNum uint64, msg RollupMessage) *big.Int { + rollupDataGas := msg.RollupDataGas() // Only fake txs for RPC view-calls are 0. + if config.Optimism == nil || msg.IsDepositTx() || rollupDataGas == 0 { + return nil + } + if blockNum != cacheBlockNum { + l1BaseFee = statedb.GetState(L1BlockAddr, L1BaseFeeSlot).Big() + overhead = statedb.GetState(L1BlockAddr, OverheadSlot).Big() + scalar = statedb.GetState(L1BlockAddr, ScalarSlot).Big() + cacheBlockNum = blockNum + } + return L1Cost(rollupDataGas, l1BaseFee, overhead, scalar) + } +} + +func L1Cost(rollupDataGas uint64, l1BaseFee, overhead, scalar *big.Int) *big.Int { + l1GasUsed := new(big.Int).SetUint64(rollupDataGas) + l1GasUsed = l1GasUsed.Add(l1GasUsed, overhead) + l1Cost := l1GasUsed.Mul(l1GasUsed, l1BaseFee) + l1Cost = l1Cost.Mul(l1Cost, scalar) + return l1Cost.Div(l1Cost, big.NewInt(1_000_000)) +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 353e0e599c..98ec77e8ed 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -28,6 +28,8 @@ import ( "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/params" "github.com/ethereum/go-ethereum/rlp" ) @@ -56,6 +58,9 @@ type Transaction struct { hash atomic.Value size atomic.Value from atomic.Value + + // cache how much gas the tx takes on L1 for its share of rollup data + rollupGas atomic.Value } // NewTx creates a new transaction. @@ -82,6 +87,7 @@ type TxData interface { value() *big.Int nonce() uint64 to() *common.Address + isSystemTx() bool rawSignatureValues() (v, r, s *big.Int) setSignatureValues(chainID, v, r, s *big.Int) @@ -184,6 +190,10 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { var inner DynamicFeeTx err := rlp.DecodeBytes(b[1:], &inner) return &inner, err + case DepositTxType: + var inner DepositTx + err := rlp.DecodeBytes(b[1:], &inner) + return &inner, err default: return nil, ErrTxTypeNotSupported } @@ -285,6 +295,36 @@ 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 { + if dep, ok := tx.inner.(*DepositTx); ok { + return dep.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 { + if dep, ok := tx.inner.(*DepositTx); ok { + return dep.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 + value. func (tx *Transaction) Cost() *big.Int { total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) @@ -292,6 +332,34 @@ func (tx *Transaction) Cost() *big.Int { return total } +// RollupDataGas is the amount of gas it takes to confirm the tx on L1 as a rollup +func (tx *Transaction) RollupDataGas() uint64 { + if tx.Type() == DepositTxType { + return 0 + } + if v := tx.rollupGas.Load(); v != nil { + return v.(uint64) + } + 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) + } + var zeroes uint64 + var ones uint64 + for _, byt := range data { + if byt == 0 { + zeroes++ + } else { + ones++ + } + } + zeroesGas := zeroes * params.TxDataZeroGas + onesGas := (ones + 68) * params.TxDataNonZeroGasEIP2028 + total := zeroesGas + onesGas + tx.rollupGas.Store(total) + return total +} + // RawSignatureValues returns the V, R, S signature values of the transaction. // The return values should not be modified by the caller. func (tx *Transaction) RawSignatureValues() (v, r, s *big.Int) { @@ -322,6 +390,9 @@ func (tx *Transaction) GasTipCapIntCmp(other *big.Int) int { // 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 } @@ -596,6 +667,11 @@ type Message struct { data []byte accessList AccessList isFake bool + // Optimism rollup fields + isSystemTx bool + isDepositTx bool + mint *big.Int + l1CostGas uint64 } func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data []byte, accessList AccessList, isFake bool) Message { @@ -611,6 +687,11 @@ func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *b data: data, accessList: accessList, isFake: isFake, + // Optimism rollup fields + isSystemTx: false, + isDepositTx: false, + mint: nil, + l1CostGas: 0, } } @@ -627,6 +708,11 @@ func (tx *Transaction) AsMessage(s Signer, baseFee *big.Int) (Message, error) { data: tx.Data(), accessList: tx.AccessList(), isFake: false, + // Optimism rollup fields + isSystemTx: tx.inner.isSystemTx(), + isDepositTx: tx.IsDepositTx(), + mint: tx.Mint(), + l1CostGas: tx.RollupDataGas(), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -648,6 +734,10 @@ func (m Message) Nonce() uint64 { return m.nonce } func (m Message) Data() []byte { return m.data } func (m Message) AccessList() AccessList { return m.accessList } func (m Message) IsFake() bool { return m.isFake } +func (m Message) IsSystemTx() bool { return m.isSystemTx } +func (m Message) IsDepositTx() bool { return m.isDepositTx } +func (m Message) Mint() *big.Int { return m.mint } +func (m Message) RollupDataGas() uint64 { return m.l1CostGas } // copyAddressPtr copies an address. func copyAddressPtr(a *common.Address) *common.Address { diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index 2566d0b8d6..ea36a8a638 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -42,6 +42,12 @@ type txJSON struct { S *hexutil.Big `json:"s"` To *common.Address `json:"to"` + // 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"` + // Access list transaction fields: ChainID *hexutil.Big `json:"chainId,omitempty"` AccessList *AccessList `json:"accessList,omitempty"` @@ -94,6 +100,18 @@ func (tx *Transaction) MarshalJSON() ([]byte, error) { enc.V = (*hexutil.Big)(itx.V) enc.R = (*hexutil.Big)(itx.R) enc.S = (*hexutil.Big)(itx.S) + case *DepositTx: + enc.Gas = (*hexutil.Uint64)(&itx.Gas) + enc.Value = (*hexutil.Big)(itx.Value) + enc.Data = (*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. } return json.Marshal(&enc) } @@ -262,7 +280,39 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error { return err } } - + case DepositTxType: + if dec.AccessList != nil || dec.V != nil || dec.R != nil || dec.S != nil || dec.MaxFeePerGas != nil || + dec.MaxPriorityFeePerGas != nil || dec.GasPrice != nil || (dec.Nonce != nil && *dec.Nonce != 0) { + return errors.New("unexpected field(s) in deposit transaction") + } + var itx DepositTx + inner = &itx + if dec.To != nil { + itx.To = dec.To + } + 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.Data == nil { + return errors.New("missing required field 'input' in transaction") + } + itx.Data = *dec.Data + 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 + } default: return ErrTxTypeNotSupported } diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index 87f0390a6f..70a10eb1c1 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -182,6 +182,9 @@ func NewLondonSigner(chainId *big.Int) Signer { } func (s londonSigner) Sender(tx *Transaction) (common.Address, error) { + if tx.Type() == DepositTxType { + return tx.inner.(*DepositTx).From, nil + } if tx.Type() != DynamicFeeTxType { return s.eip2930Signer.Sender(tx) } @@ -201,6 +204,9 @@ func (s londonSigner) Equal(s2 Signer) bool { } 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) @@ -218,6 +224,9 @@ func (s londonSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big // 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) } diff --git a/core/types/tx_access_list.go b/core/types/tx_access_list.go index 620848fe62..2ed0295cd3 100644 --- a/core/types/tx_access_list.go +++ b/core/types/tx_access_list.go @@ -105,6 +105,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) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/tx_dynamic_fee.go b/core/types/tx_dynamic_fee.go index 53f246ea1f..697da1ffd0 100644 --- a/core/types/tx_dynamic_fee.go +++ b/core/types/tx_dynamic_fee.go @@ -93,6 +93,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) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/types/tx_legacy.go b/core/types/tx_legacy.go index 14d307829c..978e69f2dc 100644 --- a/core/types/tx_legacy.go +++ b/core/types/tx_legacy.go @@ -102,6 +102,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) rawSignatureValues() (v, r, s *big.Int) { return tx.V, tx.R, tx.S diff --git a/core/vm/evm.go b/core/vm/evm.go index d78ea07926..9ab030dbab 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -20,10 +20,12 @@ import ( "math/big" "sync/atomic" + "github.com/holiman/uint256" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/params" - "github.com/holiman/uint256" ) // emptyCodeHash is used by create to ensure deployment is disallowed to already @@ -66,6 +68,8 @@ type BlockContext struct { 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 diff --git a/eth/api_backend.go b/eth/api_backend.go index 8fd3e43300..f68532557c 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -19,12 +19,14 @@ package eth 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/core" "github.com/ethereum/go-ethereum/core/bloombits" @@ -183,7 +185,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.B 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) return stateDb, header, err @@ -199,7 +201,7 @@ func (b *EthAPIBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockN 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") @@ -231,6 +233,7 @@ func (b *EthAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *sta } txContext := core.NewEVMTxContext(msg) context := core.NewEVMBlockContext(header, b.eth.BlockChain(), nil) + context.L1CostFunc = types.NewL1CostFunc(b.eth.blockchain.Config(), state) return vm.NewEVM(context, txContext, state, b.eth.blockchain.Config(), *vmConfig), state.Error, nil } @@ -258,8 +261,17 @@ func (b *EthAPIBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscri return b.eth.BlockChain().SubscribeLogsEvent(ch) } -func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { - return b.eth.txPool.AddLocal(signedTx) +func (b *EthAPIBackend) SendTx(ctx context.Context, tx *types.Transaction) error { + if b.eth.seqRPCService != nil { + data, err := tx.MarshalBinary() + if err != nil { + return err + } + if err := b.eth.seqRPCService.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data)); err != nil { + return err + } + } + return b.eth.txPool.AddLocal(tx) } func (b *EthAPIBackend) GetPoolTransactions() (types.Transactions, error) { @@ -382,3 +394,11 @@ func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, re func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (core.Message, 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 a/eth/backend.go b/eth/backend.go index 6368c0e03c..fc4e9a1e01 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -18,12 +18,14 @@ package eth import ( + "context" "errors" "fmt" "math/big" "runtime" "sync" "sync/atomic" + "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" @@ -74,6 +76,9 @@ type Ethereum struct { snapDialCandidates enode.Iterator merger *consensus.Merger + seqRPCService *rpc.Client + historicalRPCService *rpc.Client + // DB interfaces chainDb ethdb.Database // Block chain database @@ -226,6 +231,7 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { EventMux: eth.eventMux, Checkpoint: checkpoint, RequiredBlocks: config.RequiredBlocks, + NoTxGossip: config.RollupDisableTxPoolGossip, }); err != nil { return nil, err } @@ -254,6 +260,26 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { return nil, err } + 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(), 5*time.Second) + client, err := rpc.DialContext(ctx, config.RollupHistoricalRPC) + cancel() + if err != nil { + return nil, err + } + eth.historicalRPCService = client + } + // Start the RPC service eth.netRPCService = ethapi.NewNetAPI(eth.p2pServer, config.NetworkId) @@ -272,7 +298,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, @@ -533,6 +559,12 @@ func (s *Ethereum) Stop() error { s.miner.Close() s.blockchain.Stop() s.engine.Close() + if s.seqRPCService != nil { + s.seqRPCService.Close() + } + if s.historicalRPCService != nil { + s.historicalRPCService.Close() + } // Clean shutdown marker as the last thing before closing db s.shutdownTracker.Stop() diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index 95eed408f0..789cf04817 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -149,6 +149,9 @@ func NewConsensusAPI(eth *eth.Ethereum) *ConsensusAPI { invalidTipsets: make(map[common.Hash]*types.Header), } eth.Downloader().SetBadBlockCallback(api.setInvalidAncestor) + if api.eth.BlockChain().Config().Optimism != nil { // don't start the api heartbeat, there is no transition + return api + } go api.heartbeat() return api @@ -286,7 +289,7 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl // 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().NumberU64()) @@ -330,12 +333,26 @@ func (api *ConsensusAPI) forkchoiceUpdated(update engine.ForkchoiceStateV1, payl // sealed by the beacon client. The payload will be requested later, and we // will replace it arbitrarily many times in between. if payloadAttributes != nil { + if api.eth.BlockChain().Config().Optimism != nil && payloadAttributes.GasLimit == nil { + return engine.STATUS_INVALID, engine.InvalidPayloadAttributes.With(errors.New("gasLimit parameter is required")) + } + 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, + NoTxPool: payloadAttributes.NoTxPool, + Transactions: transactions, + GasLimit: payloadAttributes.GasLimit, } id := args.Id() // If we already are busy generating this work, then we do not need diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index db686c5d08..722ffd1097 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -207,6 +207,10 @@ type Config struct { // OverrideShanghai (TODO: remove after the fork) OverrideShanghai *uint64 `toml:",omitempty"` + + RollupSequencerHTTP string + RollupHistoricalRPC string + RollupDisableTxPoolGossip bool } // CreateConsensusEngine creates a consensus engine for the given chain configuration. diff --git a/eth/handler.go b/eth/handler.go index 078133f059..42b5d371d0 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -87,6 +87,7 @@ type handlerConfig struct { EventMux *event.TypeMux // Legacy event mux, deprecate for `feed` Checkpoint *params.TrustedCheckpoint // Hard coded checkpoint for sync challenges RequiredBlocks map[uint64]common.Hash // Hard coded map of required block hashes for sync challenges + NoTxGossip bool // Disable P2P transaction gossip } type handler struct { @@ -104,6 +105,8 @@ type handler struct { chain *core.BlockChain maxPeers int + noTxGossip bool + downloader *downloader.Downloader blockFetcher *fetcher.BlockFetcher txFetcher *fetcher.TxFetcher @@ -137,6 +140,7 @@ func newHandler(config *handlerConfig) (*handler, error) { eventMux: config.EventMux, database: config.Database, txpool: config.TxPool, + noTxGossip: config.NoTxGossip, chain: config.Chain, peers: newPeerSet(), merger: config.Merger, diff --git a/eth/handler_eth.go b/eth/handler_eth.go index 4ed6335769..6f573cc3e9 100644 --- a/eth/handler_eth.go +++ b/eth/handler_eth.go @@ -34,7 +34,20 @@ import ( 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 { @@ -52,6 +65,9 @@ func (h *ethHandler) PeerInfo(id enode.ID) interface{} { // 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 atomic.LoadUint32(&h.acceptTxs) == 1 } diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 3bb1464952..bc8934414e 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -218,6 +218,7 @@ func (eth *Ethereum) stateAtTransaction(ctx context.Context, block *types.Block, if idx == txIndex { return msg, context, statedb, release, nil } + context.L1CostFunc = types.NewL1CostFunc(eth.blockchain.Config(), statedb) // Not yet the searched for transaction, execute on top of the current state vmenv := vm.NewEVM(context, txContext, statedb, eth.blockchain.Config(), vm.Config{}) statedb.SetTxContext(tx.Hash(), idx) diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 55c56b40c9..2040ef2a35 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -28,6 +28,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" @@ -88,6 +89,7 @@ type Backend interface { 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) (core.Message, vm.BlockContext, *state.StateDB, StateReleaseFunc, error) + HistoricalRPCService() *rpc.Client } // API is the collection of tracing APIs exposed over the private debugging endpoint. @@ -138,7 +140,7 @@ func (api *API) blockByNumber(ctx context.Context, number rpc.BlockNumber) (*typ return nil, err } if block == nil { - return nil, fmt.Errorf("block #%d not found", number) + return nil, fmt.Errorf("block #%d %w", number, ethereum.NotFound) } return block, nil } @@ -151,7 +153,7 @@ func (api *API) blockByHash(ctx context.Context, hash common.Hash) (*types.Block return nil, err } if block == nil { - return nil, fmt.Errorf("block %s not found", hash.Hex()) + return nil, fmt.Errorf("block %s %w", hash.Hex(), ethereum.NotFound) } return block, nil } @@ -291,6 +293,7 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed signer = types.MakeSigner(api.backend.ChainConfig(), task.block.Number()) blockCtx = core.NewEVMBlockContext(task.block.Header(), api.chainContext(ctx), nil) ) + blockCtx.L1CostFunc = types.NewL1CostFunc(api.backend.ChainConfig(), task.statedb) // Trace all the transactions contained within for i, tx := range task.block.Transactions() { msg, _ := tx.AsMessage(signer, task.block.BaseFee()) @@ -455,7 +458,14 @@ func (api *API) traceChain(start, end *types.Block, config *TraceConfig, closed // EVM and returns them as a JSON object. func (api *API) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *TraceConfig) ([]*txTraceResult, error) { block, err := api.blockByNumber(ctx, number) - if err != nil { + if errors.Is(err, ethereum.NotFound) && api.backend.HistoricalRPCService() != nil { + var histResult []*txTraceResult + err = api.backend.HistoricalRPCService().CallContext(ctx, &histResult, "debug_traceBlockByNumber", number, config) + if err != nil && err.Error() == "not found" { + return nil, fmt.Errorf("block #%d %w", number, ethereum.NotFound) + } + return histResult, err + } else if err != nil { return nil, err } return api.traceBlock(ctx, block, config) @@ -465,7 +475,14 @@ func (api *API) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, // EVM and returns them as a JSON object. func (api *API) TraceBlockByHash(ctx context.Context, hash common.Hash, config *TraceConfig) ([]*txTraceResult, error) { block, err := api.blockByHash(ctx, hash) - if err != nil { + if errors.Is(err, ethereum.NotFound) && api.backend.HistoricalRPCService() != nil { + var histResult []*txTraceResult + err = api.backend.HistoricalRPCService().CallContext(ctx, &histResult, "debug_traceBlockByHash", hash, config) + if err != nil && err.Error() == "not found" { + return nil, fmt.Errorf("block #%d %w", hash, ethereum.NotFound) + } + return histResult, err + } else if err != nil { return nil, err } return api.traceBlock(ctx, block, config) @@ -548,6 +565,7 @@ func (api *API) IntermediateRoots(ctx context.Context, hash common.Hash, config vmctx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) deleteEmptyObjects = chainConfig.IsEIP158(block.Number()) ) + vmctx.L1CostFunc = types.NewL1CostFunc(chainConfig, statedb) for i, tx := range block.Transactions() { if err := ctx.Err(); err != nil { return nil, err @@ -653,7 +671,6 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat var ( txs = block.Transactions() blockHash = block.Hash() - blockCtx = core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) signer = types.MakeSigner(api.backend.ChainConfig(), block.Number()) results = make([]*txTraceResult, len(txs)) pend sync.WaitGroup @@ -669,6 +686,8 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat defer pend.Done() // Fetch and execute the next transaction trace tasks for task := range jobs { + blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx.L1CostFunc = types.NewL1CostFunc(api.backend.ChainConfig(), task.statedb) msg, _ := txs[task.index].AsMessage(signer, block.BaseFee()) txctx := &Context{ BlockHash: blockHash, @@ -687,6 +706,8 @@ func (api *API) traceBlockParallel(ctx context.Context, block *types.Block, stat // Feed the transactions into the tracers and return var failed error + blockCtx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil) + blockCtx.L1CostFunc = types.NewL1CostFunc(api.backend.ChainConfig(), statedb) txloop: for i, tx := range txs { // Send the trace task over for execution @@ -776,6 +797,7 @@ func (api *API) standardTraceBlockToFile(ctx context.Context, block *types.Block // Note: This copies the config, to not screw up the main config chainConfig, canon = overrideConfig(chainConfig, config.Overrides) } + vmctx.L1CostFunc = types.NewL1CostFunc(chainConfig, statedb) for i, tx := range block.Transactions() { // Prepare the transaction for un-traced execution var ( @@ -851,9 +873,13 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config * if err != nil { return nil, err } - // Only mined txes are supported if tx == nil { - return nil, errTxNotFound + var histResult []*txTraceResult + err = api.backend.HistoricalRPCService().CallContext(ctx, &histResult, "debug_traceTransaction", hash, config) + if err != nil && err.Error() == "not found" { + return nil, fmt.Errorf("transaction %s %w", hash, ethereum.NotFound) + } + return histResult, err } // It shouldn't happen in practice. if blockNumber == 0 { @@ -905,6 +931,29 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc } else { return nil, errors.New("invalid arguments; neither block nor hash specified") } + + // If block still holds no value, but we have an error, then one of the two previous conditions + // was entered, meaning: + // 1. blockNrOrHash has either a valid block or hash + // 2. we don't have that block locally + if block == nil && errors.Is(err, ethereum.NotFound) && api.backend.HistoricalRPCService() != nil { + var histResult json.RawMessage + err = api.backend.HistoricalRPCService().CallContext(ctx, &histResult, "debug_traceCall", args, blockNrOrHash, config) + if err != nil && err.Error() == "not found" { + // Not found locally or in history. We need to return different errors based on the input + // in order match geth's native behavior + if hash, ok := blockNrOrHash.Hash(); ok { + return nil, fmt.Errorf("block %s %w", hash, ethereum.NotFound) + } else if number, ok := blockNrOrHash.Number(); ok { + return nil, fmt.Errorf("block #%d %w", number, ethereum.NotFound) + } + } else if err != nil { + return nil, fmt.Errorf("error querying historical RPC: %w", err) + } + + return histResult, nil + } + if err != nil { return nil, err } @@ -927,6 +976,7 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc } config.BlockOverrides.Apply(&vmctx) } + vmctx.L1CostFunc = types.NewL1CostFunc(api.backend.ChainConfig(), statedb) // Execute the trace msg, err := args.ToMessage(api.backend.RPCGasCap(), block.BaseFee()) if err != nil { diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 29ec808685..6653baafbe 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -24,12 +24,15 @@ import ( "errors" "fmt" "math/big" + "net" + "net/http" "reflect" "sort" "sync/atomic" "testing" "time" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/consensus" @@ -43,6 +46,7 @@ import ( "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" ) @@ -52,6 +56,83 @@ var ( errBlockNotFound = errors.New("block not found") ) +var errFailingUpstream = errors.New("historical query failed") + +type mockHistoricalBackend struct{} + +func (m *mockHistoricalBackend) TraceBlockByHash(ctx context.Context, hash common.Hash, config *TraceConfig) ([]*txTraceResult, error) { + if hash == common.HexToHash("0xabba") { + result := make([]*txTraceResult, 1) + result[0] = &txTraceResult{Result: "0xabba"} + return result, nil + } + return nil, ethereum.NotFound +} + +func (m *mockHistoricalBackend) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *TraceConfig) ([]*txTraceResult, error) { + if number == 999 { + result := make([]*txTraceResult, 1) + result[0] = &txTraceResult{Result: "0xabba"} + return result, nil + } + return nil, ethereum.NotFound +} + +func (m *mockHistoricalBackend) TraceTransaction(ctx context.Context, hash common.Hash, config *TraceConfig) (interface{}, error) { + if hash == common.HexToHash("0xACDC") { + result := make([]*txTraceResult, 1) + result[0] = &txTraceResult{Result: "0x8888"} + return result, nil + } + return nil, ethereum.NotFound +} + +func (m *mockHistoricalBackend) TraceCall(ctx context.Context, args ethapi.TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, config *TraceCallConfig) (interface{}, error) { + num, ok := blockNrOrHash.Number() + if ok && num == 777 { + return json.RawMessage(`{"gas":21000,"failed":false,"returnValue":"777","structLogs":[]}`), nil + } + if ok && num == 12345 { + return nil, errFailingUpstream + } + return nil, ethereum.NotFound +} + +func newMockHistoricalBackend(t *testing.T) string { + s := rpc.NewServer() + err := node.RegisterApis([]rpc.API{ + { + Namespace: "debug", + 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()) +} + type testBackend struct { chainConfig *params.ChainConfig engine consensus.Engine @@ -60,15 +141,25 @@ type testBackend struct { 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 } // testBackend 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 { + historicalAddr := newMockHistoricalBackend(t) + + 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(), + historical: historicalClient, } // Generate blocks for testing _, blocks, _ := core.GenerateChainWithGenesis(gspec, backend.engine, n, generator) @@ -116,6 +207,9 @@ func (b *testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) func (b *testBackend) GetTransaction(ctx context.Context, txHash common.Hash) (*types.Transaction, common.Hash, uint64, uint64, error) { tx, hash, blockNumber, index := rawdb.ReadTransaction(b.chaindb, txHash) + if tx == nil { + return nil, common.Hash{}, 0, 0, nil + } return tx, hash, blockNumber, index, nil } @@ -177,6 +271,7 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block if idx == txIndex { return msg, context, statedb, release, nil } + context.L1CostFunc = types.NewL1CostFunc(b.chainConfig, statedb) vmenv := vm.NewEVM(context, txContext, statedb, b.chainConfig, vm.Config{}) if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { return nil, vm.BlockContext{}, nil, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) @@ -186,6 +281,10 @@ func (b *testBackend) StateAtTransaction(ctx context.Context, block *types.Block 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) { t.Parallel() @@ -250,9 +349,43 @@ func TestTraceCall(t *testing.T) { Value: (*hexutil.Big)(big.NewInt(1000)), }, config: nil, - expectErr: fmt.Errorf("block #%d not found", genBlocks+1), + expectErr: fmt.Errorf("block #%d %w", genBlocks+1, ethereum.NotFound), //expect: nil, }, + // Optimism: Trace block on the historical chain + { + blockNumber: rpc.BlockNumber(777), + call: ethapi.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: nil, + expect: `{"gas":21000,"failed":false,"returnValue":"777","structLogs":[]}`, + }, + // Optimism: Trace block that doesn't exist anywhere + { + blockNumber: rpc.BlockNumber(39347856), + call: ethapi.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: fmt.Errorf("block #39347856 %w", ethereum.NotFound), + }, + // Optimism: Trace block with failing historical upstream + { + blockNumber: rpc.BlockNumber(12345), + call: ethapi.TransactionArgs{ + From: &accounts[0].addr, + To: &accounts[1].addr, + Value: (*hexutil.Big)(big.NewInt(1000)), + }, + config: nil, + expectErr: fmt.Errorf("error querying historical RPC: %w", errFailingUpstream), + }, // Standard JSON trace upon the latest block { blockNumber: rpc.LatestBlockNumber, @@ -298,8 +431,10 @@ func TestTraceCall(t *testing.T) { t.Errorf("test %d: expect error %v, got nothing", i, testspec.expectErr) continue } - if !reflect.DeepEqual(err, testspec.expectErr) { - t.Errorf("test %d: error mismatch, want %v, git %v", i, testspec.expectErr, err) + // Have to introduce this diff to reflect the fact that errors + // from the upstream will not preserve pointer equality. + if err.Error() != testspec.expectErr.Error() { + t.Errorf("test %d: error mismatch, want %v, got %v", i, testspec.expectErr, err) } } else { if err != nil { @@ -367,6 +502,17 @@ func TestTraceTransaction(t *testing.T) { if !errors.Is(err, errTxNotFound) { t.Fatalf("want %v, have %v", errTxNotFound, err) } + // test TraceTransaction for a historical transaction + result2, err := api.TraceTransaction(context.Background(), common.HexToHash("0xACDC"), nil) + resBytes, _ := json.Marshal(result2) + have2 := string(resBytes) + if err != nil { + t.Errorf("want no error, have %v", err) + } + want2 := `[{"result":"0x8888"}]` + if have2 != want2 { + t.Errorf("test result mismatch, have\n%v\n, want\n%v\n", have2, want2) + } } func TestTraceBlock(t *testing.T) { @@ -413,7 +559,12 @@ func TestTraceBlock(t *testing.T) { // Trace non-existent block { blockNumber: rpc.BlockNumber(genBlocks + 1), - expectErr: fmt.Errorf("block #%d not found", genBlocks+1), + expectErr: fmt.Errorf("block #%d %w", genBlocks+1, ethereum.NotFound), + }, + // Optimism: Trace block on the historical chain + { + blockNumber: rpc.BlockNumber(999), + want: `[{"result":"0xabba"}]`, }, // Trace latest block { @@ -433,7 +584,7 @@ func TestTraceBlock(t *testing.T) { t.Errorf("test %d, want error %v", i, tc.expectErr) continue } - if !reflect.DeepEqual(err, tc.expectErr) { + if err.Error() != tc.expectErr.Error() { t.Errorf("test %d: error mismatch, want %v, get %v", i, tc.expectErr, err) } continue diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index 8bd8b0614c..66c703a687 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -22,10 +22,15 @@ import ( "errors" "fmt" "math/big" + "net" + "net/http" "reflect" "testing" "time" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/internal/ethapi" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/ethash" @@ -210,7 +215,62 @@ var testTx2 = types.MustSignNewTx(testKey, types.LatestSigner(genesis.Config), & To: &common.Address{2}, }) +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 == 100 { + 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 == 100 { + 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) (*node.Node, []*types.Block) { + histAddr := newMockHistoricalBackend(t) + // Generate test chain. blocks := generateTestChain() @@ -222,6 +282,7 @@ func newTestBackend(t *testing.T) (*node.Node, []*types.Block) { // Create Ethereum Service config := ðconfig.Config{Genesis: genesis} config.Ethash.PowMode = ethash.ModeFake + config.RollupHistoricalRPC = histAddr ethservice, err := eth.New(n, config) if err != nil { t.Fatalf("can't create new ethereum service: %v", err) @@ -289,6 +350,9 @@ func TestEthClient(t *testing.T) { "TransactionSender": { func(t *testing.T) { testTransactionSender(t, client) }, }, + "EstimateGas": { + func(t *testing.T) { testEstimateGas(t, client) }, + }, } t.Parallel() @@ -580,6 +644,14 @@ func testCallContract(t *testing.T, client *rpc.Client) { if _, err := ec.PendingCallContract(context.Background(), msg); err != nil { t.Fatalf("unexpected error: %v", err) } + // Historical + histVal, err := ec.CallContract(context.Background(), msg, big.NewInt(100)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(histVal) != "test" { + t.Fatalf("expected %s to equal test", string(histVal)) + } } func testAtFunctions(t *testing.T, client *rpc.Client) { @@ -687,6 +759,35 @@ func testTransactionSender(t *testing.T, client *rpc.Client) { } } +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) + } + + // historical case + var res hexutil.Uint64 + err = client.CallContext(context.Background(), &res, "eth_estimateGas", toCallArg(msg), rpc.BlockNumberOrHashWithNumber(100)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res != 12345 { + t.Fatalf("invalid result: %d", res) + } +} + func sendTransaction(ec *Client) error { chainID, err := ec.ChainID(context.Background()) if err != nil { diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 58f65f86d7..25aff817b9 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -26,6 +26,9 @@ import ( "time" "github.com/davecgh/go-spew/spew" + "github.com/tyler-smith/go-bip39" + + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -46,9 +49,10 @@ import ( "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" - "github.com/tyler-smith/go-bip39" ) +var ErrHeaderNotFound = fmt.Errorf("header %w", ethereum.NotFound) + // EthereumAPI provides an API to access Ethereum related information. type EthereumAPI struct { b Backend @@ -1048,7 +1052,11 @@ func (e *revertError) ErrorData() interface{} { // useful to execute and retrieve values. func (s *BlockChainAPI) Call(ctx context.Context, args TransactionArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) { result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, s.b.RPCEVMTimeout(), s.b.RPCGasCap()) - if err != nil { + if errors.Is(err, ethereum.NotFound) && s.b.HistoricalRPCService() != nil { + var histResult hexutil.Bytes + err = s.b.HistoricalRPCService().CallContext(ctx, &histResult, "eth_call", args, blockNrOrHash, overrides) + return histResult, err + } else if err != nil { return nil, err } // If the result contains a revert reason, try to unpack and return it. @@ -1185,7 +1193,17 @@ func (s *BlockChainAPI) EstimateGas(ctx context.Context, args TransactionArgs, b if blockNrOrHash != nil { bNrOrHash = *blockNrOrHash } - return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) + + res, err := DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) + if errors.Is(err, ethereum.NotFound) && s.b.HistoricalRPCService() != nil { + var result hexutil.Uint64 + err := s.b.HistoricalRPCService().CallContext(ctx, &result, "eth_estimateGas", args, blockNrOrHash) + return result, err + } else if err != nil { + return 0, err + } + + return res, err } // RPCMarshalHeader converts the given header to the RPC output . @@ -1301,6 +1319,11 @@ type RPCTransaction struct { V *hexutil.Big `json:"v"` R *hexutil.Big `json:"r"` S *hexutil.Big `json:"s"` + + // deposit-tx only + SourceHash *common.Hash `json:"sourceHash,omitempty"` + Mint *hexutil.Big `json:"mint,omitempty"` + IsSystemTx *bool `json:"isSystemTx,omitempty"` } // newRPCTransaction returns a transaction that will serialize to the RPC @@ -1308,6 +1331,28 @@ type RPCTransaction struct { func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber uint64, index uint64, baseFee *big.Int, config *params.ChainConfig) *RPCTransaction { signer := types.MakeSigner(config, new(big.Int).SetUint64(blockNumber)) from, _ := types.Sender(signer, tx) + if tx.Type() == types.DepositTxType { + srcHash := tx.SourceHash() + isSystemTx := tx.IsSystemTx() + result := &RPCTransaction{ + Type: hexutil.Uint64(tx.Type()), + From: from, + Gas: hexutil.Uint64(tx.Gas()), + Hash: tx.Hash(), + Input: hexutil.Bytes(tx.Data()), + To: tx.To(), + Value: (*hexutil.Big)(tx.Value()), + Mint: (*hexutil.Big)(tx.Mint()), + SourceHash: &srcHash, + IsSystemTx: &isSystemTx, + } + if blockHash != (common.Hash{}) { + result.BlockHash = &blockHash + result.BlockNumber = (*hexutil.Big)(new(big.Int).SetUint64(blockNumber)) + result.TransactionIndex = (*hexutil.Uint64)(&index) + } + return result + } v, r, s := tx.RawSignatureValues() result := &RPCTransaction{ Type: hexutil.Uint64(tx.Type()), @@ -1649,6 +1694,12 @@ func (s *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common. "logsBloom": receipt.Bloom, "type": hexutil.Uint(tx.Type()), } + if s.b.ChainConfig().Optimism != nil && !tx.IsDepositTx() { + fields["l1GasPrice"] = (*hexutil.Big)(receipt.L1GasPrice) + fields["l1GasUsed"] = (*hexutil.Big)(receipt.L1GasUsed) + fields["l1Fee"] = (*hexutil.Big)(receipt.L1Fee) + fields["l1FeeScalar"] = receipt.FeeScalar.String() + } // Assign the effective gas price paid if !s.b.ChainConfig().IsLondon(bigblock) { fields["effectiveGasPrice"] = hexutil.Uint64(tx.GasPrice().Uint64()) diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 0c1763472f..3a58ec5f6c 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -86,6 +86,8 @@ type Backend interface { 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 a/internal/ethapi/transaction_args_test.go b/internal/ethapi/transaction_args_test.go index a8f2d52148..48f38e2429 100644 --- a/internal/ethapi/transaction_args_test.go +++ b/internal/ethapi/transaction_args_test.go @@ -342,4 +342,6 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) 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 } diff --git a/internal/flags/categories.go b/internal/flags/categories.go index c2db6c6c1d..f2b3687758 100644 --- a/internal/flags/categories.go +++ b/internal/flags/categories.go @@ -31,6 +31,7 @@ const ( MinerCategory = "MINER" GasPriceCategory = "GAS PRICE ORACLE" VMCategory = "VIRTUAL MACHINE" + RollupCategory = "ROLLUP NODE" LoggingCategory = "LOGGING AND DEBUGGING" MetricsCategory = "METRICS AND STATS" MiscCategory = "MISC" diff --git a/les/api_backend.go b/les/api_backend.go index 422ac74b86..b292f40114 100644 --- a/les/api_backend.go +++ b/les/api_backend.go @@ -190,6 +190,7 @@ func (b *LesApiBackend) GetEVM(ctx context.Context, msg core.Message, state *sta } txContext := core.NewEVMTxContext(msg) context := core.NewEVMBlockContext(header, b.eth.blockchain, nil) + context.L1CostFunc = types.NewL1CostFunc(b.eth.chainConfig, state) return vm.NewEVM(context, txContext, state, b.eth.chainConfig, *vmConfig), state.Error, nil } @@ -333,3 +334,11 @@ func (b *LesApiBackend) StateAtBlock(ctx context.Context, block *types.Block, re func (b *LesApiBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (core.Message, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) } + +func (b *LesApiBackend) HistoricalRPCService() *rpc.Client { + return b.eth.historicalRPCService +} + +func (b *LesApiBackend) Genesis() *types.Block { + return b.eth.blockchain.Genesis() +} diff --git a/les/client.go b/les/client.go index 9ac85ecdac..d72e61aef6 100644 --- a/les/client.go +++ b/les/client.go @@ -18,6 +18,7 @@ package les import ( + "context" "fmt" "strings" "time" @@ -67,6 +68,9 @@ type LightEthereum struct { pruner *pruner merger *consensus.Merger + seqRPCService *rpc.Client + historicalRPCService *rpc.Client + bloomRequests chan chan *bloombits.Retrieval // Channel receiving bloom data retrieval requests bloomIndexer *core.ChainIndexer // Bloom indexer operating during block imports @@ -197,6 +201,26 @@ func New(stack *node.Node, config *ethconfig.Config) (*LightEthereum, error) { leth.blockchain.DisableCheckFreq() } + 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 + } + leth.seqRPCService = client + } + + if config.RollupHistoricalRPC != "" { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + client, err := rpc.DialContext(ctx, config.RollupHistoricalRPC) + cancel() + if err != nil { + return nil, err + } + leth.historicalRPCService = client + } + leth.netRPCService = ethapi.NewNetAPI(leth.p2pServer, leth.config.NetworkId) // Register the backend on the node diff --git a/les/state_accessor.go b/les/state_accessor.go index 091ec8871e..841b6b7553 100644 --- a/les/state_accessor.go +++ b/les/state_accessor.go @@ -67,6 +67,7 @@ func (leth *LightEthereum) stateAtTransaction(ctx context.Context, block *types. if idx == txIndex { return msg, context, statedb, release, nil } + context.L1CostFunc = types.NewL1CostFunc(leth.blockchain.Config(), statedb) // Not yet the searched for transaction, execute on top of the current state vmenv := vm.NewEVM(context, txContext, statedb, leth.blockchain.Config(), vm.Config{}) if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { diff --git a/miner/miner.go b/miner/miner.go index c969aec735..9578c88423 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/core/txpool" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -43,6 +44,10 @@ type Backend interface { TxPool() *txpool.TxPool } +type BackendWithHistoricalState interface { + StateAtBlock(block *types.Block, reexec uint64, base *state.StateDB, readOnly bool, preferDisk bool) (statedb *state.StateDB, release tracers.StateReleaseFunc, err error) +} + // Config is the configuration parameters of mining. type Config struct { Etherbase common.Address `toml:",omitempty"` // Public address for block mining rewards diff --git a/miner/payload_building.go b/miner/payload_building.go index f84d908e86..26e89eef31 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -40,6 +40,10 @@ type BuildPayloadArgs struct { FeeRecipient common.Address // The provided recipient address for collecting transaction fee Random common.Hash // The provided randomness value Withdrawals types.Withdrawals // The provided withdrawals + + 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 } // Id computes an 8-byte identifier by hashing the components of the payload arguments. @@ -51,6 +55,19 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID { hasher.Write(args.Random[:]) hasher.Write(args.FeeRecipient[:]) rlp.Encode(hasher, args.Withdrawals) + + 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) + } + var out engine.PayloadID copy(out[:], hasher.Sum(nil)[:8]) return out @@ -156,12 +173,15 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*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. - empty, _, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, true) + empty, _, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, true, args.Transactions, args.GasLimit) if err != nil { return nil, err } // Construct a payload object for return. payload := newPayload(empty, args.Id()) + if args.NoTxPool { // don't start the background payload updating job if there is no tx pool to pull from + return payload, nil + } // Spin up a routine for updating the payload in background. This strategy // can maximum the revenue for including transactions with highest fee. @@ -180,7 +200,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { select { case <-timer.C: start := time.Now() - block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, false) + block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, false, args.Transactions, args.GasLimit) if err == nil { payload.update(block, fees, time.Since(start)) } diff --git a/miner/worker.go b/miner/worker.go index 49204f71a0..d944fbc855 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -31,6 +31,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth/tracers" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" @@ -794,6 +795,14 @@ func (w *worker) makeEnv(parent *types.Block, header *types.Header, coinbase com // Retrieve the parent state to execute on top and start a prefetcher for // the miner to speed block sealing up a bit. state, err := w.chain.StateAt(parent.Root()) + if err != nil && w.chainConfig.Optimism != nil { // Allow the miner to reorg its own chain arbitrarily deep + if historicalBackend, ok := w.eth.(BackendWithHistoricalState); ok { + var release tracers.StateReleaseFunc + state, release, err = historicalBackend.StateAtBlock(parent, ^uint64(0), nil, false, false) + state = state.Copy() + release() + } + } if err != nil { return nil, err } @@ -976,6 +985,9 @@ type generateParams struct { withdrawals types.Withdrawals // List of withdrawals to include in block. noUncle bool // Flag whether the uncle block inclusion is allowed 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 } // prepareWork constructs the sealing task according to the given parameters, @@ -1026,6 +1038,9 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { header.GasLimit = core.CalcGasLimit(parentGasLimit, w.config.GasCeil) } } + if genParams.gasLimit != nil { // override gas limit if specified + header.GasLimit = *genParams.gasLimit + } // Run the consensus preparation with the default or customized consensus engine. if err := w.engine.Prepare(w.chain, header); err != nil { log.Error("Failed to prepare header for sealing", "err", err) @@ -1090,14 +1105,28 @@ func (w *worker) fillTransactions(interrupt *int32, env *environment) error { } // generateWork generates a sealing block based on the given parameters. -func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, error) { - work, err := w.prepareWork(params) +func (w *worker) generateWork(genParams *generateParams) (*types.Block, *big.Int, error) { + work, err := w.prepareWork(genParams) if err != nil { return nil, nil, err } defer work.discard() + if work.gasPool == nil { + work.gasPool = new(core.GasPool).AddGas(work.header.GasLimit) + } + + for _, tx := range genParams.txs { + from, _ := types.Sender(work.signer, tx) + work.state.SetTxContext(tx.Hash(), work.tcount) + _, err := w.commitTransaction(work, tx) + if err != nil { + return nil, nil, fmt.Errorf("failed to force-include tx: %s type: %d sender: %s nonce: %d, err: %w", tx.Hash(), tx.Type(), from, tx.Nonce(), err) + } + work.tcount++ + } - if !params.noTxs { + // forced transactions done, fill rest of block with transactions + if !genParams.noTxs { interrupt := new(int32) timer := time.AfterFunc(w.newpayloadTimeout, func() { atomic.StoreInt32(interrupt, commitInterruptTimeout) @@ -1109,7 +1138,7 @@ func (w *worker) generateWork(params *generateParams) (*types.Block, *big.Int, e log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) } } - block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, work.unclelist(), work.receipts, params.withdrawals) + block, err := w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, work.txs, work.unclelist(), work.receipts, genParams.withdrawals) if err != nil { return nil, nil, err } @@ -1226,7 +1255,7 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti // getSealingBlock generates the sealing block based on the given parameters. // The generation result will be passed back via the given channel no matter // the generation itself succeeds or not. -func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, random common.Hash, withdrawals types.Withdrawals, noTxs bool) (*types.Block, *big.Int, error) { +func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, random common.Hash, withdrawals types.Withdrawals, noTxs bool, transactions types.Transactions, gasLimit *uint64) (*types.Block, *big.Int, error) { req := &getWorkReq{ params: &generateParams{ timestamp: timestamp, @@ -1237,6 +1266,8 @@ func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase withdrawals: withdrawals, noUncle: true, noTxs: noTxs, + txs: transactions, + gasLimit: gasLimit, }, result: make(chan *newPayloadResult, 1), } diff --git a/miner/worker_test.go b/miner/worker_test.go index ba929d293d..724563515f 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -635,7 +635,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co // This API should work even when the automatic sealing is not enabled for _, c := range cases { - block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, c.random, nil, false) + block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, c.random, nil, false, nil, nil) if c.expectErr { if err == nil { t.Error("Expect error but get nil") @@ -651,7 +651,7 @@ func testGetSealingWork(t *testing.T, chainConfig *params.ChainConfig, engine co // This API should work even when the automatic sealing is enabled w.start() for _, c := range cases { - block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, c.random, nil, false) + block, _, err := w.getSealingBlock(c.parent, timestamp, c.coinbase, c.random, nil, false, nil, nil) if c.expectErr { if err == nil { t.Error("Expect error but get nil") diff --git a/params/config.go b/params/config.go index 816577a547..b65ef02032 100644 --- a/params/config.go +++ b/params/config.go @@ -21,8 +21,9 @@ import ( "fmt" "math/big" - "github.com/ethereum/go-ethereum/common" "golang.org/x/crypto/sha3" + + "github.com/ethereum/go-ethereum/common" ) // Genesis hashes to enforce below configs on. @@ -448,6 +449,9 @@ type ChainConfig struct { // 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. @@ -469,6 +473,17 @@ func (c *CliqueConfig) String() string { return "clique" } +// OptimismConfig is the optimism config. +type OptimismConfig struct { + EIP1559Elasticity uint64 `json:"eip1559Elasticity"` + EIP1559Denominator uint64 `json:"eip1559Denominator"` +} + +// 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 @@ -496,6 +511,8 @@ func (c *ChainConfig) Description() string { } else { banner += "Consensus: Beacon (proof-of-stake), merged from Clique (proof-of-authority)\n" } + case c.Optimism != nil: + banner += "Consensus: Optimism\n" default: banner += "Consensus: unknown\n" } @@ -815,11 +832,17 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, // BaseFeeChangeDenominator bounds the amount the base fee can change between blocks. func (c *ChainConfig) BaseFeeChangeDenominator() uint64 { + if c.Optimism != nil { + 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 } diff --git a/params/protocol_params.go b/params/protocol_params.go index bb703d0b74..e5a5b7f669 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -16,7 +16,18 @@ package params -import "math/big" +import ( + "math/big" + + "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. diff --git a/params/version.go b/params/version.go index 8bbf0b99be..771d0482f1 100644 --- a/params/version.go +++ b/params/version.go @@ -20,6 +20,7 @@ import ( "fmt" ) +// Version is the version of upstream geth const ( VersionMajor = 1 // Major version component of the current release VersionMinor = 11 // Minor version component of the current release @@ -27,14 +28,36 @@ const ( VersionMeta = "stable" // Version metadata to append to the version string ) +// OPVersion is the version of op-geth +const ( + 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 = "unstable" // Version metadata to append to the version string +) + // Version holds the textual version string. var Version = func() string { - return fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionPatch) + 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 != "" { + v += "-" + OPVersionMeta + } + return v +}() + +// 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 +69,8 @@ var VersionWithMeta = func() string { // 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 +83,7 @@ func VersionWithCommit(gitCommit, gitDate string) string { if len(gitCommit) >= 8 { vsn += "-" + gitCommit[:8] } - if (VersionMeta != "stable") && (gitDate != "") { + if (OPVersionMeta != "stable") && (gitDate != "") { vsn += "-" + gitDate } return vsn