op-geth

diff: ignored:
+620
-55
+65
-0

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

When charging gas, the EVM is extended to first charge from the Soul Gas Token (which is a soul bounded ERC20) balance; Then charge from the native balance if it’s not enough.

The txpool and gas estimator are also extended to use the sum of native balance and Soul Gas Token balance as the effective balance.

In order to fix a compatibility issue with wallets, the node is also extended to listen to another http port alongside the default one, the only different is that the eth_GetBalance API will return the sum of native balance and Soul Gas Token balance.

diff --git official op-geth/cmd/geth/config.go SWC op-geth/cmd/geth/config.go index 837b38777b76d2890ffd7d157d7d7f63d8018b89..3ceb7ad369034bd071df570ee7fb250f40b09798 100644 --- official op-geth/cmd/geth/config.go +++ SWC op-geth/cmd/geth/config.go @@ -237,6 +237,7 @@ // Start metrics export if enabled utils.SetupMetrics(&cfg.Metrics)   backend, eth := utils.RegisterEthService(stack, &cfg.Eth) + stack.APIBackend = backend   // Create gauge with geth system and build information if eth != nil { // The 'eth' backend may be nil in light mode
diff --git official op-geth/cmd/geth/main.go SWC op-geth/cmd/geth/main.go index 0f1974901648d77ecf2dffbc0b37965ed2e7d844..11ce2d05956ed1e3480d064f7db2d6e3226276bc 100644 --- official op-geth/cmd/geth/main.go +++ SWC op-geth/cmd/geth/main.go @@ -182,8 +182,11 @@ }, utils.NetworkFlags, utils.DatabaseFlags)   rpcFlags = []cli.Flag{ utils.HTTPEnabledFlag, + utils.HTTPSGTEnabledFlag, utils.HTTPListenAddrFlag, + utils.HTTPSGTListenAddrFlag, utils.HTTPPortFlag, + utils.HTTPSGTPortFlag, utils.HTTPCORSDomainFlag, utils.AuthListenFlag, utils.AuthPortFlag,
diff --git official op-geth/cmd/utils/flags.go SWC op-geth/cmd/utils/flags.go index c3be967fc0f8788d0d0ab9080a0fb4a6b4ff282e..f66b0b9ae197b22f456f1eb990f903a0dabfc7db 100644 --- official op-geth/cmd/utils/flags.go +++ SWC op-geth/cmd/utils/flags.go @@ -668,16 +668,33 @@ Name: "http", Usage: "Enable the HTTP-RPC server", Category: flags.APICategory, } + HTTPSGTEnabledFlag = &cli.BoolFlag{ + Name: "httpsgt", + Usage: "Enable the HTTP-RPC server for Soul Gas Token", + Category: flags.APICategory, + } HTTPListenAddrFlag = &cli.StringFlag{ Name: "http.addr", Usage: "HTTP-RPC server listening interface", Value: node.DefaultHTTPHost, Category: flags.APICategory, } + HTTPSGTListenAddrFlag = &cli.StringFlag{ + Name: "httpsgt.addr", + Usage: "HTTP-RPC server listening interface for Soul Gas Token", + Value: node.DefaultHTTPSGTHost, + Category: flags.APICategory, + } HTTPPortFlag = &cli.IntFlag{ Name: "http.port", Usage: "HTTP-RPC server listening port", Value: node.DefaultHTTPPort, + Category: flags.APICategory, + } + HTTPSGTPortFlag = &cli.IntFlag{ + Name: "httpsgt.port", + Usage: "HTTP-RPC server listening port for Soul Gas Token", + Value: node.DefaultHTTPSGTPort, Category: flags.APICategory, } HTTPCORSDomainFlag = &cli.StringFlag{ @@ -1199,6 +1216,8 @@ urls = params.V5OPBootnodes } else { urls = params.V5OPTestnetBootnodes } + case ctx.Uint64(NetworkIdFlag.Name) == 3335: + urls = params.BetaTestnetBootnodes }   cfg.BootstrapNodesV5 = make([]*enode.Node, 0, len(urls)) @@ -1259,9 +1278,19 @@ if ctx.IsSet(HTTPListenAddrFlag.Name) { cfg.HTTPHost = ctx.String(HTTPListenAddrFlag.Name) } } + if ctx.Bool(HTTPSGTEnabledFlag.Name) { + if ctx.IsSet(HTTPSGTListenAddrFlag.Name) { + cfg.HTTPSGTHost = ctx.String(HTTPSGTListenAddrFlag.Name) + } else if cfg.HTTPSGTHost == "" { + cfg.HTTPSGTHost = "127.0.0.1" + } + }   if ctx.IsSet(HTTPPortFlag.Name) { cfg.HTTPPort = ctx.Int(HTTPPortFlag.Name) + } + if ctx.IsSet(HTTPSGTPortFlag.Name) { + cfg.HTTPSGTPort = ctx.Int(HTTPSGTPortFlag.Name) }   if ctx.IsSet(AuthListenFlag.Name) {
diff --git official op-geth/core/state_transition.go SWC op-geth/core/state_transition.go index cfa818a4ac4d3331c20f175bcb44e64978a143f7..4eeadb618edc815fd1878a038d1532e055a582ed 100644 --- official op-geth/core/state_transition.go +++ SWC op-geth/core/state_transition.go @@ -22,10 +22,12 @@ "fmt" "math" "math/big"   + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/params" "github.com/holiman/uint256" @@ -247,6 +249,25 @@ gasRemaining uint64 initialGas uint64 state vm.StateDB evm *vm.EVM + + // nil means SGT is not used at all + usedSGTBalance *uint256.Int + // should not be used if usedSGTBalance is nil; + // only set when: 1. usedSGTBalance is non-nil 2. used native balance is gt 0 + usedNativeBalance *uint256.Int + + // these are set once for checking gas formula only + boughtGas *uint256.Int + boughtGasLimitXGasPrice *uint256.Int + boughtBlobFee *uint256.Int + boughtL1Fee *uint256.Int + boughtOperatorFee *uint256.Int + refundedGas *uint256.Int + refundedOperatorFee *uint256.Int + tipFee *uint256.Int + baseFee *uint256.Int + l1Fee *uint256.Int + operatorFee *uint256.Int }   // newStateTransition initialises and returns a new state transition object. @@ -259,6 +280,90 @@ state: evm.StateDB, } }   +func (st *stateTransition) checkGasFormula() error { + if st.boughtGas.Cmp( + new(uint256.Int).Add( + new(uint256.Int).Add(st.boughtL1Fee, st.boughtOperatorFee), + new(uint256.Int).Add(st.boughtGasLimitXGasPrice, st.boughtBlobFee))) != 0 { + return fmt.Errorf("gas formula doesn't hold: boughtGas(%v) != boughtGasLimitXGasPrice(%v) + boughtBlobFee(%v) + boughtL1Fee(%v) + boughtOperatorFee(%v)", st.boughtGas, st.boughtGasLimitXGasPrice, st.boughtBlobFee, st.boughtL1Fee, st.boughtOperatorFee) + } + sumGas := new(uint256.Int).Add( + st.refundedGas, new(uint256.Int).Add( + st.tipFee, new(uint256.Int).Add( + st.baseFee, + new(uint256.Int).Add( + new(uint256.Int).Add( + st.l1Fee, st.operatorFee), + st.refundedOperatorFee)))) + if st.boughtGas.Cmp(sumGas) != 0 { + return fmt.Errorf("gas formula doesn't hold: boughtGas(%v) != sumGas(%v) [boughtGas = boughtGasLimitXGasPrice(%v) + boughtBlobFee(%v) + boughtL1Fee(%v) + boughtOperatorFee(%v),sumGas = refundedGas(%v) + refundedOperatorFee(%v) + tipFee(%v) + baseFee(%v) + l1Fee(%v) + operatorFee(%v)], BaseFee:%v BlobBaseFee:%v GasLimit:%v GasUsed:%v GasPrice:%v GasFeeCap:%v GasTipCap:%v", sumGas, st.boughtGas, st.boughtGasLimitXGasPrice, st.boughtBlobFee, st.boughtL1Fee, st.boughtOperatorFee, st.refundedGas, st.refundedOperatorFee, st.tipFee, st.baseFee, st.l1Fee, st.operatorFee, st.evm.Context.BaseFee, st.evm.Context.BlobBaseFee, st.initialGas, st.gasUsed(), st.msg.GasPrice, st.msg.GasFeeCap, st.msg.GasTipCap) + } + return nil +} + +func (st *stateTransition) collectableNativeBalance(amount *uint256.Int) *uint256.Int { + // we burn the token if gas is from SoulGasToken which is not backed by native + if st.usedSGTBalance != nil && st.evm.ChainConfig().IsOptimism() && !st.evm.ChainConfig().Optimism.IsSoulBackedByNative { + _, amount = st.distributeGas(amount, st.usedSGTBalance, st.usedNativeBalance) + } + return amount +} + +// distributeGas distributes the gas according to the priority: +// +// first pool1, then pool2. +// +// In more detail: +// split amount among two pools, first pool1, then pool2, where poolx means max amount for pool x. +// quotax is the amount distributed to pool x. +// +// note: +// 1. the returned values are always non-nil. +// 2. pool1 and pool2 are updated in-place if they are non-nil. +func (st *stateTransition) distributeGas(amount, pool1, pool2 *uint256.Int) (quota1, quota2 *uint256.Int) { + if amount == nil { + panic("amount should not be nil") + } + if st.usedSGTBalance == nil { + panic("should not happen when usedSGTBalance is nil") + } + if pool1 == nil { + // pool1 empty, all to pool2 + quota1 = new(uint256.Int) + quota2 = amount.Clone() + + pool2.Sub(pool2, quota2) + return + } + if pool2 == nil { + // pool2 empty, all to pool1 + quota1 = amount.Clone() + quota2 = new(uint256.Int) + + pool1.Sub(pool1, quota1) + return + } + + // from here, both pool1 and pool2 are non-nil + + if amount.Cmp(pool1) >= 0 { + // partial pool1, remaining to pool2 + quota1 = pool1.Clone() + quota2 = new(uint256.Int).Sub(amount, quota1) + + pool1.Clear() + pool2.Sub(pool2, quota2) + } else { + // all to pool1 + quota1 = amount.Clone() + quota2 = new(uint256.Int) + + pool1.Sub(pool1, quota1) + } + + return +} + // to returns the recipient of the message. func (st *stateTransition) to() common.Address { if st.msg == nil || st.msg.To == nil /* contract creation */ { @@ -267,20 +372,113 @@ } return *st.msg.To }   +const ( + // should keep it in sync with the balances field of SoulGasToken contract + BalancesSlot = uint64(51) +) + +var ( + slotArgs abi.Arguments +) + +func init() { + uint64Ty, _ := abi.NewType("uint64", "", nil) + addressTy, _ := abi.NewType("address", "", nil) + slotArgs = abi.Arguments{{Name: "addr", Type: addressTy, Indexed: false}, {Name: "slot", Type: uint64Ty, Indexed: false}} +} + +func TargetSGTBalanceSlot(account common.Address) (slot common.Hash) { + data, _ := slotArgs.Pack(account, BalancesSlot) + slot = crypto.Keccak256Hash(data) + return +} + +func (st *stateTransition) GetSoulBalance(account common.Address) *uint256.Int { + slot := TargetSGTBalanceSlot(account) + value := st.state.GetState(types.SoulGasTokenAddr, slot) + balance := new(uint256.Int) + balance.SetBytes(value[:]) + return balance +} + +// Get the effective balance to pay gas +func GetEffectiveGasBalance(state vm.StateDB, chainconfig *params.ChainConfig, account common.Address, value *big.Int, targetHeight uint64) (*big.Int, error) { + bal, sgtBal := GetGasBalancesInBig(state, chainconfig, account, targetHeight) + if value == nil { + value = big.NewInt(0) + } + if bal.Cmp(value) < 0 { + return nil, ErrInsufficientFundsForTransfer + } + bal.Sub(bal, value) + return new(big.Int).Add(bal, sgtBal), nil +} + +func GetGasBalances(state vm.StateDB, chainconfig *params.ChainConfig, account common.Address, targetHeight uint64) (*uint256.Int, *uint256.Int) { + balance := state.GetBalance(account).Clone() + if chainconfig != nil && chainconfig.IsOptimism() && chainconfig.Optimism.IsSoulGasToken(targetHeight) { + sgtBalanceSlot := TargetSGTBalanceSlot(account) + sgtBalanceValue := state.GetState(types.SoulGasTokenAddr, sgtBalanceSlot) + sgtBalance := new(uint256.Int).SetBytes(sgtBalanceValue[:]) + + return balance, sgtBalance + } + + return balance, uint256.NewInt(0) +} + +func GetGasBalancesInBig(state vm.StateDB, chainconfig *params.ChainConfig, account common.Address, targetHeight uint64) (*big.Int, *big.Int) { + bal, sgtBal := GetGasBalances(state, chainconfig, account, targetHeight) + return bal.ToBig(), sgtBal.ToBig() +} + +// called by buyGas +func (st *stateTransition) subSoulBalance(account common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) (err error) { + current := st.GetSoulBalance(account) + if current.Cmp(amount) < 0 { + return fmt.Errorf("soul balance not enough, current:%v, expect:%v", current, amount) + } + + value := current.Sub(current, amount).Bytes32() + st.state.SetState(types.SoulGasTokenAddr, TargetSGTBalanceSlot(account), value) + + if st.evm.ChainConfig().IsOptimism() && st.evm.ChainConfig().Optimism.IsSoulBackedByNative { + st.state.SubBalance(types.SoulGasTokenAddr, amount, reason) + } + + return +} + +// called by refundGas +func (st *stateTransition) addSoulBalance(account common.Address, amount *uint256.Int, reason tracing.BalanceChangeReason) { + current := st.GetSoulBalance(account) + value := current.Add(current, amount).Bytes32() + st.state.SetState(types.SoulGasTokenAddr, TargetSGTBalanceSlot(account), value) + + if st.evm.ChainConfig().IsOptimism() && st.evm.ChainConfig().Optimism.IsSoulBackedByNative { + st.state.AddBalance(types.SoulGasTokenAddr, amount, reason) + } +} + func (st *stateTransition) buyGas() error { mgval := new(big.Int).SetUint64(st.msg.GasLimit) mgval.Mul(mgval, st.msg.GasPrice) + st.boughtGasLimitXGasPrice, _ = uint256.FromBig(mgval) + st.boughtL1Fee = new(uint256.Int) + st.boughtOperatorFee = new(uint256.Int) var l1Cost *big.Int var operatorCost *uint256.Int if !st.msg.SkipNonceChecks && !st.msg.SkipFromEOACheck { if st.evm.Context.L1CostFunc != nil { l1Cost = st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time) if l1Cost != nil { + st.boughtL1Fee, _ = uint256.FromBig(l1Cost) mgval = mgval.Add(mgval, l1Cost) } } if st.evm.Context.OperatorCostFunc != nil { operatorCost = st.evm.Context.OperatorCostFunc(st.msg.GasLimit, st.evm.Context.Time) + st.boughtOperatorFee = operatorCost.Clone() mgval = mgval.Add(mgval, operatorCost.ToBig()) } } @@ -297,6 +495,7 @@ } } balanceCheck.Add(balanceCheck, st.msg.Value)   + st.boughtBlobFee = new(uint256.Int) if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { if blobGas := st.blobGasUsed(); blobGas > 0 { // Check that the user has enough funds to cover blobGasUsed * tx.BlobGasFeeCap @@ -306,16 +505,33 @@ balanceCheck.Add(balanceCheck, blobBalanceCheck) // Pay for blobGasUsed * actual blob fee blobFee := new(big.Int).SetUint64(blobGas) blobFee.Mul(blobFee, st.evm.Context.BlobBaseFee) + st.boughtBlobFee, _ = uint256.FromBig(blobFee) mgval.Add(mgval, blobFee) } } + balanceCheckU256, overflow := uint256.FromBig(balanceCheck) if overflow { return fmt.Errorf("%w: address %v required balance exceeds 256 bits", ErrInsufficientFunds, st.msg.From.Hex()) } - if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 { - return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) + + nativeBalance := st.state.GetBalance(st.msg.From) + var soulBalance *uint256.Int + if st.evm.ChainConfig().IsOptimism() && st.evm.ChainConfig().Optimism.IsSoulGasToken(st.evm.Context.BlockNumber.Uint64()) { + if have, want := nativeBalance.ToBig(), st.msg.Value; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have native balance %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) + } + + soulBalance = st.GetSoulBalance(st.msg.From) + if have, want := new(uint256.Int).Add(nativeBalance, soulBalance), balanceCheckU256; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have total balance %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) + } + } else { + if have, want := st.state.GetBalance(st.msg.From), balanceCheckU256; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, st.msg.From.Hex(), have, want) + } } + if err := st.gp.SubGas(st.msg.GasLimit); err != nil { return err } @@ -326,8 +542,34 @@ } st.gasRemaining = st.msg.GasLimit   st.initialGas = st.msg.GasLimit + mgvalU256, _ := uint256.FromBig(mgval) - st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) + st.boughtGas = mgvalU256.Clone() + if soulBalance == nil { + st.state.SubBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) + } else { + if mgvalU256.Cmp(soulBalance) <= 0 { + err := st.subSoulBalance(st.msg.From, mgvalU256, tracing.BalanceDecreaseGasBuy) + if err != nil { + return err + } + st.usedSGTBalance = mgvalU256 + } else { + err := st.subSoulBalance(st.msg.From, soulBalance, tracing.BalanceDecreaseGasBuy) + if err != nil { + return err + } + st.usedSGTBalance = soulBalance + // when both SGT and native balance are used, we record both amounts for refund. + // the priority for refund is: first native, then SGT + usedNativeBalance := new(uint256.Int).Sub(mgvalU256, soulBalance) + if usedNativeBalance.Sign() > 0 { + st.state.SubBalance(st.msg.From, usedNativeBalance, tracing.BalanceDecreaseGasBuy) + st.usedNativeBalance = usedNativeBalance + } + } + } + return nil }   @@ -655,8 +897,13 @@ // Skip fee payment when NoBaseFee is set and the fee fields // are 0. This avoids a negative effectiveTip being applied to // the coinbase when simulating calls. } else { + shouldCheckGasFormula := !st.msg.SkipNonceChecks && !st.msg.SkipFromEOACheck fee := new(uint256.Int).SetUint64(st.gasUsed()) fee.Mul(fee, effectiveTipU256) + + st.tipFee = fee.Clone() + + fee = st.collectableNativeBalance(fee) st.state.AddBalance(st.evm.Context.Coinbase, fee, tracing.BalanceIncreaseRewardTransactionFee)   // add the coinbase to the witness iff the fee is greater than 0 @@ -668,26 +915,59 @@ // Check that we are post bedrock to enable op-geth to be able to create pseudo pre-bedrock blocks (these are pre-bedrock, but don't follow l2 geth rules) // Note optimismConfig will not be nil if rules.IsOptimismBedrock is true if optimismConfig := st.evm.ChainConfig().Optimism; optimismConfig != nil && rules.IsOptimismBedrock && !st.msg.IsDepositTx { gasCost := new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), st.evm.Context.BaseFee) + + if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) { + gasCost.Add(gasCost, new(big.Int).Mul(new(big.Int).SetUint64(st.blobGasUsed()), st.evm.Context.BlobBaseFee)) + } + amtU256, overflow := uint256.FromBig(gasCost) if overflow { return nil, fmt.Errorf("optimism gas cost overflows U256: %d", gasCost) } + if shouldCheckGasFormula { + st.baseFee = amtU256.Clone() + } + + amtU256 = st.collectableNativeBalance(amtU256) st.state.AddBalance(params.OptimismBaseFeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) if l1Cost := st.evm.Context.L1CostFunc(st.msg.RollupCostData, st.evm.Context.Time); l1Cost != nil { amtU256, overflow = uint256.FromBig(l1Cost) if overflow { return nil, fmt.Errorf("optimism l1 cost overflows U256: %d", l1Cost) } + if shouldCheckGasFormula { + st.l1Fee = amtU256.Clone() + } + + amtU256 = st.collectableNativeBalance(amtU256) st.state.AddBalance(params.OptimismL1FeeRecipient, amtU256, tracing.BalanceIncreaseRewardTransactionFee) } + if rules.IsOptimismIsthmus { // Operator Fee refunds are only applied if Isthmus is active and the transaction is *not* a deposit. st.refundIsthmusOperatorCost()   operatorFeeCost := st.evm.Context.OperatorCostFunc(st.gasUsed(), st.evm.Context.Time) + st.operatorFee = operatorFeeCost.Clone() st.state.AddBalance(params.OptimismOperatorFeeRecipient, operatorFeeCost, tracing.BalanceIncreaseRewardTransactionFee) } + + if shouldCheckGasFormula { + if st.l1Fee == nil { + st.l1Fee = new(uint256.Int) + } + if st.operatorFee == nil { + st.operatorFee = new(uint256.Int) + } + if st.refundedOperatorFee == nil { + st.refundedOperatorFee = new(uint256.Int) + } + if err := st.checkGasFormula(); err != nil { + return nil, err + } + } } + }   return &ExecutionResult{ @@ -780,7 +1060,8 @@ // exchanged at the original rate. func (st *stateTransition) returnGas() { remaining := uint256.NewInt(st.gasRemaining) remaining.Mul(remaining, uint256.MustFromBig(st.msg.GasPrice)) - st.state.AddBalance(st.msg.From, remaining, tracing.BalanceIncreaseGasReturn) + st.refundedGas = remaining.Clone() + st.refundGas(remaining)   if st.evm.Config.Tracer != nil && st.evm.Config.Tracer.OnGasChange != nil && st.gasRemaining > 0 { st.evm.Config.Tracer.OnGasChange(st.gasRemaining, 0, tracing.GasChangeTxLeftOverReturned) @@ -791,6 +1072,23 @@ // available for the next transaction. st.gp.AddGas(st.gasRemaining) }   +// refundGas refunds a specified amount of gas to user. +// the priority for refund is: first native, then SGT. +// it's called by both returnGas and refundIsthmusOperatorCost. +func (st *stateTransition) refundGas(amount *uint256.Int) { + if st.usedSGTBalance == nil { + st.state.AddBalance(st.msg.From, amount, tracing.BalanceIncreaseGasReturn) + } else { + native, sgt := st.distributeGas(amount, st.usedNativeBalance, st.usedSGTBalance) + if native.Sign() > 0 { + st.state.AddBalance(st.msg.From, native, tracing.BalanceIncreaseGasReturn) + } + if sgt.Sign() > 0 { + st.addSoulBalance(st.msg.From, sgt, tracing.BalanceIncreaseGasReturn) + } + } +} + func (st *stateTransition) refundIsthmusOperatorCost() { // Return ETH to transaction sender for operator cost overcharge. operatorCostGasLimit := st.evm.Context.OperatorCostFunc(st.msg.GasLimit, st.evm.Context.Time) @@ -800,7 +1098,9 @@ if operatorCostGasUsed.Cmp(operatorCostGasLimit) > 0 { // Sanity check. panic(fmt.Sprintf("operator cost gas used (%d) > operator cost gas limit (%d)", operatorCostGasUsed, operatorCostGasLimit)) }   - st.state.AddBalance(st.msg.From, new(uint256.Int).Sub(operatorCostGasLimit, operatorCostGasUsed), tracing.BalanceIncreaseGasReturn) + refundedOperatorCost := new(uint256.Int).Sub(operatorCostGasLimit, operatorCostGasUsed) + st.refundedOperatorFee = refundedOperatorCost.Clone() + st.refundGas(refundedOperatorCost) }   // gasUsed returns the amount of gas used up by the state transition.
diff --git official op-geth/core/txpool/blobpool/blobpool.go SWC op-geth/core/txpool/blobpool/blobpool.go index 86b7298b17ceaeac25b234872393ed61cb1d4d4f..ccbd616e3c15085868ee47cc0d8570bf79a3ef95 100644 --- official op-geth/core/txpool/blobpool/blobpool.go +++ SWC op-geth/core/txpool/blobpool/blobpool.go @@ -680,10 +680,10 @@ break } // Ensure that there's no over-draft, this is expected to happen when some // transactions get included without publishing on the network - var ( - balance = p.state.GetBalance(addr) - spent = p.spent[addr] - ) + balance, sgtBalance := core.GetGasBalances(p.state, p.chain.Config(), addr, p.head.Number.Uint64()+1) + // TODO: we may need a better filter such as tx.value < acc.balance + balance = balance.Add(balance, sgtBalance) + spent := p.spent[addr] if spent.Cmp(balance) > 0 { // Evict the highest nonce transactions until the pending set falls under // the account's available balance @@ -1106,7 +1106,8 @@ return err } // Ensure the transaction adheres to the stateful pool filters (nonce, balance) stateOpts := &txpool.ValidationOptionsWithState{ - State: p.state, + State: p.state, + Chainconfig: p.chain.Config(),   FirstNonceGap: func(addr common.Address) uint64 { // Nonce gaps are not permitted in the blob pool, the first gap will @@ -1134,6 +1135,7 @@ return p.index[addr][int(nonce-next)].costCap.ToBig() } return nil }, + TargetHeight: p.head.Number.Uint64() + 1, } if err := txpool.ValidateTransactionWithState(tx, p.signer, stateOpts); err != nil { return err
diff --git official op-geth/core/txpool/legacypool/legacypool.go SWC op-geth/core/txpool/legacypool/legacypool.go index 7bb73e19f0ca4db67e8ed7c99fd77d5b52ce9d64..ec86e29b66531640c24dcac4d73a11422ea59d02 100644 --- official op-geth/core/txpool/legacypool/legacypool.go +++ SWC op-geth/core/txpool/legacypool/legacypool.go @@ -647,7 +647,8 @@ // 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 *LegacyPool) validateTx(tx *types.Transaction) error { opts := &txpool.ValidationOptionsWithState{ - State: pool.currentState, + State: pool.currentState, + Chainconfig: pool.chainconfig,   FirstNonceGap: nil, // Pool allows arbitrary arrival order, don't invalidate nonce gaps UsedAndLeftSlots: nil, // Pool has own mechanism to limit the number of transactions @@ -669,6 +670,7 @@ } return nil }, RollupCostFn: pool.rollupCostFn, + TargetHeight: pool.currentHead.Load().Number.Uint64() + 1, } if err := txpool.ValidateTransactionWithState(tx, pool.signer, opts); err != nil { return err @@ -1489,7 +1491,9 @@ for _, tx := range forwards { pool.all.Remove(tx.Hash()) } log.Trace("Removed old queued transactions", "count", len(forwards)) - balance := pool.currentState.GetBalance(addr) + balance, sgtBalance := core.GetGasBalances(pool.currentState, pool.chainconfig, addr, pool.currentHead.Load().Number.Uint64()+1) + // TODO: we may need a better filter such as tx.value < acc.balance + balance = balance.Add(balance, sgtBalance) // Drop all transactions that are too costly (low balance or out of gas) drops, _ := list.Filter(balance, gasLimit) for _, tx := range drops { @@ -1678,7 +1682,10 @@ hash := tx.Hash() pool.all.Remove(hash) log.Trace("Removed old pending transaction", "hash", hash) } - balance := pool.currentState.GetBalance(addr) + balance, sgtBalance := core.GetGasBalances(pool.currentState, pool.chainconfig, addr, pool.currentHead.Load().Number.Uint64()+1) + // TODO: we may need a better filter such as tx.value < acc.balance + balance = balance.Add(balance, sgtBalance) + // Drop all transactions that are too costly (low balance or out of gas), and queue any invalids back for later drops, invalids := list.Filter(balance, gasLimit) for _, tx := range drops {
diff --git official op-geth/core/txpool/validation.go SWC op-geth/core/txpool/validation.go index 901568563332271e9365e114d2d02d10b672358c..9e67e6fcad237326e07c30331e77df74b47d89df 100644 --- official op-geth/core/txpool/validation.go +++ SWC op-geth/core/txpool/validation.go @@ -87,7 +87,7 @@ // as the external engine-API user authenticates deposits. if tx.Type() == types.DepositTxType { return core.ErrTxTypeNotSupported } - if opts.Config.IsOptimism() && tx.Type() == types.BlobTxType { + if tx.Type() == types.BlobTxType && opts.Config.IsOptimism() && !opts.Config.IsL2Blob(head.Number, head.Time) { return core.ErrTxTypeNotSupported } // Ensure transactions not implemented by the calling pool are rejected @@ -228,7 +228,8 @@ // ValidationOptionsWithState define certain differences between stateful transaction // validation across the different pools without having to duplicate those checks. type ValidationOptionsWithState struct { - State *state.StateDB // State database to check nonces and balances against + State *state.StateDB // State database to check nonces and balances against + Chainconfig *params.ChainConfig   // FirstNonceGap is an optional callback to retrieve the first nonce gap in // the list of pooled transactions of a specific account. If this method is @@ -251,6 +252,8 @@ ExistingCost func(addr common.Address, nonce uint64) *big.Int   // RollupCostFn is an optional extension, to validate total rollup costs of a tx RollupCostFn RollupCostFunc + // TargetHeight is the height of the block the transaction is expected to be included in + TargetHeight uint64 }   // ValidateTransactionWithState is a helper method to check whether a transaction @@ -277,10 +280,13 @@ return fmt.Errorf("%w: tx nonce %v, gapped nonce %v", core.ErrNonceTooHigh, tx.Nonce(), gap) } } // Ensure the transactor has enough funds to cover the transaction costs - var ( - balance = opts.State.GetBalance(from).ToBig() - cost256, overflow = TotalTxCost(tx, opts.RollupCostFn) - ) + balance, err := core.GetEffectiveGasBalance(opts.State, opts.Chainconfig, from, tx.Value(), opts.TargetHeight) + if err != nil { + return fmt.Errorf("%w: balance %v, tx value %v", err, balance, tx.Value()) + } + // add tx.Value() back immediately to be compatible with ExistingExpenditure/ExistingCost below + balance = new(big.Int).Add(balance, tx.Value()) + cost256, overflow := TotalTxCost(tx, opts.RollupCostFn) if overflow { return fmt.Errorf("%w: total tx cost overflow", core.ErrInsufficientFunds) }
diff --git official op-geth/eth/gasestimator/gasestimator.go SWC op-geth/eth/gasestimator/gasestimator.go index 2c50dac924034663ed94049a16bf1c3bbeeb876c..723706a18157a9c26b470c94c3df4b6ecbaa730e 100644 --- official op-geth/eth/gasestimator/gasestimator.go +++ SWC op-geth/eth/gasestimator/gasestimator.go @@ -73,15 +73,12 @@ feeCap = common.Big0 } // Recap the highest gas limit with account's available balance. if feeCap.BitLen() != 0 { - balance := opts.State.GetBalance(call.From).ToBig() + balance, err := core.GetEffectiveGasBalance(opts.State, opts.Config, call.From, call.Value, opts.Header.Number.Uint64()) + if err != nil { + return 0, nil, err + }   available := balance - if call.Value != nil { - if call.Value.Cmp(available) >= 0 { - return 0, nil, core.ErrInsufficientFundsForTransfer - } - available.Sub(available, call.Value) - } if opts.Config.IsCancun(opts.Header.Number, opts.Header.Time) && len(call.BlobHashes) > 0 { blobGasPerBlob := new(big.Int).SetInt64(params.BlobTxBlobGasPerBlob) blobBalanceUsage := new(big.Int).SetInt64(int64(len(call.BlobHashes)))
diff --git official op-geth/internal/ethapi/api.go SWC op-geth/internal/ethapi/api.go index cc102e16f9efe6451bd82f7286b1e582d7449808..337057f1900a1fbc58d3813d86c4d44f2dcb62a6 100644 --- official op-geth/internal/ethapi/api.go +++ SWC op-geth/internal/ethapi/api.go @@ -296,12 +296,18 @@ }   // BlockChainAPI provides an API to access Ethereum blockchain data. type BlockChainAPI struct { - b Backend + b Backend + sgt bool }   // NewBlockChainAPI creates a new Ethereum blockchain API. func NewBlockChainAPI(b Backend) *BlockChainAPI { - return &BlockChainAPI{b} + return &BlockChainAPI{b, false} +} + +// NewBlockChainAPIForSGT creates a new Ethereum blockchain API for SGT. +func NewBlockChainAPIForSGT(b Backend) *BlockChainAPI { + return &BlockChainAPI{b, true} }   // ChainId is the EIP-155 replay-protection chain id for the current Ethereum chain config. @@ -346,8 +352,12 @@ state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) if state == nil || err != nil { return nil, err } - b := state.GetBalance(address).ToBig() - return (*hexutil.Big)(b), state.Error() + if !api.sgt { + b := state.GetBalance(address).ToBig() + return (*hexutil.Big)(b), state.Error() + } + nativeBalance, sgtBalance := core.GetGasBalancesInBig(state, api.b.ChainConfig(), address, header.Number.Uint64()) + return (*hexutil.Big)(new(big.Int).Add(nativeBalance, sgtBalance)), state.Error() }   // AccountResult structs for GetProof
diff --git official op-geth/node/config.go SWC op-geth/node/config.go index dc436876cce98bc047918630c2877bbbcbd80ecb..d81788455ed54f691c85bdcf98854d87824c967e 100644 --- official op-geth/node/config.go +++ SWC op-geth/node/config.go @@ -106,6 +106,9 @@ // HTTPHost is the host interface on which to start the HTTP RPC server. If this // field is empty, no HTTP API endpoint will be started. HTTPHost string   + HTTPSGTHost string + HTTPSGTPort int `toml:",omitempty"` + // HTTPPort is the TCP port number on which to start the HTTP RPC server. The // default zero value is/ valid and will pick a port number randomly (useful // for ephemeral nodes). @@ -269,7 +272,7 @@ }   // DefaultHTTPEndpoint returns the HTTP endpoint used by default. func DefaultHTTPEndpoint() string { - config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort, AuthPort: DefaultAuthPort} + config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort, HTTPSGTPort: DefaultHTTPSGTPort, AuthPort: DefaultAuthPort} return config.HTTPEndpoint() }
diff --git official op-geth/node/defaults.go SWC op-geth/node/defaults.go index 307d9e186a251b7963b5fbb4266249fbfbcc6fa4..880dc20d4fa8830334b018c8cbadab8dd41f784e 100644 --- official op-geth/node/defaults.go +++ SWC op-geth/node/defaults.go @@ -34,6 +34,9 @@ DefaultWSHost = "localhost" // Default host interface for the websocket RPC server DefaultWSPort = 8546 // Default TCP port for the websocket RPC server DefaultAuthHost = "localhost" // Default host interface for the authenticated apis DefaultAuthPort = 8551 // Default port for the authenticated apis + + DefaultHTTPSGTHost = "localhost" // Default host interface for the HTTPSGT RPC server + DefaultHTTPSGTPort = 8645 // Default TCP port for the HTTPSGT RPC server )   const ( @@ -56,6 +59,7 @@ // DefaultConfig contains reasonable default settings. var DefaultConfig = Config{ DataDir: DefaultDataDir(), HTTPPort: DefaultHTTPPort, + HTTPSGTPort: DefaultHTTPSGTPort, AuthAddr: DefaultAuthHost, AuthPort: DefaultAuthPort, AuthVirtualHosts: DefaultAuthVhosts,
diff --git official op-geth/node/node.go SWC op-geth/node/node.go index 24905c128ef316605fcfd058b0566954d1a29e9a..f914101eb2cc81f187152b3fae20c16607074d15 100644 --- official op-geth/node/node.go +++ SWC op-geth/node/node.go @@ -36,6 +36,7 @@ "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb/memorydb" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" @@ -60,6 +61,7 @@ lock sync.Mutex lifecycles []Lifecycle // All registered backends, services, and auxiliary services that have a lifecycle rpcAPIs []rpc.API // List of APIs currently provided by the node http *httpServer // + httpSGT *httpServer // ws *httpServer // httpAuth *httpServer // wsAuth *httpServer // @@ -67,6 +69,8 @@ ipc *ipcServer // Stores information about the ipc http server inprocHandler *rpc.Server // In-process RPC request handler to process the API requests   databases map[*closeTrackingDB]struct{} // All open databases + + APIBackend ethapi.Backend // Ethereum API backend, used for SGT }   const ( @@ -151,6 +155,7 @@ }   // Configure RPC servers. node.http = newHTTPServer(node.log, conf.HTTPTimeouts) + node.httpSGT = newHTTPServer(node.log, conf.HTTPTimeouts) node.httpAuth = newHTTPServer(node.log, conf.HTTPTimeouts) node.ws = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) node.wsAuth = newHTTPServer(node.log, rpc.DefaultHTTPTimeouts) @@ -411,6 +416,30 @@ } servers = append(servers, server) return nil } + initHttpSGT := func(server *httpServer) error { + if n.APIBackend == nil { + panic("bug: Node.APIBackend is nil when initHttpSGT is called") + } + if err := server.setListenAddr(n.config.HTTPSGTHost, n.config.HTTPSGTPort); err != nil { + return err + } + // appended API will override the existing one + updatedOpenAPIs := append(openAPIs, rpc.API{ + Namespace: "eth", + Service: ethapi.NewBlockChainAPIForSGT(n.APIBackend), + }) + if err := server.enableRPC(updatedOpenAPIs, httpConfig{ + CorsAllowedOrigins: n.config.HTTPCors, + Vhosts: n.config.HTTPVirtualHosts, + Modules: n.config.HTTPModules, + prefix: n.config.HTTPPathPrefix, + rpcEndpointConfig: rpcConfig, + }); err != nil { + return err + } + servers = append(servers, server) + return nil + }   initWS := func(port int) error { server := n.wsServerForPort(port, false) @@ -485,6 +514,12 @@ if err := initHttp(n.http, n.config.HTTPPort); err != nil { return err } } + if n.config.HTTPSGTHost != "" { + // Configure unauthenticated HTTP for SGT. + if err := initHttpSGT(n.httpSGT); err != nil { + return err + } + } // Configure WebSocket. if n.config.WSHost != "" { // legacy unauthenticated @@ -524,6 +559,7 @@ }   func (n *Node) stopRPC() { n.http.stop() + n.httpSGT.stop() n.ws.stop() n.httpAuth.stop() n.wsAuth.stop()

The txpool is extended to allow blob transaction is l2 blob is enabled.

The block-building code (in the “miner” package because of Proof-Of-Work legacy of ethereum) is extended to ensure that, for blob transactions, the blobs are required when it’s sequencing, but not required when it’s deriving.

We also charge a minimum data-availability fee for each blob.

diff --git official op-geth/beacon/engine/types.go SWC op-geth/beacon/engine/types.go index 4067988e52ebfebee47682a341bbd3a5b63bfc47..d2f82e15198adc4b18641b0031c6d304a09ec515 100644 --- official op-geth/beacon/engine/types.go +++ SWC op-geth/beacon/engine/types.go @@ -269,6 +269,10 @@ var blobHashes = make([]common.Hash, 0, len(txs)) for _, tx := range txs { blobHashes = append(blobHashes, tx.BlobHashes()...) } + // we only want to check versionedHashes when it's not empty + if len(versionedHashes) == 0 { + versionedHashes = blobHashes + } if len(blobHashes) != len(versionedHashes) { return nil, fmt.Errorf("invalid number of versionedHashes: %v blobHashes: %v", versionedHashes, blobHashes) }
diff --git official op-geth/consensus/misc/eip4844/eip4844.go SWC op-geth/consensus/misc/eip4844/eip4844.go index 5be65229ca91853e579cf5f12bc09f3e849fd807..784b4de9b79052768555e31ed737c9f470471812 100644 --- official op-geth/consensus/misc/eip4844/eip4844.go +++ SWC op-geth/consensus/misc/eip4844/eip4844.go @@ -84,13 +84,16 @@ func CalcBlobFee(config *params.ChainConfig, header *types.Header) *big.Int { // OP-Stack chains don't support blobs, but still set the excessBlobGas field (always to zero). // So this function is called in many places for OP-Stack chains too. In order to not require // a blob schedule in the chain config, we short circuit here. - if config.IsOptimism() { - if config.BlobScheduleConfig != nil || header.ExcessBlobGas == nil || *header.ExcessBlobGas != 0 { - panic("OP-Stack: CalcBlobFee: unexpected blob schedule or excess blob gas") - } - return minBlobGasPrice + // if config.IsOptimism() { + // if config.BlobScheduleConfig != nil || header.ExcessBlobGas == nil || *header.ExcessBlobGas != 0 { + // panic("OP-Stack: CalcBlobFee: unexpected blob schedule or excess blob gas") + // } + // return minBlobGasPrice + // } + + if config.BlobScheduleConfig == nil { + config.BlobScheduleConfig = params.DefaultBlobSchedule } - var frac uint64 switch config.LatestFork(header.Time) { case forks.Osaka:
diff --git official op-geth/core/types/rollup_cost.go SWC op-geth/core/types/rollup_cost.go index 68db0a1188af5b824ef7d3a1e845da6c9b247b5d..0a54ea741da60665231fb1990bed4cdb3bc89826 100644 --- official op-geth/core/types/rollup_cost.go +++ SWC op-geth/core/types/rollup_cost.go @@ -61,6 +61,8 @@ // L1BlockAddr is the address of the L1Block contract which stores the L1 gas attributes. L1BlockAddr = common.HexToAddress("0x4200000000000000000000000000000000000015")   + SoulGasTokenAddr = common.HexToAddress("0x4200000000000000000000000000000000000800") + L1BaseFeeSlot = common.BigToHash(big.NewInt(1)) OverheadSlot = common.BigToHash(big.NewInt(5)) ScalarSlot = common.BigToHash(big.NewInt(6)) @@ -96,6 +98,7 @@ // availability costs for the transaction. type RollupCostData struct { Zeroes, Ones uint64 FastLzSize uint64 + Blobs uint64 }   type StateGetter interface { @@ -127,7 +130,7 @@ // operatorCostFunc is an internal version of OperatorCostFunc that is used for caching. type operatorCostFunc func(gasUsed uint64) *uint256.Int   -func NewRollupCostData(data []byte) (out RollupCostData) { +func NewRollupCostData(data []byte, blobs int) (out RollupCostData) { for _, b := range data { if b == 0 { out.Zeroes++ @@ -135,7 +138,9 @@ } else { out.Ones++ } } + out.FastLzSize = uint64(FlzCompressLen(data)) + out.Blobs = uint64(blobs) return out }   @@ -313,6 +318,7 @@ fee = new(big.Int).Add(calldataCostPerByte, blobCostPerByte) fee = fee.Mul(fee, calldataGasUsed) fee = fee.Div(fee, ecotoneDivisor) + fee = fee.Add(fee, new(big.Int).Mul(big.NewInt(int64(costData.Blobs)), big.NewInt(params.BlobDAFee)))   return fee, calldataGasUsed } @@ -538,6 +544,7 @@ l1FeeScaled := new(big.Int).Add(calldataCostPerByte, blobCostPerByte) estimatedSize := costData.estimatedDASizeScaled() l1CostScaled := new(big.Int).Mul(estimatedSize, l1FeeScaled) l1Cost := new(big.Int).Div(l1CostScaled, fjordDivisor) + l1Cost = new(big.Int).Add(l1Cost, new(big.Int).Mul(big.NewInt(int64(costData.Blobs)), big.NewInt(params.BlobDAFee)))   calldataGasUsed = new(big.Int).Mul(estimatedSize, new(big.Int).SetUint64(params.TxDataNonZeroGasEIP2028)) calldataGasUsed.Div(calldataGasUsed, big.NewInt(1e6))
diff --git official op-geth/core/types/transaction.go SWC op-geth/core/types/transaction.go index bca2cc03a93cf5b8abed07f62795e04f2617a6a7..92f3e4ddcfd57f1c1057392cc8c226373137dedd 100644 --- official op-geth/core/types/transaction.go +++ SWC op-geth/core/types/transaction.go @@ -392,11 +392,14 @@ } if v := tx.rollupCostData.Load(); v != nil { return v.(RollupCostData) } - data, err := tx.MarshalBinary() + // When called from commitBlobTransaction, tx contains blob. + // When called from state processor, tx doesn't contain blob. + // In order to make the result consistent, we should use the version without blob. + data, err := tx.WithoutBlobTxSidecar().MarshalBinary() if err != nil { // Silent error, invalid txs will not be marshalled/unmarshalled for batch submission anyway. log.Error("failed to encode tx for L1 cost computation", "err", err) } - out := NewRollupCostData(data) + out := NewRollupCostData(data, len(tx.BlobHashes())) tx.rollupCostData.Store(out) return out }
diff --git official op-geth/core/types/transaction_signing.go SWC op-geth/core/types/transaction_signing.go index 8b5e56db75bdf8b6191ebc3b8131df3207d75365..5fef5122cfa4969948549c16ae4267311c58489e 100644 --- official op-geth/core/types/transaction_signing.go +++ SWC op-geth/core/types/transaction_signing.go @@ -44,7 +44,7 @@ case config.IsIsthmus(blockTime): signer = NewIsthmusSigner(config.ChainID) case config.IsPrague(blockNumber, blockTime) && !config.IsOptimism(): signer = NewPragueSigner(config.ChainID) - case config.IsCancun(blockNumber, blockTime) && !config.IsOptimism(): + case config.IsCancun(blockNumber, blockTime) && (!config.IsOptimism() || config.IsL2Blob(blockNumber, blockTime)): signer = NewCancunSigner(config.ChainID) case config.IsLondon(blockNumber): signer = NewLondonSigner(config.ChainID) @@ -75,7 +75,7 @@ case config.IsthmusTime != nil: signer = NewIsthmusSigner(config.ChainID) case config.PragueTime != nil && !config.IsOptimism(): signer = NewPragueSigner(config.ChainID) - case config.CancunTime != nil && !config.IsOptimism(): + case config.CancunTime != nil && (!config.IsOptimism() || config.L2BlobTime != nil): signer = NewCancunSigner(config.ChainID) case config.LondonBlock != nil: signer = NewLondonSigner(config.ChainID)
diff --git official op-geth/eth/api_backend.go SWC op-geth/eth/api_backend.go index 9e42c6155a94834f79840063fe8a64e3fedc3a60..3e4b805b43de0a637488ad4a14cc6db25cf3ddd2 100644 --- official op-geth/eth/api_backend.go +++ SWC op-geth/eth/api_backend.go @@ -277,7 +277,8 @@ return b.eth.BlockChain().SubscribeLogsEvent(ch) }   func (b *EthAPIBackend) SendTx(ctx context.Context, signedTx *types.Transaction) error { - if b.ChainConfig().IsOptimism() && signedTx.Type() == types.BlobTxType { + header := b.eth.blockchain.CurrentHeader() + if signedTx.Type() == types.BlobTxType && b.ChainConfig().IsOptimism() && !b.ChainConfig().IsL2Blob(header.Number, header.Time) { return types.ErrTxTypeNotSupported }
diff --git official op-geth/eth/backend.go SWC op-geth/eth/backend.go index 7465ad772b2bc9f75c3b324c6027ca483e353526..43d2423f97d41da4b9a7538c64043cf31994f0b9 100644 --- official op-geth/eth/backend.go +++ SWC op-geth/eth/backend.go @@ -279,7 +279,7 @@ } legacyPool := legacypool.New(config.TxPool, eth.blockchain)   txPools := []txpool.SubPool{legacyPool} - if !eth.BlockChain().Config().IsOptimism() { + if !eth.BlockChain().Config().IsOptimism() || eth.BlockChain().Config().L2BlobTime != nil { blobPool := blobpool.New(config.BlobPool, eth.blockchain) txPools = append(txPools, blobPool) }
diff --git official op-geth/miner/worker.go SWC op-geth/miner/worker.go index 6726b88b03cba072b9f0530b2234ab9ab0fc7fbd..95515fe852f44a5eb0b79343c5acf27b1964ca11 100644 --- official op-geth/miner/worker.go +++ SWC op-geth/miner/worker.go @@ -77,11 +77,17 @@ txs []*types.Transaction receipts []*types.Receipt sidecars []*types.BlobTxSidecar blobs int + witness *stateless.Witness   - witness *stateless.Witness - + // this is used as a flag whether it's *effectively* sequencing or deriving (es comment) noTxs bool // true if we are reproducing a block, and do not have to check interop txs rpcCtx context.Context // context to control block-building RPC work. No RPC allowed if nil. +} + +// isEffectivelySequencing is true when PayloadAttributes.NoTxPool is false, +// which only happens when it's sequencing. +func (env *environment) isEffectivelySequencing() bool { + return !env.noTxs }   const ( @@ -325,6 +331,7 @@ } if miner.chainConfig.IsPrague(header.Number, header.Time) { core.ProcessParentBlockHash(header.ParentHash, env.evm) } + env.noTxs = genParams.noTxs // invariant: genParams.noTxs has the same boolean value as PayloadAttributes.NoTxPool return env, nil }   @@ -397,15 +404,25 @@ }   func (miner *Miner) commitBlobTransaction(env *environment, tx *types.Transaction) error { sc := tx.BlobTxSidecar() + var nblobs int if sc == nil { - panic("blob transaction without blobs in miner") + if env.isEffectivelySequencing() /* we want to allow blob tx without blobs when it's deriving */ { + panic("blob transaction without blobs in sequencing") + } else { // deriving, which does not have the sidecar + nblobs = len(tx.BlobHashes()) + } + } else { + if !env.isEffectivelySequencing() { + panic("blob transaction with blobs in derivation") + } + nblobs = len(sc.Blobs) } // Checking against blob gas limit: It's kind of ugly to perform this check here, but there // isn't really a better place right now. The blob gas limit is checked at block validation time // and not during execution. This means core.ApplyTransaction will not return an error if the // tx has too many blobs. So we have to explicitly check it here. maxBlobs := eip4844.MaxBlobsPerBlock(miner.chainConfig, env.header.Time) - if env.blobs+len(sc.Blobs) > maxBlobs { + if env.blobs+nblobs > maxBlobs { return errors.New("max data blobs reached") } receipt, err := miner.applyTransaction(env, tx) @@ -414,8 +431,10 @@ return err } env.txs = append(env.txs, tx.WithoutBlobTxSidecar()) env.receipts = append(env.receipts, receipt) - env.sidecars = append(env.sidecars, sc) - env.blobs += len(sc.Blobs) + if sc != nil { + env.sidecars = append(env.sidecars, sc) + } + env.blobs += len(tx.BlobHashes()) *env.header.BlobGasUsed += receipt.BlobGasUsed env.tcount++ return nil
diff --git official op-geth/params/config.go SWC op-geth/params/config.go index 780234ab60a3768132a0c36f337d17e445d6aecd..dce825472cec436be890b021ae04822d4edbd7a4 100644 --- official op-geth/params/config.go +++ SWC op-geth/params/config.go @@ -425,6 +425,7 @@ JovianTime *uint64 `json:"jovianTime,omitempty"` // Jovian switch time (nil = no fork, 0 = already on Optimism Jovian)   InteropTime *uint64 `json:"interopTime,omitempty"` // Interop switch time (nil = no fork, 0 = already on optimism interop)   + L2BlobTime *uint64 `json:"l2BlobTime,omitempty"` // L2Blob switch time (nil = no fork, 0 = already on optimism l2blob) // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` @@ -477,11 +478,19 @@ type OptimismConfig struct { EIP1559Elasticity uint64 `json:"eip1559Elasticity"` EIP1559Denominator uint64 `json:"eip1559Denominator"` EIP1559DenominatorCanyon *uint64 `json:"eip1559DenominatorCanyon,omitempty"` + // Flag for when to activate SoulGasToken for gas fee. + SoulGasTokenBlock *uint64 `json:"soulGasTokenBlock"` + // Whether SoulGasToken is backed by native token or minted by whitelisted miners, only effective when SoulGasTokenBlock is non-nil + IsSoulBackedByNative bool `json:"isSoulBackedByNative"` }   // String implements the stringer interface, returning the optimism fee config details. func (o *OptimismConfig) String() string { return "optimism" +} + +func (o *OptimismConfig) IsSoulGasToken(targetHeight uint64) bool { + return o.SoulGasTokenBlock != nil && *o.SoulGasTokenBlock <= targetHeight }   // Description returns a human-readable description of ChainConfig. @@ -588,6 +597,17 @@ } if c.InteropTime != nil { banner += fmt.Sprintf(" - Interop: @%-10v\n", *c.InteropTime) } + if c.L2BlobTime != nil { + banner += fmt.Sprintf(" - L2BLob: @%-10v\n", *c.L2BlobTime) + } + banner += "\n" + if c.Optimism != nil { + if c.Optimism.SoulGasTokenBlock == nil { + banner += "SGT: false" + } else { + banner += fmt.Sprintf("SGT: @%-10v, Back by native %t", *c.Optimism.SoulGasTokenBlock, c.Optimism.IsSoulBackedByNative) + } + } return banner }   @@ -696,6 +716,11 @@ func (c *ChainConfig) IsCancun(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.CancunTime, time) }   +// IsL2Blob returns whether l2 blob is enabled +func (c *ChainConfig) IsL2Blob(num *big.Int, time uint64) bool { + return c.IsCancun(num, time) && c.Optimism != nil && isTimestampForked(c.L2BlobTime, time) +} + // IsPrague returns whether time is either equal to the Prague fork time or greater. func (c *ChainConfig) IsPrague(num *big.Int, time uint64) bool { return c.IsLondon(num) && isTimestampForked(c.PragueTime, time) @@ -914,9 +939,7 @@ // OP-Stack chains don't support blobs, and must have a nil BlobScheduleConfig. if c.IsOptimism() { if c.BlobScheduleConfig == nil { - return nil - } else { - return errors.New("OP-Stack chains must have empty blob configuration") + c.BlobScheduleConfig = DefaultBlobSchedule } }   @@ -960,6 +983,13 @@ if bc.UpdateFraction == 0 { return errors.New("update fraction must be defined and non-zero") } return nil +} + +func uint64ptr2Big(ptr *uint64) *big.Int { + if ptr == nil { + return nil + } + return new(big.Int).SetUint64(*ptr) }   func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, headNumber *big.Int, headTimestamp uint64, genesisTimestamp *uint64) *ConfigCompatError { @@ -1018,6 +1048,13 @@ } if isForkBlockIncompatible(c.MergeNetsplitBlock, newcfg.MergeNetsplitBlock, headNumber) { return newBlockCompatError("Merge netsplit fork block", c.MergeNetsplitBlock, newcfg.MergeNetsplitBlock) } + if c.Optimism != nil && newcfg.Optimism != nil { + oldBlock := uint64ptr2Big(c.Optimism.SoulGasTokenBlock) + newBlock := uint64ptr2Big(newcfg.Optimism.SoulGasTokenBlock) + if isForkBlockIncompatible(oldBlock, newBlock, headNumber) { + return newBlockCompatError("SoulGasToken fork block", oldBlock, newBlock) + } + } if isForkTimestampIncompatible(c.ShanghaiTime, newcfg.ShanghaiTime, headTimestamp, genesisTimestamp) { return newTimestampCompatError("Shanghai fork timestamp", c.ShanghaiTime, newcfg.ShanghaiTime) } @@ -1062,6 +1099,9 @@ return newTimestampCompatError("Jovian fork timestamp", c.JovianTime, newcfg.JovianTime) } if isForkTimestampIncompatible(c.InteropTime, newcfg.InteropTime, headTimestamp, genesisTimestamp) { return newTimestampCompatError("Interop fork timestamp", c.InteropTime, newcfg.InteropTime) + } + if isForkTimestampIncompatible(c.L2BlobTime, newcfg.L2BlobTime, headTimestamp, genesisTimestamp) { + return newTimestampCompatError("L2Blob fork timestamp", c.L2BlobTime, newcfg.L2BlobTime) } return nil }
diff --git official op-geth/params/protocol_params.go SWC op-geth/params/protocol_params.go index 00a83375acd4deef456a38cb56f99da484c1c352..6bb3e0b387a626ee4dc5d0c92d688ff2ec2c2530 100644 --- official op-geth/params/protocol_params.go +++ SWC op-geth/params/protocol_params.go @@ -51,6 +51,8 @@ QuadCoeffDiv uint64 = 512 // Divisor for the quadratic particle of the memory cost equation. LogDataGas uint64 = 8 // Per byte in a LOG* operation's data. CallStipend uint64 = 2300 // Free gas given at beginning of call.   + BlobDAFee = 2000 // Fee for storing blob to DA provider + Keccak256Gas uint64 = 30 // Once per KECCAK256 operation. Keccak256WordGas uint64 = 6 // Once per word of the KECCAK256 operation's data. InitCodeWordGas uint64 = 2 // Once per word of the init code when creating a contract.

As we’re using the QKC token (which is an ERC20 token) on L1 as the gas token, the RPCTxFeeCap is changed from 1 to 1000.

diff --git official op-geth/eth/ethconfig/config.go SWC op-geth/eth/ethconfig/config.go index 7668ee28f5fbf54bcc7dd3373ecd1092e1ce9a9a..ac1b9c3651aa9ebcfeb2c40241cd27617a9db8d1 100644 --- official op-geth/eth/ethconfig/config.go +++ SWC op-geth/eth/ethconfig/config.go @@ -66,7 +66,7 @@ BlobPool: blobpool.DefaultConfig, RPCGasCap: 50000000, RPCEVMTimeout: 5 * time.Second, GPO: FullNodeGPO, - RPCTxFeeCap: 1, // 1 ether + RPCTxFeeCap: 1000, // 1000 QKC }   //go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go
diff --git official op-geth/cmd/geth/chaincmd.go SWC op-geth/cmd/geth/chaincmd.go index bbadb1cc19286831c14a2fdc3dade46d6f2db774..a88a156d8fa64b6b902fe5340b8c171b7441a910 100644 --- official op-geth/cmd/geth/chaincmd.go +++ SWC op-geth/cmd/geth/chaincmd.go @@ -230,9 +230,12 @@ triedb := utils.MakeTrieDatabase(ctx, chaindb, ctx.Bool(utils.CachePreimagesFlag.Name), false, genesis.IsVerkle()) defer triedb.Close()   - _, hash, _, err := core.SetupGenesisBlockWithOverride(chaindb, triedb, genesis, &overrides) + _, hash, compatErr, err := core.SetupGenesisBlockWithOverride(chaindb, triedb, genesis, &overrides) if err != nil { utils.Fatalf("Failed to write genesis block: %v", err) + } + if compatErr != nil { + utils.Fatalf("Failed to write chain config: %v", compatErr) } log.Info("Successfully wrote genesis state", "database", "chaindata", "hash", hash)
diff --git official op-geth/params/bootnodes.go SWC op-geth/params/bootnodes.go index dc2abe59c74f1d2d6c7d06bea47d0d14af25d83d..ea1b73b78d8d1393dcaaff013d53a593d18e6183 100644 --- official op-geth/params/bootnodes.go +++ SWC op-geth/params/bootnodes.go @@ -47,6 +47,11 @@ "enode://10d62eff032205fcef19497f35ca8477bea0eadfff6d769a147e895d8b2b8f8ae6341630c645c30f5df6e67547c03494ced3d9c5764e8622a26587b083b028e8@139.59.49.206:30303", // sepolia-bootnode-1-blr1 "enode://9e9492e2e8836114cc75f5b929784f4f46c324ad01daf87d956f98b3b6c5fcba95524d6e5cf9861dc96a2c8a171ea7105bb554a197455058de185fa870970c7c@138.68.123.152:30303", // sepolia-bootnode-1-ams3 }   +var BetaTestnetBootnodes = []string{ + "enode://8c83b5f320d499e6b92d26b986931780d35ae97e6621fc3cafce0ced5b33ead4fd06aef00e7a4dca11fee0ebcd168898d2b8ac94ca642fba87d73bd54b004050@176.9.142.29:30303", + "enode://4e9d3ba7d41cf1a42456c3e9d024808128027ead5d887a1365fd6181f75b4a7ca748acfac8f6b492aaa8fca349fbc0b07226b1772b6380608536494917a62f03@46.4.99.154:30303", +} + var V5Bootnodes = []string{ // Teku team's bootnode "enr:-KG4QMOEswP62yzDjSwWS4YEjtTZ5PO6r65CPqYBkgTTkrpaedQ8uEUo1uMALtJIvb2w_WWEVmg5yt1UAuK1ftxUU7QDhGV0aDKQu6TalgMAAAD__________4JpZIJ2NIJpcIQEnfA2iXNlY3AyNTZrMaEDfol8oLr6XJ7FsdAYE7lpJhKMls4G_v6qQOGKJUWGb_uDdGNwgiMog3VkcIIjKA", // # 4.157.240.54 | azure-us-east-virginia
diff --git official op-geth/qkc-fork.yaml SWC op-geth/qkc-fork.yaml new file mode 100644 index 0000000000000000000000000000000000000000..57c0a9e836420f6a6298dc5ce07ec11cb31ca80c --- /dev/null +++ SWC op-geth/qkc-fork.yaml @@ -0,0 +1,80 @@ +title: "SWC-op-geth - official-op-geth fork diff overview" +footer: | + Fork-diff overview of [`SWC op-geth`](https://github.com/ethstorage/op-geth), a fork of [`official op-geth`](https://github.com/ethereum-optimism/op-geth). + and execution-engine of the [SWC OP-stack](https://github.com/ethstorage/optimism). +base: + name: official op-geth + url: https://github.com/ethereum-optimism/op-geth + hash: 2b9abb39077cb88f6e8a513f09a5ea2c2569dfed +fork: + name: SWC op-geth + url: https://github.com/ethstorage/op-geth + ref: refs/heads/op-es +def: + title: "op-geth" + description: | + This is an overview of the changes in [`SWC op-geth`](https://github.com/ethstorage/op-geth), + a fork of [`official op-geth`](https://github.com/ethereum-optimism/op-geth), part of the [SWC OP-stack](https://github.com/ethstorage/optimism). + sub: + - title: "Soul Gas Token modifications" + description: | + When charging gas, the EVM is extended to first charge from the Soul Gas Token (which is a soul bounded ERC20) balance; + Then charge from the native balance if it's not enough. + + The txpool and gas estimator are also extended to use the sum of native balance and Soul Gas Token balance as the effective balance. + + In order to fix a compatibility [issue](https://github.com/ethstorage/optimism/issues/148) with wallets, the node is + also extended to listen to another http port alongside the default one, the only different is that the `eth_GetBalance` API + will return the sum of native balance and Soul Gas Token balance. + globs: + - "core/state_transition.go" + - "core/txpool/legacypool/legacypool.go" + - "core/txpool/blobpool/blobpool.go" + - "core/txpool/validation.go" + - "eth/gasestimator/gasestimator.go" + - "internal/ethapi/api.go" + - "node/config.go" + - "node/defaults.go" + - "node/node.go" + - "cmd/geth/main.go" + - "cmd/utils/flags.go" + - "cmd/geth/config.go" + - title: "L2 Blob modifications" + description: | + The txpool is extended to allow blob transaction is l2 blob is enabled. + + The block-building code (in the "miner" package because of Proof-Of-Work legacy of ethereum) is extended to ensure that, + for blob transactions, the blobs are required when it's sequencing, but not required when it's deriving. + + We also charge a minimum data-availability fee for each blob. + globs: + - "miner/worker.go" + - "eth/api_backend.go" + - "eth/backend.go" + - "core/txpool/validation.go" + - "core/types/transaction.go" + - "core/types/transaction_signing.go" + - "params/config.go" + - "params/protocol_params.go" + - "beacon/engine/types.go" + - "core/types/rollup_cost.go" + - "consensus/misc/eip4844/eip4844.go" + - title: "Custom Gas Token modification" + description: | + As we're using the QKC token (which is an ERC20 token) on L1 as the gas token, the `RPCTxFeeCap` is changed from 1 to 1000. + globs: + - "eth/ethconfig/config.go" + + +# ignored globally, does not count towards line count +ignore: + - ".circleci/*" + - "*.sum" + - "go.mod" + - "fork.yaml" + - "Makefile" + - ".golangci.yml" + - ".github/**" + - "**/*.gob" # data asset, not code + - "core/vm/testdata/precompiles/p256Verify.json" # data asset, not code + - "eth/tracers/internal/tracetest/testdata/**/*.json"
diff --git official op-geth/.github/workflows/publish-op-geth.yml SWC op-geth/.github/workflows/publish-op-geth.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd56364d6c9d49694d17f35a8f75a31ae9b1eab2 --- /dev/null +++ SWC op-geth/.github/workflows/publish-op-geth.yml @@ -0,0 +1,65 @@ +# This workflow will publish a github release for op-geth + +name: Publish +run-name: ${{ github.actor }} is publishing an op-geth release 🚀 +on: + push: + tags: + - 'v*' + +# Always wait for previous release to finish before releasing again +concurrency: ${{ github.workflow }}-${{ github.ref }} + + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + arch: [amd64, arm64] + exclude: + - os: ubuntu-latest + arch: arm64 + env: + BUILD_DIR: op-geth.${{ github.ref_name }} + BIN_DIR: op-geth.${{ github.ref_name }}/build/bin + FILE_NAME: op-geth.${{ github.ref_name }}.${{ matrix.os == 'ubuntu-latest' && 'linux' || 'darwin' }}-${{ matrix.arch }}.tar.gz + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup go + uses: actions/setup-go@v5 + with: + go-version: '1.22.7' + + - name: Build + run: | + CGO_ENABLED=0 TARGETOS=${{ matrix.os == 'ubuntu-latest' && 'linux' || 'darwin' }} TARGETARCH=${{ matrix.arch }} make geth + mkdir -p ${{ env.BIN_DIR }} + mv build/bin/geth ${{ env.BIN_DIR }}/ + tar -czvf ${{ env.FILE_NAME }} ${{ env.BUILD_DIR }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.FILE_NAME }} + path: ${{ env.FILE_NAME }} + + release: + needs: build + runs-on: ubuntu-latest + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref }} + name: Release ${{ github.ref_name }} + files: | + **/* + fail_on_unmatched_files: true + generate_release_notes: true