Commit a54e19f4 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

Merge pull request #1248 from ethereum-optimism/develop

Develop -> Master Merge
parents fa4c743f 08083333
---
'@eth-optimism/contracts': patch
---
Add a hardhat task for setting the L2 gas price
---
'@eth-optimism/l2geth': patch
---
Allow zero gas price transactions from the `OVM_GasPriceOracle.owner` when enforce fees is set to true. This is to prevent the need to manage an additional hot wallet as well as prevent any situation where a bug causes the fees to go too high that it is not possible to lower the fee by sending a transaction
---
'@eth-optimism/integration-tests': patch
---
Add various stress tests
---
'@eth-optimism/l2geth': patch
---
Add sequencer fee buffer with config options `ROLLUP_FEE_THRESHOLD_UP` and `ROLLUP_FEE_THRESHOLD_DOWN` that are interpreted as floating point numbers
---
'@eth-optimism/gas-oracle': patch
---
Initial implementation of the `gas-oracle`
# @eth-optimism/gas-oracle
## 0.0.2
### Patch Changes
- ce3c353b: Initial implementation of the `gas-oracle`
{ {
"name": "@eth-optimism/gas-oracle", "name": "@eth-optimism/gas-oracle",
"version": "0.0.1", "version": "0.0.2",
"private": true, "private": true,
"devDependencies": {} "devDependencies": {}
} }
# @eth-optimism/integration-tests # @eth-optimism/integration-tests
## 0.2.1
### Patch Changes
- f1dc8b77: Add various stress tests
## 0.2.0 ## 0.2.0
### Minor Changes ### Minor Changes
......
{ {
"name": "@eth-optimism/integration-tests", "name": "@eth-optimism/integration-tests",
"version": "0.2.0", "version": "0.2.1",
"description": "[Optimism] Integration Tests", "description": "[Optimism] Integration Tests",
"private": true, "private": true,
"author": "Optimism PBC", "author": "Optimism PBC",
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
"clean": "rimraf cache artifacts artifacts-ovm cache-ovm" "clean": "rimraf cache artifacts artifacts-ovm cache-ovm"
}, },
"devDependencies": { "devDependencies": {
"@eth-optimism/contracts": "^0.4.2", "@eth-optimism/contracts": "^0.4.3",
"@eth-optimism/core-utils": "^0.5.0", "@eth-optimism/core-utils": "^0.5.0",
"@eth-optimism/hardhat-ovm": "^0.2.2", "@eth-optimism/hardhat-ovm": "^0.2.2",
"@eth-optimism/message-relayer": "^0.1.6", "@eth-optimism/message-relayer": "^0.1.6",
......
...@@ -166,6 +166,8 @@ var ( ...@@ -166,6 +166,8 @@ var (
utils.RollupMaxCalldataSizeFlag, utils.RollupMaxCalldataSizeFlag,
utils.RollupBackendFlag, utils.RollupBackendFlag,
utils.RollupEnforceFeesFlag, utils.RollupEnforceFeesFlag,
utils.RollupFeeThresholdDownFlag,
utils.RollupFeeThresholdUpFlag,
utils.GasPriceOracleOwnerAddress, utils.GasPriceOracleOwnerAddress,
} }
......
...@@ -81,6 +81,8 @@ var AppHelpFlagGroups = []flagGroup{ ...@@ -81,6 +81,8 @@ var AppHelpFlagGroups = []flagGroup{
utils.RollupMaxCalldataSizeFlag, utils.RollupMaxCalldataSizeFlag,
utils.RollupBackendFlag, utils.RollupBackendFlag,
utils.RollupEnforceFeesFlag, utils.RollupEnforceFeesFlag,
utils.RollupFeeThresholdDownFlag,
utils.RollupFeeThresholdUpFlag,
utils.GasPriceOracleOwnerAddress, utils.GasPriceOracleOwnerAddress,
}, },
}, },
......
...@@ -898,6 +898,16 @@ var ( ...@@ -898,6 +898,16 @@ var (
Usage: "Disable transactions with 0 gas price", Usage: "Disable transactions with 0 gas price",
EnvVar: "ROLLUP_ENFORCE_FEES", EnvVar: "ROLLUP_ENFORCE_FEES",
} }
RollupFeeThresholdDownFlag = cli.BoolFlag{
Name: "rollup.feethresholddown",
Usage: "Allow txs with fees below the current fee up to this amount, must be < 1",
EnvVar: "ROLLUP_FEE_THRESHOLD_DOWN",
}
RollupFeeThresholdUpFlag = cli.BoolFlag{
Name: "rollup.feethresholdup",
Usage: "Allow txs with fees above the current fee up to this amount, must be > 1",
EnvVar: "ROLLUP_FEE_THRESHOLD_UP",
}
GasPriceOracleOwnerAddress = cli.StringFlag{ GasPriceOracleOwnerAddress = cli.StringFlag{
Name: "rollup.gaspriceoracleowneraddress", Name: "rollup.gaspriceoracleowneraddress",
Usage: "Owner of the OVM_GasPriceOracle", Usage: "Owner of the OVM_GasPriceOracle",
...@@ -1196,6 +1206,14 @@ func setRollup(ctx *cli.Context, cfg *rollup.Config) { ...@@ -1196,6 +1206,14 @@ func setRollup(ctx *cli.Context, cfg *rollup.Config) {
if ctx.GlobalIsSet(RollupEnforceFeesFlag.Name) { if ctx.GlobalIsSet(RollupEnforceFeesFlag.Name) {
cfg.EnforceFees = true cfg.EnforceFees = true
} }
if ctx.GlobalIsSet(RollupFeeThresholdDownFlag.Name) {
val := ctx.GlobalFloat64(RollupFeeThresholdDownFlag.Name)
cfg.FeeThresholdDown = new(big.Float).SetFloat64(val)
}
if ctx.GlobalIsSet(RollupFeeThresholdUpFlag.Name) {
val := ctx.GlobalFloat64(RollupFeeThresholdUpFlag.Name)
cfg.FeeThresholdUp = new(big.Float).SetFloat64(val)
}
} }
// setLes configures the les server and ultra light client settings from the command line flags. // setLes configures the les server and ultra light client settings from the command line flags.
......
...@@ -39,4 +39,9 @@ type Config struct { ...@@ -39,4 +39,9 @@ type Config struct {
Backend Backend Backend Backend
// Only accept transactions with fees // Only accept transactions with fees
EnforceFees bool EnforceFees bool
// Allow fees within a buffer upwards or downwards
// to take fee volatility into account between being
// quoted and the transaction being executed
FeeThresholdDown *big.Float
FeeThresholdUp *big.Float
} }
package fees package fees
import ( import (
"errors"
"fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
var (
// errFeeTooLow represents the error case of then the user pays too little
ErrFeeTooLow = errors.New("fee too low")
// errFeeTooHigh represents the error case of when the user pays too much
ErrFeeTooHigh = errors.New("fee too high")
// errMissingInput represents the error case of missing required input to
// PaysEnough
errMissingInput = errors.New("missing input")
)
// overhead represents the fixed cost of batch submission of a single // overhead represents the fixed cost of batch submission of a single
// transaction in gas. // transaction in gas.
const overhead uint64 = 4200 + 200*params.TxDataNonZeroGasEIP2028 const overhead uint64 = 4200 + 200*params.TxDataNonZeroGasEIP2028
...@@ -87,6 +99,52 @@ func DecodeL2GasLimitU64(gasLimit uint64) uint64 { ...@@ -87,6 +99,52 @@ func DecodeL2GasLimitU64(gasLimit uint64) uint64 {
return scaled * tenThousand return scaled * tenThousand
} }
// PaysEnoughOpts represent the options to PaysEnough
type PaysEnoughOpts struct {
UserFee, ExpectedFee *big.Int
ThresholdUp, ThresholdDown *big.Float
}
// PaysEnough returns an error if the fee is not large enough
// `GasPrice` and `Fee` are required arguments.
func PaysEnough(opts *PaysEnoughOpts) error {
if opts.UserFee == nil {
return fmt.Errorf("%w: no user fee", errMissingInput)
}
if opts.ExpectedFee == nil {
return fmt.Errorf("%w: no expected fee", errMissingInput)
}
fee := opts.ExpectedFee
// Allow for a downward buffer to protect against L1 gas price volatility
if opts.ThresholdDown != nil {
fee = mulByFloat(fee, opts.ThresholdDown)
}
// Protect the sequencer from being underpaid
// if user fee < expected fee, return error
if opts.UserFee.Cmp(fee) == -1 {
return ErrFeeTooLow
}
// Protect users from overpaying by too much
if opts.ThresholdUp != nil {
// overpaying = user fee - expected fee
overpaying := new(big.Int).Sub(opts.UserFee, opts.ExpectedFee)
threshold := mulByFloat(overpaying, opts.ThresholdUp)
// if overpaying > threshold, return error
if overpaying.Cmp(threshold) == 1 {
return ErrFeeTooHigh
}
}
return nil
}
func mulByFloat(num *big.Int, float *big.Float) *big.Int {
n := new(big.Float).SetUint64(num.Uint64())
n = n.Mul(n, float)
value, _ := float.Uint64()
return new(big.Int).SetUint64(value)
}
// calculateL1GasLimit computes the L1 gasLimit based on the calldata and // calculateL1GasLimit computes the L1 gasLimit based on the calldata and
// constant sized overhead. The overhead can be decreased as the cost of the // constant sized overhead. The overhead can be decreased as the cost of the
// batch submission goes down via contract optimizations. This will not overflow // batch submission goes down via contract optimizations. This will not overflow
......
package fees package fees
import ( import (
"errors"
"math/big" "math/big"
"testing" "testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
...@@ -102,3 +104,74 @@ func TestCalculateRollupFee(t *testing.T) { ...@@ -102,3 +104,74 @@ func TestCalculateRollupFee(t *testing.T) {
}) })
} }
} }
func TestPaysEnough(t *testing.T) {
tests := map[string]struct {
opts *PaysEnoughOpts
err error
}{
"missing-gas-price": {
opts: &PaysEnoughOpts{
UserFee: nil,
ExpectedFee: new(big.Int),
ThresholdUp: nil,
ThresholdDown: nil,
},
err: errMissingInput,
},
"missing-fee": {
opts: &PaysEnoughOpts{
UserFee: nil,
ExpectedFee: nil,
ThresholdUp: nil,
ThresholdDown: nil,
},
err: errMissingInput,
},
"equal-fee": {
opts: &PaysEnoughOpts{
UserFee: common.Big1,
ExpectedFee: common.Big1,
ThresholdUp: nil,
ThresholdDown: nil,
},
err: nil,
},
"fee-too-low": {
opts: &PaysEnoughOpts{
UserFee: common.Big1,
ExpectedFee: common.Big2,
ThresholdUp: nil,
ThresholdDown: nil,
},
err: ErrFeeTooLow,
},
"fee-threshold-down": {
opts: &PaysEnoughOpts{
UserFee: common.Big1,
ExpectedFee: common.Big2,
ThresholdUp: nil,
ThresholdDown: new(big.Float).SetFloat64(0.5),
},
err: nil,
},
"fee-threshold-up": {
opts: &PaysEnoughOpts{
UserFee: common.Big3,
ExpectedFee: common.Big1,
ThresholdUp: new(big.Float).SetFloat64(1.5),
ThresholdDown: nil,
},
err: ErrFeeTooHigh,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
err := PaysEnough(tt.opts)
if !errors.Is(err, tt.err) {
t.Fatalf("%s: got %s, expected %s", name, err, tt.err)
}
})
}
}
...@@ -24,14 +24,30 @@ import ( ...@@ -24,14 +24,30 @@ import (
"github.com/ethereum/go-ethereum/rollup/fees" "github.com/ethereum/go-ethereum/rollup/fees"
) )
// errShortRemoteTip is an error for when the remote tip is shorter than the var (
// local tip // errBadConfig is the error when the SyncService is started with invalid
var errShortRemoteTip = errors.New("Unexpected remote less than tip") // configuration options
errBadConfig = errors.New("bad config")
// errShortRemoteTip is an error for when the remote tip is shorter than the
// local tip
errShortRemoteTip = errors.New("unexpected remote less than tip")
// errZeroGasPriceTx is the error for when a user submits a transaction
// with gas price zero and fees are currently enforced
errZeroGasPriceTx = errors.New("cannot accept 0 gas price transaction")
float1 = big.NewFloat(1)
)
// L2GasPrice slot refers to the storage slot that the execution price is stored var (
// in the L2 predeploy contract, the GasPriceOracle // l2GasPriceSlot refers to the storage slot that the L2 gas price is stored
var l2GasPriceSlot = common.BigToHash(big.NewInt(1)) // in in the OVM_GasPriceOracle predeploy
var l2GasPriceOracleAddress = common.HexToAddress("0x420000000000000000000000000000000000000F") l2GasPriceSlot = common.BigToHash(big.NewInt(1))
// l2GasPriceOracleOwnerSlot refers to the storage slot that the owner of
// the OVM_GasPriceOracle is stored in
l2GasPriceOracleOwnerSlot = common.BigToHash(big.NewInt(0))
// l2GasPriceOracleAddress is the address of the OVM_GasPriceOracle
// predeploy
l2GasPriceOracleAddress = common.HexToAddress("0x420000000000000000000000000000000000000F")
)
// SyncService implements the main functionality around pulling in transactions // SyncService implements the main functionality around pulling in transactions
// and executing them. It can be configured to run in both sequencer mode and in // and executing them. It can be configured to run in both sequencer mode and in
...@@ -58,7 +74,12 @@ type SyncService struct { ...@@ -58,7 +74,12 @@ type SyncService struct {
timestampRefreshThreshold time.Duration timestampRefreshThreshold time.Duration
chainHeadCh chan core.ChainHeadEvent chainHeadCh chan core.ChainHeadEvent
backend Backend backend Backend
gasPriceOracleOwnerAddress common.Address
gasPriceOracleOwnerAddressLock *sync.RWMutex
enforceFees bool enforceFees bool
signer types.Signer
feeThresholdUp *big.Float
feeThresholdDown *big.Float
} }
// NewSyncService returns an initialized sync service // NewSyncService returns an initialized sync service
...@@ -74,6 +95,9 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co ...@@ -74,6 +95,9 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co
log.Info("Running in verifier mode", "sync-backend", cfg.Backend.String()) log.Info("Running in verifier mode", "sync-backend", cfg.Backend.String())
} else { } else {
log.Info("Running in sequencer mode", "sync-backend", cfg.Backend.String()) log.Info("Running in sequencer mode", "sync-backend", cfg.Backend.String())
log.Info("Fees", "gas-price", fees.BigTxGasPrice, "threshold-up", cfg.FeeThresholdUp,
"threshold-down", cfg.FeeThresholdDown)
log.Info("Enforce Fees", "set", cfg.EnforceFees)
} }
pollInterval := cfg.PollInterval pollInterval := cfg.PollInterval
...@@ -95,7 +119,23 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co ...@@ -95,7 +119,23 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co
// Initialize the rollup client // Initialize the rollup client
client := NewClient(cfg.RollupClientHttp, chainID) client := NewClient(cfg.RollupClientHttp, chainID)
log.Info("Configured rollup client", "url", cfg.RollupClientHttp, "chain-id", chainID.Uint64(), "ctc-deploy-height", cfg.CanonicalTransactionChainDeployHeight) log.Info("Configured rollup client", "url", cfg.RollupClientHttp, "chain-id", chainID.Uint64(), "ctc-deploy-height", cfg.CanonicalTransactionChainDeployHeight)
log.Info("Enforce Fees", "set", cfg.EnforceFees)
// Ensure sane values for the fee thresholds
if cfg.FeeThresholdDown != nil {
// The fee threshold down should be less than 1
if cfg.FeeThresholdDown.Cmp(float1) != -1 {
return nil, fmt.Errorf("%w: fee threshold down not lower than 1: %f", errBadConfig,
cfg.FeeThresholdDown)
}
}
if cfg.FeeThresholdUp != nil {
// The fee threshold up should be greater than 1
if cfg.FeeThresholdUp.Cmp(float1) != 1 {
return nil, fmt.Errorf("%w: fee threshold up not larger than 1: %f", errBadConfig,
cfg.FeeThresholdUp)
}
}
service := SyncService{ service := SyncService{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
...@@ -111,7 +151,12 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co ...@@ -111,7 +151,12 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co
pollInterval: pollInterval, pollInterval: pollInterval,
timestampRefreshThreshold: timestampRefreshThreshold, timestampRefreshThreshold: timestampRefreshThreshold,
backend: cfg.Backend, backend: cfg.Backend,
gasPriceOracleOwnerAddress: cfg.GasPriceOracleOwnerAddress,
gasPriceOracleOwnerAddressLock: new(sync.RWMutex),
enforceFees: cfg.EnforceFees, enforceFees: cfg.EnforceFees,
signer: types.NewEIP155Signer(chainID),
feeThresholdDown: cfg.FeeThresholdDown,
feeThresholdUp: cfg.FeeThresholdUp,
} }
// The chainHeadSub is used to synchronize the SyncService with the chain. // The chainHeadSub is used to synchronize the SyncService with the chain.
...@@ -207,8 +252,12 @@ func (s *SyncService) Start() error { ...@@ -207,8 +252,12 @@ func (s *SyncService) Start() error {
return nil return nil
} }
log.Info("Initializing Sync Service", "eth1-chainid", s.eth1ChainId) log.Info("Initializing Sync Service", "eth1-chainid", s.eth1ChainId)
s.updateL2GasPrice(nil) if err := s.updateGasPriceOracleCache(nil); err != nil {
s.updateL1GasPrice() return err
}
if err := s.updateL1GasPrice(); err != nil {
return err
}
if s.verifier { if s.verifier {
go s.VerifierLoop() go s.VerifierLoop()
...@@ -334,7 +383,7 @@ func (s *SyncService) VerifierLoop() { ...@@ -334,7 +383,7 @@ func (s *SyncService) VerifierLoop() {
if err := s.verify(); err != nil { if err := s.verify(); err != nil {
log.Error("Could not verify", "error", err) log.Error("Could not verify", "error", err)
} }
if err := s.updateL2GasPrice(nil); err != nil { if err := s.updateGasPriceOracleCache(nil); err != nil {
log.Error("Cannot update L2 gas price", "msg", err) log.Error("Cannot update L2 gas price", "msg", err)
} }
} }
...@@ -371,7 +420,7 @@ func (s *SyncService) SequencerLoop() { ...@@ -371,7 +420,7 @@ func (s *SyncService) SequencerLoop() {
} }
s.txLock.Unlock() s.txLock.Unlock()
if err := s.updateL2GasPrice(nil); err != nil { if err := s.updateGasPriceOracleCache(nil); err != nil {
log.Error("Cannot update L2 gas price", "msg", err) log.Error("Cannot update L2 gas price", "msg", err)
} }
if err := s.updateContext(); err != nil { if err := s.updateContext(); err != nil {
...@@ -435,25 +484,68 @@ func (s *SyncService) updateL1GasPrice() error { ...@@ -435,25 +484,68 @@ func (s *SyncService) updateL1GasPrice() error {
return nil return nil
} }
// updateL2GasPrice accepts a state root and reads the gas price from the gas // updateL2GasPrice accepts a state db and reads the gas price from the gas
// price oracle at the state that corresponds to the state root. If no state // price oracle at the state that corresponds to the state db. If no state db
// root is passed in, then the tip is used. // is passed in, then the tip is used.
func (s *SyncService) updateL2GasPrice(hash *common.Hash) error { func (s *SyncService) updateL2GasPrice(statedb *state.StateDB) error {
var state *state.StateDB var err error
if statedb == nil {
statedb, err = s.bc.State()
if err != nil {
return err
}
}
result := statedb.GetState(l2GasPriceOracleAddress, l2GasPriceSlot)
s.RollupGpo.SetL2GasPrice(result.Big())
return nil
}
// cacheGasPriceOracleOwner accepts a statedb and caches the gas price oracle
// owner address locally
func (s *SyncService) cacheGasPriceOracleOwner(statedb *state.StateDB) error {
var err error
if statedb == nil {
statedb, err = s.bc.State()
if err != nil {
return err
}
}
s.gasPriceOracleOwnerAddressLock.Lock()
defer s.gasPriceOracleOwnerAddressLock.Unlock()
result := statedb.GetState(l2GasPriceOracleAddress, l2GasPriceOracleOwnerSlot)
s.gasPriceOracleOwnerAddress = common.BytesToAddress(result.Bytes())
return nil
}
// updateGasPriceOracleCache caches the owner as well as updating the
// the L2 gas price from the OVM_GasPriceOracle
func (s *SyncService) updateGasPriceOracleCache(hash *common.Hash) error {
var statedb *state.StateDB
var err error var err error
if hash != nil { if hash != nil {
state, err = s.bc.StateAt(*hash) statedb, err = s.bc.StateAt(*hash)
} else { } else {
state, err = s.bc.State() statedb, err = s.bc.State()
} }
if err != nil { if err != nil {
return err return err
} }
result := state.GetState(l2GasPriceOracleAddress, l2GasPriceSlot) if err := s.cacheGasPriceOracleOwner(statedb); err != nil {
s.RollupGpo.SetL2GasPrice(result.Big()) return err
}
if err := s.updateL2GasPrice(statedb); err != nil {
return err
}
return nil return nil
} }
// A thread safe getter for the gas price oracle owner address
func (s *SyncService) GasPriceOracleOwnerAddress() *common.Address {
s.gasPriceOracleOwnerAddressLock.RLock()
defer s.gasPriceOracleOwnerAddressLock.RUnlock()
return &s.gasPriceOracleOwnerAddress
}
/// Update the execution context's timestamp and blocknumber /// Update the execution context's timestamp and blocknumber
/// over time. This is only necessary for the sequencer. /// over time. This is only necessary for the sequencer.
func (s *SyncService) updateContext() error { func (s *SyncService) updateContext() error {
...@@ -728,9 +820,21 @@ func (s *SyncService) applyBatchedTransaction(tx *types.Transaction) error { ...@@ -728,9 +820,21 @@ func (s *SyncService) applyBatchedTransaction(tx *types.Transaction) error {
// verifyFee will verify that a valid fee is being paid. // verifyFee will verify that a valid fee is being paid.
func (s *SyncService) verifyFee(tx *types.Transaction) error { func (s *SyncService) verifyFee(tx *types.Transaction) error {
if tx.GasPrice().Cmp(common.Big0) == 0 { if tx.GasPrice().Cmp(common.Big0) == 0 {
// Allow 0 gas price transactions only if it is the owner of the gas
// price oracle
gpoOwner := s.GasPriceOracleOwnerAddress()
if gpoOwner != nil {
from, err := types.Sender(s.signer, tx)
if err != nil {
return fmt.Errorf("invalid transaction: %w", core.ErrInvalidSender)
}
if from == *gpoOwner {
return nil
}
}
// Exit early if fees are enforced and the gasPrice is set to 0 // Exit early if fees are enforced and the gasPrice is set to 0
if s.enforceFees { if s.enforceFees {
return errors.New("cannot accept 0 gas price transaction") return errZeroGasPriceTx
} }
// If fees are not enforced and the gas price is 0, return early // If fees are not enforced and the gas price is 0, return early
return nil return nil
...@@ -750,28 +854,36 @@ func (s *SyncService) verifyFee(tx *types.Transaction) error { ...@@ -750,28 +854,36 @@ func (s *SyncService) verifyFee(tx *types.Transaction) error {
// Calculate the fee based on decoded L2 gas limit // Calculate the fee based on decoded L2 gas limit
gas := new(big.Int).SetUint64(tx.Gas()) gas := new(big.Int).SetUint64(tx.Gas())
l2GasLimit := fees.DecodeL2GasLimit(gas) l2GasLimit := fees.DecodeL2GasLimit(gas)
// Only count the calldata here as the overhead of the fully encoded // Only count the calldata here as the overhead of the fully encoded
// RLP transaction is handled inside of EncodeL2GasLimit // RLP transaction is handled inside of EncodeL2GasLimit
fee := fees.EncodeTxGasLimit(tx.Data(), l1GasPrice, l2GasLimit, l2GasPrice) expectedTxGasLimit := fees.EncodeTxGasLimit(tx.Data(), l1GasPrice, l2GasLimit, l2GasPrice)
if err != nil { if err != nil {
return err return err
} }
// This should only happen if the transaction fee is greater than 18.44 ETH
if !fee.IsUint64() { // This should only happen if the unscaled transaction fee is greater than 18.44 ETH
return fmt.Errorf("fee overflow: %s", fee.String()) if !expectedTxGasLimit.IsUint64() {
return fmt.Errorf("fee overflow: %s", expectedTxGasLimit.String())
}
userFee := new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice())
opts := fees.PaysEnoughOpts{
UserFee: userFee,
ExpectedFee: expectedTxGasLimit.Mul(expectedTxGasLimit, fees.BigTxGasPrice),
ThresholdUp: s.feeThresholdUp,
ThresholdDown: s.feeThresholdDown,
} }
// Compute the user's fee // Check the error type and return the correct error message to the user
paying := new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice()) if err := fees.PaysEnough(&opts); err != nil {
// Compute the minimum expected fee if errors.Is(err, fees.ErrFeeTooLow) {
expecting := new(big.Int).Mul(fee, fees.BigTxGasPrice) return fmt.Errorf("%w: %d, use at least tx.gasLimit = %d and tx.gasPrice = %d",
if paying.Cmp(expecting) == -1 { fees.ErrFeeTooLow, userFee, expectedTxGasLimit, fees.BigTxGasPrice)
return fmt.Errorf("fee too low: %d, use at least tx.gasLimit = %d and tx.gasPrice = %d", paying, fee.Uint64(), fees.BigTxGasPrice)
} }
// Protect users from overpaying by too much if errors.Is(err, fees.ErrFeeTooHigh) {
overpaying := new(big.Int).Sub(paying, expecting) return fmt.Errorf("%w: %d", fees.ErrFeeTooHigh, userFee)
threshold := new(big.Int).Mul(expecting, common.Big3) }
if overpaying.Cmp(threshold) == 1 { return err
return fmt.Errorf("fee too large: %d", paying)
} }
return nil return nil
} }
...@@ -797,8 +909,7 @@ func (s *SyncService) ValidateAndApplySequencerTransaction(tx *types.Transaction ...@@ -797,8 +909,7 @@ func (s *SyncService) ValidateAndApplySequencerTransaction(tx *types.Transaction
if qo != types.QueueOriginSequencer { if qo != types.QueueOriginSequencer {
return fmt.Errorf("invalid transaction with queue origin %d", qo) return fmt.Errorf("invalid transaction with queue origin %d", qo)
} }
err := s.txpool.ValidateTx(tx) if err := s.txpool.ValidateTx(tx); err != nil {
if err != nil {
return fmt.Errorf("invalid transaction: %w", err) return fmt.Errorf("invalid transaction: %w", err)
} }
return s.applyTransaction(tx) return s.applyTransaction(tx)
......
...@@ -17,7 +17,9 @@ import ( ...@@ -17,7 +17,9 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
...@@ -532,9 +534,9 @@ func TestSyncServiceL2GasPrice(t *testing.T) { ...@@ -532,9 +534,9 @@ func TestSyncServiceL2GasPrice(t *testing.T) {
} }
l2GasPrice := big.NewInt(100000000000) l2GasPrice := big.NewInt(100000000000)
state.SetState(l2GasPriceOracleAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice)) state.SetState(l2GasPriceOracleAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice))
root, _ := state.Commit(false) _, _ = state.Commit(false)
service.updateL2GasPrice(&root) service.updateL2GasPrice(state)
post, err := service.RollupGpo.SuggestL2GasPrice(context.Background()) post, err := service.RollupGpo.SuggestL2GasPrice(context.Background())
if err != nil { if err != nil {
...@@ -546,6 +548,95 @@ func TestSyncServiceL2GasPrice(t *testing.T) { ...@@ -546,6 +548,95 @@ func TestSyncServiceL2GasPrice(t *testing.T) {
} }
} }
func TestSyncServiceGasPriceOracleOwnerAddress(t *testing.T) {
service, _, _, err := newTestSyncService(true)
if err != nil {
t.Fatal(err)
}
// newTestSyncService doesn't set the initial owner address
// so it initializes to the zero value
owner := service.GasPriceOracleOwnerAddress()
if *owner != (common.Address{}) {
t.Fatal("address not initialized to 0")
}
state, err := service.bc.State()
if err != nil {
t.Fatal("cannot get state db")
}
// Update the owner in the state to a non zero address
updatedOwner := common.HexToAddress("0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8")
state.SetState(l2GasPriceOracleAddress, l2GasPriceOracleOwnerSlot, updatedOwner.Hash())
hash, _ := state.Commit(false)
// Update the cache based on the latest state root
if err := service.updateGasPriceOracleCache(&hash); err != nil {
t.Fatal(err)
}
got := service.GasPriceOracleOwnerAddress()
if *got != updatedOwner {
t.Fatalf("mismatch:\ngot %s\nexpected %s", got.Hex(), updatedOwner.Hex())
}
}
// Only the gas price oracle owner can send 0 gas price txs
// when fees are enforced
func TestFeeGasPriceOracleOwnerTransactions(t *testing.T) {
service, _, _, err := newTestSyncService(true)
if err != nil {
t.Fatal(err)
}
signer := types.NewEIP155Signer(big.NewInt(420))
// Fees must be enforced for this test
service.enforceFees = true
// Generate a key
key, _ := crypto.GenerateKey()
owner := crypto.PubkeyToAddress(key.PublicKey)
// Set as the owner on the SyncService
service.gasPriceOracleOwnerAddress = owner
if owner != *service.GasPriceOracleOwnerAddress() {
t.Fatal("owner mismatch")
}
// Create a mock transaction and sign using the
// owner's key
tx := mockTx()
// Make sure the gas price is 0 on the dummy tx
if tx.GasPrice().Cmp(common.Big0) != 0 {
t.Fatal("gas price not 0")
}
// Sign the dummy tx with the owner key
signedTx, err := types.SignTx(tx, signer, key)
if err != nil {
t.Fatal(err)
}
// Verify the fee of the signed tx, ensure it does not error
if err := service.verifyFee(signedTx); err != nil {
t.Fatal(err)
}
// Generate a new random key that is not the owner
badKey, _ := crypto.GenerateKey()
// Ensure that it is not the owner
if owner == crypto.PubkeyToAddress(badKey.PublicKey) {
t.Fatal("key mismatch")
}
// Sign the transaction with the bad key
badSignedTx, err := types.SignTx(tx, signer, badKey)
if err != nil {
t.Fatal(err)
}
// Attempt to verify the fee of the bad tx
// It should error and be a errZeroGasPriceTx
if err := service.verifyFee(badSignedTx); err != nil {
if !errors.Is(errZeroGasPriceTx, err) {
t.Fatal(err)
}
} else {
t.Fatal("err is nil")
}
}
// Pass true to set as a verifier // Pass true to set as a verifier
func TestSyncServiceSync(t *testing.T) { func TestSyncServiceSync(t *testing.T) {
service, txCh, sub, err := newTestSyncService(true) service, txCh, sub, err := newTestSyncService(true)
...@@ -664,7 +755,54 @@ func TestInitializeL1ContextPostGenesis(t *testing.T) { ...@@ -664,7 +755,54 @@ func TestInitializeL1ContextPostGenesis(t *testing.T) {
} }
} }
func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, event.Subscription, error) { func TestBadFeeThresholds(t *testing.T) {
// Create the deps for the sync service
cfg, txPool, chain, db, err := newTestSyncServiceDeps(false)
if err != nil {
t.Fatal(err)
}
tests := map[string]struct {
thresholdUp *big.Float
thresholdDown *big.Float
err error
}{
"nil-values": {
thresholdUp: nil,
thresholdDown: nil,
err: nil,
},
"good-values": {
thresholdUp: new(big.Float).SetFloat64(2),
thresholdDown: new(big.Float).SetFloat64(0.8),
err: nil,
},
"bad-value-up": {
thresholdUp: new(big.Float).SetFloat64(0.8),
thresholdDown: nil,
err: errBadConfig,
},
"bad-value-down": {
thresholdUp: nil,
thresholdDown: new(big.Float).SetFloat64(1.1),
err: errBadConfig,
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
cfg.FeeThresholdDown = tt.thresholdDown
cfg.FeeThresholdUp = tt.thresholdUp
_, err := NewSyncService(context.Background(), cfg, txPool, chain, db)
if !errors.Is(err, tt.err) {
t.Fatalf("%s: %s", name, err)
}
})
}
}
func newTestSyncServiceDeps(isVerifier bool) (Config, *core.TxPool, *core.BlockChain, ethdb.Database, error) {
chainCfg := params.AllEthashProtocolChanges chainCfg := params.AllEthashProtocolChanges
chainID := big.NewInt(420) chainID := big.NewInt(420)
chainCfg.ChainID = chainID chainCfg.ChainID = chainID
...@@ -674,7 +812,7 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e ...@@ -674,7 +812,7 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e
_ = new(core.Genesis).MustCommit(db) _ = new(core.Genesis).MustCommit(db)
chain, err := core.NewBlockChain(db, nil, chainCfg, engine, vm.Config{}, nil) chain, err := core.NewBlockChain(db, nil, chainCfg, engine, vm.Config{}, nil)
if err != nil { if err != nil {
return nil, nil, nil, fmt.Errorf("Cannot initialize blockchain: %w", err) return Config{}, nil, nil, nil, fmt.Errorf("Cannot initialize blockchain: %w", err)
} }
chaincfg := params.ChainConfig{ChainID: chainID} chaincfg := params.ChainConfig{ChainID: chainID}
...@@ -687,7 +825,14 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e ...@@ -687,7 +825,14 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e
RollupClientHttp: "", RollupClientHttp: "",
Backend: BackendL2, Backend: BackendL2,
} }
return cfg, txPool, chain, db, nil
}
func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, event.Subscription, error) {
cfg, txPool, chain, db, err := newTestSyncServiceDeps(isVerifier)
if err != nil {
return nil, nil, nil, fmt.Errorf("Cannot initialize syncservice: %w", err)
}
service, err := NewSyncService(context.Background(), cfg, txPool, chain, db) service, err := NewSyncService(context.Background(), cfg, txPool, chain, db)
if err != nil { if err != nil {
return nil, nil, nil, fmt.Errorf("Cannot initialize syncservice: %w", err) return nil, nil, nil, fmt.Errorf("Cannot initialize syncservice: %w", err)
......
# Changelog # Changelog
## 0.4.3
### Patch Changes
- 694cf429: Add a hardhat task for setting the L2 gas price
## 0.4.2 ## 0.4.2
### Patch Changes ### Patch Changes
......
{ {
"name": "@eth-optimism/contracts", "name": "@eth-optimism/contracts",
"version": "0.4.2", "version": "0.4.3",
"main": "dist/index", "main": "dist/index",
"files": [ "files": [
"dist/**/*.js", "dist/**/*.js",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment