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,41 +24,62 @@ import ( ...@@ -24,41 +24,62 @@ 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
// verifier mode. // verifier mode.
type SyncService struct { type SyncService struct {
ctx context.Context ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
verifier bool verifier bool
db ethdb.Database db ethdb.Database
scope event.SubscriptionScope scope event.SubscriptionScope
txFeed event.Feed txFeed event.Feed
txLock sync.Mutex txLock sync.Mutex
loopLock sync.Mutex loopLock sync.Mutex
enable bool enable bool
eth1ChainId uint64 eth1ChainId uint64
bc *core.BlockChain bc *core.BlockChain
txpool *core.TxPool txpool *core.TxPool
RollupGpo *gasprice.RollupOracle RollupGpo *gasprice.RollupOracle
client RollupClient client RollupClient
syncing atomic.Value syncing atomic.Value
chainHeadSub event.Subscription chainHeadSub event.Subscription
OVMContext OVMContext OVMContext OVMContext
pollInterval time.Duration pollInterval time.Duration
timestampRefreshThreshold time.Duration timestampRefreshThreshold time.Duration
chainHeadCh chan core.ChainHeadEvent chainHeadCh chan core.ChainHeadEvent
backend Backend backend Backend
enforceFees bool gasPriceOracleOwnerAddress common.Address
gasPriceOracleOwnerAddressLock *sync.RWMutex
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,23 +119,44 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co ...@@ -95,23 +119,44 @@ 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,
verifier: cfg.IsVerifier, verifier: cfg.IsVerifier,
enable: cfg.Eth1SyncServiceEnable, enable: cfg.Eth1SyncServiceEnable,
syncing: atomic.Value{}, syncing: atomic.Value{},
bc: bc, bc: bc,
txpool: txpool, txpool: txpool,
chainHeadCh: make(chan core.ChainHeadEvent, 1), chainHeadCh: make(chan core.ChainHeadEvent, 1),
eth1ChainId: cfg.Eth1ChainId, eth1ChainId: cfg.Eth1ChainId,
client: client, client: client,
db: db, db: db,
pollInterval: pollInterval, pollInterval: pollInterval,
timestampRefreshThreshold: timestampRefreshThreshold, timestampRefreshThreshold: timestampRefreshThreshold,
backend: cfg.Backend, backend: cfg.Backend,
enforceFees: cfg.EnforceFees, gasPriceOracleOwnerAddress: cfg.GasPriceOracleOwnerAddress,
gasPriceOracleOwnerAddressLock: new(sync.RWMutex),
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())
} }
// Compute the user's fee
paying := new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice()) userFee := new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice())
// Compute the minimum expected fee opts := fees.PaysEnoughOpts{
expecting := new(big.Int).Mul(fee, fees.BigTxGasPrice) UserFee: userFee,
if paying.Cmp(expecting) == -1 { ExpectedFee: expectedTxGasLimit.Mul(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) ThresholdUp: s.feeThresholdUp,
ThresholdDown: s.feeThresholdDown,
} }
// Protect users from overpaying by too much // Check the error type and return the correct error message to the user
overpaying := new(big.Int).Sub(paying, expecting) if err := fees.PaysEnough(&opts); err != nil {
threshold := new(big.Int).Mul(expecting, common.Big3) if errors.Is(err, fees.ErrFeeTooLow) {
if overpaying.Cmp(threshold) == 1 { return fmt.Errorf("%w: %d, use at least tx.gasLimit = %d and tx.gasPrice = %d",
return fmt.Errorf("fee too large: %d", paying) fees.ErrFeeTooLow, userFee, expectedTxGasLimit, fees.BigTxGasPrice)
}
if errors.Is(err, fees.ErrFeeTooHigh) {
return fmt.Errorf("%w: %d", fees.ErrFeeTooHigh, userFee)
}
return err
} }
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