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",
"version": "0.0.1",
"version": "0.0.2",
"private": true,
"devDependencies": {}
}
# @eth-optimism/integration-tests
## 0.2.1
### Patch Changes
- f1dc8b77: Add various stress tests
## 0.2.0
### Minor Changes
......
{
"name": "@eth-optimism/integration-tests",
"version": "0.2.0",
"version": "0.2.1",
"description": "[Optimism] Integration Tests",
"private": true,
"author": "Optimism PBC",
......@@ -18,7 +18,7 @@
"clean": "rimraf cache artifacts artifacts-ovm cache-ovm"
},
"devDependencies": {
"@eth-optimism/contracts": "^0.4.2",
"@eth-optimism/contracts": "^0.4.3",
"@eth-optimism/core-utils": "^0.5.0",
"@eth-optimism/hardhat-ovm": "^0.2.2",
"@eth-optimism/message-relayer": "^0.1.6",
......
......@@ -166,6 +166,8 @@ var (
utils.RollupMaxCalldataSizeFlag,
utils.RollupBackendFlag,
utils.RollupEnforceFeesFlag,
utils.RollupFeeThresholdDownFlag,
utils.RollupFeeThresholdUpFlag,
utils.GasPriceOracleOwnerAddress,
}
......
......@@ -81,6 +81,8 @@ var AppHelpFlagGroups = []flagGroup{
utils.RollupMaxCalldataSizeFlag,
utils.RollupBackendFlag,
utils.RollupEnforceFeesFlag,
utils.RollupFeeThresholdDownFlag,
utils.RollupFeeThresholdUpFlag,
utils.GasPriceOracleOwnerAddress,
},
},
......
......@@ -898,6 +898,16 @@ var (
Usage: "Disable transactions with 0 gas price",
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{
Name: "rollup.gaspriceoracleowneraddress",
Usage: "Owner of the OVM_GasPriceOracle",
......@@ -1196,6 +1206,14 @@ func setRollup(ctx *cli.Context, cfg *rollup.Config) {
if ctx.GlobalIsSet(RollupEnforceFeesFlag.Name) {
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.
......
......@@ -39,4 +39,9 @@ type Config struct {
Backend Backend
// Only accept transactions with fees
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
import (
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"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
// transaction in gas.
const overhead uint64 = 4200 + 200*params.TxDataNonZeroGasEIP2028
......@@ -87,6 +99,52 @@ func DecodeL2GasLimitU64(gasLimit uint64) uint64 {
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
// constant sized overhead. The overhead can be decreased as the cost of the
// batch submission goes down via contract optimizations. This will not overflow
......
package fees
import (
"errors"
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
)
......@@ -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 (
"github.com/ethereum/go-ethereum/rollup/fees"
)
// errShortRemoteTip is an error for when the remote tip is shorter than the
// local tip
var errShortRemoteTip = errors.New("Unexpected remote less than tip")
var (
// errBadConfig is the error when the SyncService is started with invalid
// 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
// in the L2 predeploy contract, the GasPriceOracle
var l2GasPriceSlot = common.BigToHash(big.NewInt(1))
var l2GasPriceOracleAddress = common.HexToAddress("0x420000000000000000000000000000000000000F")
var (
// l2GasPriceSlot refers to the storage slot that the L2 gas price is stored
// in in the OVM_GasPriceOracle predeploy
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
// and executing them. It can be configured to run in both sequencer mode and in
// verifier mode.
type SyncService struct {
ctx context.Context
cancel context.CancelFunc
verifier bool
db ethdb.Database
scope event.SubscriptionScope
txFeed event.Feed
txLock sync.Mutex
loopLock sync.Mutex
enable bool
eth1ChainId uint64
bc *core.BlockChain
txpool *core.TxPool
RollupGpo *gasprice.RollupOracle
client RollupClient
syncing atomic.Value
chainHeadSub event.Subscription
OVMContext OVMContext
pollInterval time.Duration
timestampRefreshThreshold time.Duration
chainHeadCh chan core.ChainHeadEvent
backend Backend
enforceFees bool
ctx context.Context
cancel context.CancelFunc
verifier bool
db ethdb.Database
scope event.SubscriptionScope
txFeed event.Feed
txLock sync.Mutex
loopLock sync.Mutex
enable bool
eth1ChainId uint64
bc *core.BlockChain
txpool *core.TxPool
RollupGpo *gasprice.RollupOracle
client RollupClient
syncing atomic.Value
chainHeadSub event.Subscription
OVMContext OVMContext
pollInterval time.Duration
timestampRefreshThreshold time.Duration
chainHeadCh chan core.ChainHeadEvent
backend Backend
gasPriceOracleOwnerAddress common.Address
gasPriceOracleOwnerAddressLock *sync.RWMutex
enforceFees bool
signer types.Signer
feeThresholdUp *big.Float
feeThresholdDown *big.Float
}
// NewSyncService returns an initialized sync service
......@@ -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())
} else {
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
......@@ -95,23 +119,44 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co
// Initialize the rollup client
client := NewClient(cfg.RollupClientHttp, chainID)
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{
ctx: ctx,
cancel: cancel,
verifier: cfg.IsVerifier,
enable: cfg.Eth1SyncServiceEnable,
syncing: atomic.Value{},
bc: bc,
txpool: txpool,
chainHeadCh: make(chan core.ChainHeadEvent, 1),
eth1ChainId: cfg.Eth1ChainId,
client: client,
db: db,
pollInterval: pollInterval,
timestampRefreshThreshold: timestampRefreshThreshold,
backend: cfg.Backend,
enforceFees: cfg.EnforceFees,
ctx: ctx,
cancel: cancel,
verifier: cfg.IsVerifier,
enable: cfg.Eth1SyncServiceEnable,
syncing: atomic.Value{},
bc: bc,
txpool: txpool,
chainHeadCh: make(chan core.ChainHeadEvent, 1),
eth1ChainId: cfg.Eth1ChainId,
client: client,
db: db,
pollInterval: pollInterval,
timestampRefreshThreshold: timestampRefreshThreshold,
backend: cfg.Backend,
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.
......@@ -207,8 +252,12 @@ func (s *SyncService) Start() error {
return nil
}
log.Info("Initializing Sync Service", "eth1-chainid", s.eth1ChainId)
s.updateL2GasPrice(nil)
s.updateL1GasPrice()
if err := s.updateGasPriceOracleCache(nil); err != nil {
return err
}
if err := s.updateL1GasPrice(); err != nil {
return err
}
if s.verifier {
go s.VerifierLoop()
......@@ -334,7 +383,7 @@ func (s *SyncService) VerifierLoop() {
if err := s.verify(); err != nil {
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)
}
}
......@@ -371,7 +420,7 @@ func (s *SyncService) SequencerLoop() {
}
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)
}
if err := s.updateContext(); err != nil {
......@@ -435,25 +484,68 @@ func (s *SyncService) updateL1GasPrice() error {
return nil
}
// updateL2GasPrice accepts a state root and reads the gas price from the gas
// price oracle at the state that corresponds to the state root. If no state
// root is passed in, then the tip is used.
func (s *SyncService) updateL2GasPrice(hash *common.Hash) error {
var state *state.StateDB
// updateL2GasPrice accepts a state db and reads the gas price from the gas
// price oracle at the state that corresponds to the state db. If no state db
// is passed in, then the tip is used.
func (s *SyncService) updateL2GasPrice(statedb *state.StateDB) error {
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
if hash != nil {
state, err = s.bc.StateAt(*hash)
statedb, err = s.bc.StateAt(*hash)
} else {
state, err = s.bc.State()
statedb, err = s.bc.State()
}
if err != nil {
return err
}
result := state.GetState(l2GasPriceOracleAddress, l2GasPriceSlot)
s.RollupGpo.SetL2GasPrice(result.Big())
if err := s.cacheGasPriceOracleOwner(statedb); err != nil {
return err
}
if err := s.updateL2GasPrice(statedb); err != nil {
return err
}
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
/// over time. This is only necessary for the sequencer.
func (s *SyncService) updateContext() error {
......@@ -728,9 +820,21 @@ func (s *SyncService) applyBatchedTransaction(tx *types.Transaction) error {
// verifyFee will verify that a valid fee is being paid.
func (s *SyncService) verifyFee(tx *types.Transaction) error {
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
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
return nil
......@@ -750,28 +854,36 @@ func (s *SyncService) verifyFee(tx *types.Transaction) error {
// Calculate the fee based on decoded L2 gas limit
gas := new(big.Int).SetUint64(tx.Gas())
l2GasLimit := fees.DecodeL2GasLimit(gas)
// Only count the calldata here as the overhead of the fully encoded
// 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 {
return err
}
// This should only happen if the transaction fee is greater than 18.44 ETH
if !fee.IsUint64() {
return fmt.Errorf("fee overflow: %s", fee.String())
// This should only happen if the unscaled transaction fee is greater than 18.44 ETH
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())
// Compute the minimum expected fee
expecting := new(big.Int).Mul(fee, fees.BigTxGasPrice)
if paying.Cmp(expecting) == -1 {
return fmt.Errorf("fee too low: %d, use at least tx.gasLimit = %d and tx.gasPrice = %d", paying, fee.Uint64(), fees.BigTxGasPrice)
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,
}
// Protect users from overpaying by too much
overpaying := new(big.Int).Sub(paying, expecting)
threshold := new(big.Int).Mul(expecting, common.Big3)
if overpaying.Cmp(threshold) == 1 {
return fmt.Errorf("fee too large: %d", paying)
// Check the error type and return the correct error message to the user
if err := fees.PaysEnough(&opts); err != nil {
if errors.Is(err, fees.ErrFeeTooLow) {
return fmt.Errorf("%w: %d, use at least tx.gasLimit = %d and tx.gasPrice = %d",
fees.ErrFeeTooLow, userFee, expectedTxGasLimit, fees.BigTxGasPrice)
}
if errors.Is(err, fees.ErrFeeTooHigh) {
return fmt.Errorf("%w: %d", fees.ErrFeeTooHigh, userFee)
}
return err
}
return nil
}
......@@ -797,8 +909,7 @@ func (s *SyncService) ValidateAndApplySequencerTransaction(tx *types.Transaction
if qo != types.QueueOriginSequencer {
return fmt.Errorf("invalid transaction with queue origin %d", qo)
}
err := s.txpool.ValidateTx(tx)
if err != nil {
if err := s.txpool.ValidateTx(tx); err != nil {
return fmt.Errorf("invalid transaction: %w", err)
}
return s.applyTransaction(tx)
......
......@@ -17,7 +17,9 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"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/eth/gasprice"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/params"
)
......@@ -532,9 +534,9 @@ func TestSyncServiceL2GasPrice(t *testing.T) {
}
l2GasPrice := big.NewInt(100000000000)
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())
if err != nil {
......@@ -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
func TestSyncServiceSync(t *testing.T) {
service, txCh, sub, err := newTestSyncService(true)
......@@ -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
chainID := big.NewInt(420)
chainCfg.ChainID = chainID
......@@ -674,7 +812,7 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e
_ = new(core.Genesis).MustCommit(db)
chain, err := core.NewBlockChain(db, nil, chainCfg, engine, vm.Config{}, 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}
......@@ -687,7 +825,14 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e
RollupClientHttp: "",
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)
if err != nil {
return nil, nil, nil, fmt.Errorf("Cannot initialize syncservice: %w", err)
......
# Changelog
## 0.4.3
### Patch Changes
- 694cf429: Add a hardhat task for setting the L2 gas price
## 0.4.2
### Patch Changes
......
{
"name": "@eth-optimism/contracts",
"version": "0.4.2",
"version": "0.4.3",
"main": "dist/index",
"files": [
"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