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

feat: fees v2 (#976)

* l2 geth: new fee logic

* l2 geth: migrate to fees package

* core-utils: new fee scheme

* chore: add changeset

* l2geth: delete dead code

* integration-tests: fix typo

* integration-tests: fixes

* fees: use fee scalar

* lint: fix

* rollup: correct gas payment comparison

* fix(integration-tests): do not hardcode gas price

* core-utils: update with new scheme

* l2geth: refactor rollup oracle

* l2geth: clean up DoEstimateGas

* l2geth: implement latest scheme

* tests: fix up

* lint: fix

* l2geth: better sycn service test

* optimism: rename to TxGasLimit

* fee: fix docstring

* tests: fix

* variables: rename

* l2geth: prevent users from sending txs with too high of a fee

* integration-tests: fix import

* integration-tests: fix type

* integration-tests: fix gas limits

* lint: fix

* l2geth: log error
Co-authored-by: default avatarGeorgios Konstantopoulos <me@gakonst.com>
parent 4e03f8a9
---
'@eth-optimism/l2geth': patch
'@eth-optimism/core-utils': patch
---
Implement the next fee spec in both geth and in core-utils
import { Contract, ContractFactory, Wallet } from 'ethers' import { Contract, ContractFactory, Wallet } from 'ethers'
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import { TxGasLimit, TxGasPrice } from '@eth-optimism/core-utils'
import chai, { expect } from 'chai' import chai, { expect } from 'chai'
import { GWEI } from './shared/utils' import { GWEI } from './shared/utils'
import { OptimismEnv } from './shared/env' import { OptimismEnv } from './shared/env'
...@@ -64,7 +65,10 @@ describe('Basic ERC20 interactions', async () => { ...@@ -64,7 +65,10 @@ describe('Basic ERC20 interactions', async () => {
const receipt = await transfer.wait() const receipt = await transfer.wait()
// The expected fee paid is the value returned by eth_estimateGas // The expected fee paid is the value returned by eth_estimateGas
const expectedFeePaid = await ERC20.estimateGas.transfer(other.address, 100) const gasLimit = await ERC20.estimateGas.transfer(other.address, 100)
const gasPrice = await wallet.getGasPrice()
expect(gasPrice).to.deep.equal(TxGasPrice)
const expectedFeePaid = gasLimit.mul(gasPrice)
// There are two events from the transfer with the first being // There are two events from the transfer with the first being
// the ETH fee paid and the second of the value transfered (100) // the ETH fee paid and the second of the value transfered (100)
......
...@@ -3,7 +3,7 @@ import chaiAsPromised from 'chai-as-promised' ...@@ -3,7 +3,7 @@ import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised) chai.use(chaiAsPromised)
import { BigNumber, utils } from 'ethers' import { BigNumber, utils } from 'ethers'
import { OptimismEnv } from './shared/env' import { OptimismEnv } from './shared/env'
import { L2GasLimit } from '@eth-optimism/core-utils' import { TxGasLimit } from '@eth-optimism/core-utils'
describe('Fee Payment Integration Tests', async () => { describe('Fee Payment Integration Tests', async () => {
let env: OptimismEnv let env: OptimismEnv
...@@ -29,7 +29,7 @@ describe('Fee Payment Integration Tests', async () => { ...@@ -29,7 +29,7 @@ describe('Fee Payment Integration Tests', async () => {
) )
const executionGas = await (env.ovmEth const executionGas = await (env.ovmEth
.provider as any).send('eth_estimateExecutionGas', [tx]) .provider as any).send('eth_estimateExecutionGas', [tx])
const decoded = L2GasLimit.decode(gas) const decoded = TxGasLimit.decode(gas)
expect(BigNumber.from(executionGas)).deep.eq(decoded) expect(BigNumber.from(executionGas)).deep.eq(decoded)
}) })
...@@ -38,8 +38,7 @@ describe('Fee Payment Integration Tests', async () => { ...@@ -38,8 +38,7 @@ describe('Fee Payment Integration Tests', async () => {
const balanceBefore = await env.l2Wallet.getBalance() const balanceBefore = await env.l2Wallet.getBalance()
expect(balanceBefore.gt(amount)) expect(balanceBefore.gt(amount))
const gas = await env.ovmEth.estimateGas.transfer(other, amount) const tx = await env.ovmEth.transfer(other, amount)
const tx = await env.ovmEth.transfer(other, amount, { gasPrice: 1 })
const receipt = await tx.wait() const receipt = await tx.wait()
expect(receipt.status).to.eq(1) expect(receipt.status).to.eq(1)
......
...@@ -45,13 +45,13 @@ describe('Native ETH Integration Tests', async () => { ...@@ -45,13 +45,13 @@ describe('Native ETH Integration Tests', async () => {
const amount = utils.parseEther('0.5') const amount = utils.parseEther('0.5')
const addr = '0x' + '1234'.repeat(10) const addr = '0x' + '1234'.repeat(10)
const gas = await env.ovmEth.estimateGas.transfer(addr, amount) const gas = await env.ovmEth.estimateGas.transfer(addr, amount)
expect(gas).to.be.deep.eq(BigNumber.from(0x23284d28fe6d)) expect(gas).to.be.deep.eq(BigNumber.from(0x0ef897216d))
}) })
it('Should estimate gas for ETH withdraw', async () => { it('Should estimate gas for ETH withdraw', async () => {
const amount = utils.parseEther('0.5') const amount = utils.parseEther('0.5')
const gas = await env.ovmEth.estimateGas.withdraw(amount) const gas = await env.ovmEth.estimateGas.withdraw(amount)
expect(gas).to.be.deep.eq(BigNumber.from(0x207ad91a77b4)) expect(gas).to.be.deep.eq(BigNumber.from(61400489396))
}) })
}) })
......
import { import {
injectL2Context, injectL2Context,
L2GasLimit, TxGasLimit,
roundL1GasPrice, TxGasPrice,
toRpcHexString,
} from '@eth-optimism/core-utils' } from '@eth-optimism/core-utils'
import { Wallet, BigNumber, Contract } from 'ethers' import { Wallet, BigNumber, Contract } from 'ethers'
import { ethers } from 'hardhat' import { ethers } from 'hardhat'
import chai, { expect } from 'chai' import chai, { expect } from 'chai'
import { sleep, l2Provider, GWEI } from './shared/utils' import { sleep, l2Provider, l1Provider } from './shared/utils'
import chaiAsPromised from 'chai-as-promised' import chaiAsPromised from 'chai-as-promised'
import { OptimismEnv } from './shared/env' import { OptimismEnv } from './shared/env'
import { import {
...@@ -130,11 +131,25 @@ describe('Basic RPC tests', () => { ...@@ -130,11 +131,25 @@ describe('Basic RPC tests', () => {
const tx = { const tx = {
...DEFAULT_TRANSACTION, ...DEFAULT_TRANSACTION,
gasLimit: 1, gasLimit: 1,
gasPrice: 1, gasPrice: TxGasPrice,
} }
const fee = tx.gasPrice.mul(tx.gasLimit)
const gasLimit = 59300000001
await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith( await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
'fee too low: 1, use at least tx.gasLimit = 33600000000001 and tx.gasPrice = 1' `fee too low: ${fee}, use at least tx.gasLimit = ${gasLimit} and tx.gasPrice = ${TxGasPrice.toString()}`
)
})
it('should reject a transaction with an incorrect gas price', async () => {
const tx = {
...DEFAULT_TRANSACTION,
gasLimit: 1,
gasPrice: TxGasPrice.sub(1),
}
await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
`tx.gasPrice must be ${TxGasPrice.toString()}`
) )
}) })
...@@ -198,7 +213,7 @@ describe('Basic RPC tests', () => { ...@@ -198,7 +213,7 @@ describe('Basic RPC tests', () => {
it('correctly exposes revert data for contract calls', async () => { it('correctly exposes revert data for contract calls', async () => {
const req: TransactionRequest = { const req: TransactionRequest = {
...revertingTx, ...revertingTx,
gasLimit: 934111908999999, // override gas estimation gasLimit: 59808999999, // override gas estimation
} }
const tx = await wallet.sendTransaction(req) const tx = await wallet.sendTransaction(req)
...@@ -221,7 +236,7 @@ describe('Basic RPC tests', () => { ...@@ -221,7 +236,7 @@ describe('Basic RPC tests', () => {
it('correctly exposes revert data for contract creations', async () => { it('correctly exposes revert data for contract creations', async () => {
const req: TransactionRequest = { const req: TransactionRequest = {
...revertingDeployTx, ...revertingDeployTx,
gasLimit: 1051391908999999, // override gas estimation gasLimit: 177008999999, // override gas estimation
} }
const tx = await wallet.sendTransaction(req) const tx = await wallet.sendTransaction(req)
...@@ -311,12 +326,14 @@ describe('Basic RPC tests', () => { ...@@ -311,12 +326,14 @@ describe('Basic RPC tests', () => {
}) })
describe('eth_gasPrice', () => { describe('eth_gasPrice', () => {
it('gas price should be 1 gwei', async () => { it('gas price should be the fee scalar', async () => {
expect(await provider.getGasPrice()).to.be.deep.equal(1) expect(await provider.getGasPrice()).to.be.deep.equal(
TxGasPrice.toNumber()
)
}) })
}) })
describe('eth_estimateGas (returns the fee)', () => { describe('eth_estimateGas (returns the scaled fee)', () => {
it('gas estimation is deterministic', async () => { it('gas estimation is deterministic', async () => {
let lastEstimate: BigNumber let lastEstimate: BigNumber
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
...@@ -338,7 +355,7 @@ describe('Basic RPC tests', () => { ...@@ -338,7 +355,7 @@ describe('Basic RPC tests', () => {
to: DEFAULT_TRANSACTION.to, to: DEFAULT_TRANSACTION.to,
value: 0, value: 0,
}) })
expect(estimate).to.be.eq(33600000119751) expect(estimate).to.be.eq(0x0dce9004c7)
}) })
it('should return a gas estimate that grows with the size of data', async () => { it('should return a gas estimate that grows with the size of data', async () => {
...@@ -349,7 +366,6 @@ describe('Basic RPC tests', () => { ...@@ -349,7 +366,6 @@ describe('Basic RPC tests', () => {
for (const data of dataLen) { for (const data of dataLen) {
const tx = { const tx = {
to: '0x' + '1234'.repeat(10), to: '0x' + '1234'.repeat(10),
gasPrice: '0x1',
value: '0x0', value: '0x0',
data: '0x' + '00'.repeat(data), data: '0x' + '00'.repeat(data),
from: '0x' + '1234'.repeat(10), from: '0x' + '1234'.repeat(10),
...@@ -359,16 +375,16 @@ describe('Basic RPC tests', () => { ...@@ -359,16 +375,16 @@ describe('Basic RPC tests', () => {
tx, tx,
]) ])
const decoded = L2GasLimit.decode(estimate) const decoded = TxGasLimit.decode(estimate)
expect(decoded).to.deep.eq(BigNumber.from(l2Gaslimit)) expect(decoded).to.deep.eq(BigNumber.from(l2Gaslimit))
expect(estimate.toString().endsWith(l2Gaslimit.toString())) expect(estimate.toString().endsWith(l2Gaslimit.toString()))
const l2GasPrice = BigNumber.from(0)
// The L2GasPrice should be fetched from the L2GasPrice oracle contract, // The L2GasPrice should be fetched from the L2GasPrice oracle contract,
// but it does not yet exist. Use the default value for now // but it does not yet exist. Use the default value for now
const l2GasPrice = BigNumber.from(1) const expected = TxGasLimit.encode({
const expected = L2GasLimit.encode({
data: tx.data, data: tx.data,
l1GasPrice: roundL1GasPrice(l1GasPrice), l1GasPrice,
l2GasLimit: BigNumber.from(l2Gaslimit), l2GasLimit: BigNumber.from(l2Gaslimit),
l2GasPrice, l2GasPrice,
}) })
......
package core
import (
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params"
)
// overhead represents the fixed cost of batch submission of a single
// transaction in gas
const overhead uint64 = 4200
// hundredMillion is a constant used in the gas encoding formula
const hundredMillion uint64 = 100_000_000
var bigHundredMillion = new(big.Int).SetUint64(hundredMillion)
// errInvalidGasPrice is the error returned when a user submits an incorrect gas
// price. The gas price must satisfy a particular equation depending on if it
// is a L1 gas price or a L2 gas price
var errInvalidGasPrice = errors.New("rollup fee: invalid gas price")
// CalculateFee calculates the fee that must be paid to the Rollup sequencer, taking into
// account the cost of publishing data to L1.
// l2_gas_price * l2_gas_limit + l1_gas_price * l1_gas_used
// where the l2 gas price must satisfy the equation `x % (10**8) = 1`
// and the l1 gas price must satisfy the equation `x % (10**8) = 0`
func CalculateRollupFee(data []byte, l1GasPrice, l2GasLimit, l2GasPrice *big.Int) (*big.Int, error) {
if err := VerifyL1GasPrice(l1GasPrice); err != nil {
return nil, fmt.Errorf("invalid L1 gas price %d: %w", l1GasPrice, err)
}
if err := VerifyL2GasPrice(l2GasPrice); err != nil {
return nil, fmt.Errorf("invalid L2 gas price %d: %w", l2GasPrice, err)
}
l1GasLimit := calculateL1GasLimit(data, overhead)
l1Fee := new(big.Int).Mul(l1GasPrice, l1GasLimit)
l2Fee := new(big.Int).Mul(l2GasLimit, l2GasPrice)
fee := new(big.Int).Add(l1Fee, l2Fee)
return fee, nil
}
// 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
// under standard network conditions.
func calculateL1GasLimit(data []byte, overhead uint64) *big.Int {
zeroes, ones := zeroesAndOnes(data)
zeroesCost := zeroes * params.TxDataZeroGas
onesCost := ones * params.TxDataNonZeroGasEIP2028
gasLimit := zeroesCost + onesCost + overhead
return new(big.Int).SetUint64(gasLimit)
}
// ceilModOneHundredMillion rounds the input integer up to the nearest modulus
// of one hundred million
func ceilModOneHundredMillion(num *big.Int) *big.Int {
if new(big.Int).Mod(num, bigHundredMillion).Cmp(common.Big0) == 0 {
return num
}
sum := new(big.Int).Add(num, bigHundredMillion)
mod := new(big.Int).Mod(num, bigHundredMillion)
return new(big.Int).Sub(sum, mod)
}
// VerifyL1GasPrice returns an error if the number is an invalid possible L1 gas
// price
func VerifyL1GasPrice(l1GasPrice *big.Int) error {
if new(big.Int).Mod(l1GasPrice, bigHundredMillion).Cmp(common.Big0) != 0 {
return errInvalidGasPrice
}
return nil
}
// VerifyL2GasPrice returns an error if the number is an invalid possible L2 gas
// price
func VerifyL2GasPrice(l2GasPrice *big.Int) error {
isNonZero := l2GasPrice.Cmp(common.Big0) != 0
isNotModHundredMillion := new(big.Int).Mod(l2GasPrice, bigHundredMillion).Cmp(common.Big1) != 0
if isNonZero && isNotModHundredMillion {
return errInvalidGasPrice
}
if l2GasPrice.Cmp(common.Big0) == 0 {
return errInvalidGasPrice
}
return nil
}
// RoundL1GasPrice returns a ceilModOneHundredMillion where 0
// is the identity function
func RoundL1GasPrice(gasPrice *big.Int) *big.Int {
return ceilModOneHundredMillion(gasPrice)
}
// RoundL2GasPriceBig implements the algorithm:
// if gasPrice is 0; return 1
// if gasPrice is 1; return 10**8 + 1
// return ceilModOneHundredMillion(gasPrice-1)+1
func RoundL2GasPrice(gasPrice *big.Int) *big.Int {
if gasPrice.Cmp(common.Big0) == 0 {
return big.NewInt(1)
}
if gasPrice.Cmp(common.Big1) == 0 {
return new(big.Int).Add(bigHundredMillion, common.Big1)
}
gp := new(big.Int).Sub(gasPrice, common.Big1)
mod := ceilModOneHundredMillion(gp)
return new(big.Int).Add(mod, common.Big1)
}
func DecodeL2GasLimit(gasLimit *big.Int) *big.Int {
return new(big.Int).Mod(gasLimit, bigHundredMillion)
}
func zeroesAndOnes(data []byte) (uint64, uint64) {
var zeroes uint64
for _, byt := range data {
if byt == 0 {
zeroes++
}
}
ones := uint64(len(data)) - zeroes
return zeroes, ones
}
...@@ -5,62 +5,55 @@ import ( ...@@ -5,62 +5,55 @@ import (
"math/big" "math/big"
"sync" "sync"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
) )
// RollupOracle holds the L1 and L2 gas prices for fee calculation // RollupOracle holds the L1 and L2 gas prices for fee calculation
type RollupOracle struct { type RollupOracle struct {
dataPrice *big.Int l1GasPrice *big.Int
executionPrice *big.Int l2GasPrice *big.Int
dataPriceLock sync.RWMutex l1GasPriceLock sync.RWMutex
executionPriceLock sync.RWMutex l2GasPriceLock sync.RWMutex
} }
// NewRollupOracle returns an initialized RollupOracle // NewRollupOracle returns an initialized RollupOracle
func NewRollupOracle(dataPrice *big.Int, executionPrice *big.Int) *RollupOracle { func NewRollupOracle(l1GasPrice *big.Int, l2GasPrice *big.Int) *RollupOracle {
return &RollupOracle{ return &RollupOracle{
dataPrice: dataPrice, l1GasPrice: l1GasPrice,
executionPrice: executionPrice, l2GasPrice: l2GasPrice,
} }
} }
// SuggestL1GasPrice returns the gas price which should be charged per byte of published // SuggestL1GasPrice returns the gas price which should be charged per byte of published
// data by the sequencer. // data by the sequencer.
func (gpo *RollupOracle) SuggestL1GasPrice(ctx context.Context) (*big.Int, error) { func (gpo *RollupOracle) SuggestL1GasPrice(ctx context.Context) (*big.Int, error) {
gpo.dataPriceLock.RLock() gpo.l1GasPriceLock.RLock()
defer gpo.dataPriceLock.RUnlock() defer gpo.l1GasPriceLock.RUnlock()
return gpo.dataPrice, nil return gpo.l1GasPrice, nil
} }
// SetL1GasPrice returns the current L1 gas price // SetL1GasPrice returns the current L1 gas price
func (gpo *RollupOracle) SetL1GasPrice(dataPrice *big.Int) error { func (gpo *RollupOracle) SetL1GasPrice(gasPrice *big.Int) error {
gpo.dataPriceLock.Lock() gpo.l1GasPriceLock.Lock()
defer gpo.dataPriceLock.Unlock() defer gpo.l1GasPriceLock.Unlock()
if err := core.VerifyL1GasPrice(dataPrice); err != nil { gpo.l1GasPrice = gasPrice
return err log.Info("Set L1 Gas Price", "gasprice", gpo.l1GasPrice)
}
gpo.dataPrice = dataPrice
log.Info("Set L1 Gas Price", "gasprice", gpo.dataPrice)
return nil return nil
} }
// SuggestL2GasPrice returns the gas price which should be charged per unit of gas // SuggestL2GasPrice returns the gas price which should be charged per unit of gas
// set manually by the sequencer depending on congestion // set manually by the sequencer depending on congestion
func (gpo *RollupOracle) SuggestL2GasPrice(ctx context.Context) (*big.Int, error) { func (gpo *RollupOracle) SuggestL2GasPrice(ctx context.Context) (*big.Int, error) {
gpo.executionPriceLock.RLock() gpo.l2GasPriceLock.RLock()
defer gpo.executionPriceLock.RUnlock() defer gpo.l2GasPriceLock.RUnlock()
return gpo.executionPrice, nil return gpo.l2GasPrice, nil
} }
// SetL2GasPrice returns the current L2 gas price // SetL2GasPrice returns the current L2 gas price
func (gpo *RollupOracle) SetL2GasPrice(executionPrice *big.Int) error { func (gpo *RollupOracle) SetL2GasPrice(gasPrice *big.Int) error {
gpo.executionPriceLock.Lock() gpo.l2GasPriceLock.Lock()
defer gpo.executionPriceLock.Unlock() defer gpo.l2GasPriceLock.Unlock()
if err := core.VerifyL2GasPrice(executionPrice); err != nil { gpo.l2GasPrice = gasPrice
return err log.Info("Set L2 Gas Price", "gasprice", gpo.l2GasPrice)
}
gpo.executionPrice = executionPrice
log.Info("Set L2 Gas Price", "gasprice", gpo.executionPrice)
return nil return nil
} }
...@@ -45,15 +45,17 @@ import ( ...@@ -45,15 +45,17 @@ import (
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rollup/fees"
"github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/rpc"
"github.com/tyler-smith/go-bip39" "github.com/tyler-smith/go-bip39"
) )
const ( const (
defaultGasPrice = params.Wei defaultGasPrice = params.Wei * fees.TxGasPrice
) )
var errOVMUnsupported = errors.New("OVM: Unsupported RPC Method") var errOVMUnsupported = errors.New("OVM: Unsupported RPC Method")
var bigDefaultGasPrice = new(big.Int).SetUint64(defaultGasPrice)
// PublicEthereumAPI provides an API to access Ethereum related information. // PublicEthereumAPI provides an API to access Ethereum related information.
// It offers only methods that operate on public data that is freely available to anyone. // It offers only methods that operate on public data that is freely available to anyone.
...@@ -68,7 +70,7 @@ func NewPublicEthereumAPI(b Backend) *PublicEthereumAPI { ...@@ -68,7 +70,7 @@ func NewPublicEthereumAPI(b Backend) *PublicEthereumAPI {
// GasPrice always returns 1 gwei. See `DoEstimateGas` below for context. // GasPrice always returns 1 gwei. See `DoEstimateGas` below for context.
func (s *PublicEthereumAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) { func (s *PublicEthereumAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) {
return (*hexutil.Big)(big.NewInt(defaultGasPrice)), nil return (*hexutil.Big)(bigDefaultGasPrice), nil
} }
// ProtocolVersion returns the current Ethereum protocol version this node supports // ProtocolVersion returns the current Ethereum protocol version this node supports
...@@ -1037,31 +1039,27 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash ...@@ -1037,31 +1039,27 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
if err != nil { if err != nil {
return 0, err return 0, err
} }
// 2a. fetch the data price, depends on how the sequencer has chosen to update their values based on the // 2a. fetch the data price, depends on how the sequencer has chosen to update their values based on the
// l1 gas prices // l1 gas prices
l1GasPrice, err := b.SuggestL1GasPrice(ctx) l1GasPrice, err := b.SuggestL1GasPrice(ctx)
if err != nil { if err != nil {
return 0, err return 0, err
} }
// 2b. fetch the execution gas price, by the typical mempool dynamics // 2b. fetch the execution gas price, by the typical mempool dynamics
l2GasPrice, err := b.SuggestL2GasPrice(ctx) l2GasPrice, err := b.SuggestL2GasPrice(ctx)
if err != nil { if err != nil {
return 0, err return 0, err
} }
data := []byte{}
var data []byte if args.Data != nil {
if args.Data == nil {
data = []byte{}
} else {
data = *args.Data data = *args.Data
} }
// 3. calculate the fee // 3. calculate the fee using just the calldata. The additional overhead of
// RLP encoding is covered inside of EncodeL2GasLimit
l2GasLimit := new(big.Int).SetUint64(uint64(gasUsed)) l2GasLimit := new(big.Int).SetUint64(uint64(gasUsed))
fee, err := core.CalculateRollupFee(data, l1GasPrice, l2GasLimit, l2GasPrice) fee := fees.EncodeTxGasLimit(data, l1GasPrice, l2GasLimit, l2GasPrice)
if err != nil { if !fee.IsUint64() {
return 0, err return 0, fmt.Errorf("estimate gas overflow: %s", fee)
} }
return (hexutil.Uint64)(fee.Uint64()), nil return (hexutil.Uint64)(fee.Uint64()), nil
} }
......
...@@ -570,7 +570,7 @@ func (c *Client) GetTransactionBatch(index uint64) (*Batch, []*types.Transaction ...@@ -570,7 +570,7 @@ func (c *Client) GetTransactionBatch(index uint64) (*Batch, []*types.Transaction
Get("/batch/transaction/index/{index}") Get("/batch/transaction/index/{index}")
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("Cannot get transaction batch %d", index) return nil, nil, fmt.Errorf("Cannot get transaction batch %d: %w", index, err)
} }
txBatch, ok := response.Result().(*TransactionBatchResponse) txBatch, ok := response.Result().(*TransactionBatchResponse)
if !ok { if !ok {
......
package fees
import (
"math/big"
"github.com/ethereum/go-ethereum/params"
)
// overhead represents the fixed cost of batch submission of a single
// transaction in gas.
const overhead uint64 = 4200 + 200*params.TxDataNonZeroGasEIP2028
// hundredMillion is a constant used in the gas encoding formula
const hundredMillion uint64 = 100_000_000
// feeScalar is used to scale the calculations in EncodeL2GasLimit
// to prevent them from being too large
const feeScalar uint64 = 1000
// TxGasPrice is a constant that determines the result of `eth_gasPrice`
// It is scaled upwards by 50%
// tx.gasPrice is hard coded to 1500 * wei and all transactions must set that
// gas price.
const TxGasPrice uint64 = feeScalar + (feeScalar / 2)
// BigTxGasPrice is the L2GasPrice as type big.Int
var BigTxGasPrice = new(big.Int).SetUint64(TxGasPrice)
var bigFeeScalar = new(big.Int).SetUint64(feeScalar)
var bigHundredMillion = new(big.Int).SetUint64(hundredMillion)
// EncodeTxGasLimit computes the `tx.gasLimit` based on the L1/L2 gas prices and
// the L2 gas limit. The L2 gas limit is encoded inside of the lower order bits
// of the number like so: [ | l2GasLimit ]
// [ tx.gaslimit ]
// The lower order bits must be large enough to fit the L2 gas limit, so 10**8
// is chosen. If higher order bits collide with any bits from the L2 gas limit,
// the L2 gas limit will not be able to be decoded.
// An explicit design goal of this scheme was to make the L2 gas limit be human
// readable. The entire number is interpreted as the gas limit when computing
// the fee, so increasing the L2 Gas limit will increase the fee paid.
// The calculation is:
// l1GasLimit = zero_count(data) * 4 + non_zero_count(data) * 16 + overhead
// l1Fee = l1GasPrice * l1GasLimit
// l2Fee = l2GasPrice * l2GasLimit
// sum = l1Fee + l2Fee
// scaled = sum / scalar
// rounded = ceilmod(scaled, hundredMillion)
// result = rounded + l2GasLimit
// Note that for simplicity purposes, only the calldata is passed into this
// function when in reality the RLP encoded transaction should be. The
// additional cost is added to the overhead constant to prevent the need to RLP
// encode transactions during calls to `eth_estimateGas`
func EncodeTxGasLimit(data []byte, l1GasPrice, l2GasLimit, l2GasPrice *big.Int) *big.Int {
l1GasLimit := calculateL1GasLimit(data, overhead)
l1Fee := new(big.Int).Mul(l1GasPrice, l1GasLimit)
l2Fee := new(big.Int).Mul(l2GasPrice, l2GasLimit)
sum := new(big.Int).Add(l1Fee, l2Fee)
scaled := new(big.Int).Div(sum, bigFeeScalar)
remainder := new(big.Int).Mod(scaled, bigHundredMillion)
scaledSum := new(big.Int).Add(scaled, bigHundredMillion)
rounded := new(big.Int).Sub(scaledSum, remainder)
result := new(big.Int).Add(rounded, l2GasLimit)
return result
}
// DecodeL2GasLimit decodes the L2 gas limit from an encoded L2 gas limit
func DecodeL2GasLimit(gasLimit *big.Int) *big.Int {
return new(big.Int).Mod(gasLimit, bigHundredMillion)
}
// 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
// under standard network conditions.
func calculateL1GasLimit(data []byte, overhead uint64) *big.Int {
zeroes, ones := zeroesAndOnes(data)
zeroesCost := zeroes * params.TxDataZeroGas
onesCost := ones * params.TxDataNonZeroGasEIP2028
gasLimit := zeroesCost + onesCost + overhead
return new(big.Int).SetUint64(gasLimit)
}
func zeroesAndOnes(data []byte) (uint64, uint64) {
var zeroes uint64
var ones uint64
for _, byt := range data {
if byt == 0 {
zeroes++
} else {
ones++
}
}
return zeroes, ones
}
package core package fees
import ( import (
"errors"
"math"
"math/big" "math/big"
"testing" "testing"
)
var roundingL1GasPriceTests = map[string]struct {
input uint64
expect uint64
}{
"simple": {10, pow10(8)},
"one-over": {pow10(8) + 1, 2 * pow10(8)},
"exact": {pow10(8), pow10(8)},
"one-under": {pow10(8) - 1, pow10(8)},
"small": {3, pow10(8)},
"two": {2, pow10(8)},
"one": {1, pow10(8)},
"zero": {0, 0},
}
func TestRoundL1GasPrice(t *testing.T) {
for name, tt := range roundingL1GasPriceTests {
t.Run(name, func(t *testing.T) {
got := RoundL1GasPrice(new(big.Int).SetUint64(tt.input))
if got.Uint64() != tt.expect {
t.Fatalf("Mismatched rounding to nearest, got %d expected %d", got, tt.expect)
}
})
}
}
var roundingL2GasPriceTests = map[string]struct { "github.com/ethereum/go-ethereum/params"
input uint64 )
expect uint64
}{
"simple": {10, pow10(8) + 1},
"one-over": {pow10(8) + 2, 2*pow10(8) + 1},
"exact": {pow10(8) + 1, pow10(8) + 1},
"one-under": {pow10(8), pow10(8) + 1},
"small": {3, pow10(8) + 1},
"two": {2, pow10(8) + 1},
"one": {1, pow10(8) + 1},
"zero": {0, 1},
}
func TestRoundL2GasPrice(t *testing.T) {
for name, tt := range roundingL2GasPriceTests {
t.Run(name, func(t *testing.T) {
got := RoundL2GasPrice(new(big.Int).SetUint64(tt.input))
if got.Uint64() != tt.expect {
t.Fatalf("Mismatched rounding to nearest, got %d expected %d", got, tt.expect)
}
})
}
}
var l1GasLimitTests = map[string]struct { var l1GasLimitTests = map[string]struct {
data []byte data []byte
...@@ -84,38 +34,70 @@ var feeTests = map[string]struct { ...@@ -84,38 +34,70 @@ var feeTests = map[string]struct {
l1GasPrice uint64 l1GasPrice uint64
l2GasLimit uint64 l2GasLimit uint64
l2GasPrice uint64 l2GasPrice uint64
err error
}{ }{
"simple": {100, 100_000_000, 437118, 100_000_001, nil}, "simple": {
"zero-l2-gasprice": {10, 100_000_000, 196205, 0, errInvalidGasPrice}, dataLen: 10,
"one-l2-gasprice": {10, 100_000_000, 196205, 1, nil}, l1GasPrice: params.GWei,
"zero-l1-gasprice": {10, 0, 196205, 100_000_001, nil}, l2GasLimit: 437118,
"one-l1-gasprice": {10, 1, 23255, 23254, errInvalidGasPrice}, l2GasPrice: params.GWei,
},
"zero-l2-gasprice": {
dataLen: 10,
l1GasPrice: params.GWei,
l2GasLimit: 196205,
l2GasPrice: 0,
},
"one-l2-gasprice": {
dataLen: 10,
l1GasPrice: params.GWei,
l2GasLimit: 196205,
l2GasPrice: 1,
},
"zero-l1-gasprice": {
dataLen: 10,
l1GasPrice: 0,
l2GasLimit: 196205,
l2GasPrice: params.GWei,
},
"one-l1-gasprice": {
dataLen: 10,
l1GasPrice: 1,
l2GasLimit: 23255,
l2GasPrice: params.GWei,
},
"zero-gasprices": {
dataLen: 10,
l1GasPrice: 0,
l2GasLimit: 23255,
l2GasPrice: 0,
},
"max-gaslimit": {
dataLen: 10,
l1GasPrice: params.GWei,
l2GasLimit: 99999999,
l2GasPrice: params.GWei,
},
"larger-divisor": {
dataLen: 10,
l1GasPrice: 0,
l2GasLimit: 10,
l2GasPrice: 0,
},
} }
func TestCalculateRollupFee(t *testing.T) { func TestCalculateRollupFee(t *testing.T) {
for name, tt := range feeTests { for name, tt := range feeTests {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
data := make([]byte, 0, tt.dataLen) data := make([]byte, tt.dataLen)
l1GasPrice := new(big.Int).SetUint64(tt.l1GasPrice) l1GasPrice := new(big.Int).SetUint64(tt.l1GasPrice)
l2GasLimit := new(big.Int).SetUint64(tt.l2GasLimit) l2GasLimit := new(big.Int).SetUint64(tt.l2GasLimit)
l2GasPrice := new(big.Int).SetUint64(tt.l2GasPrice) l2GasPrice := new(big.Int).SetUint64(tt.l2GasPrice)
fee, err := CalculateRollupFee(data, l1GasPrice, l2GasLimit, l2GasPrice) fee := EncodeTxGasLimit(data, l1GasPrice, l2GasLimit, l2GasPrice)
if !errors.Is(err, tt.err) { decodedGasLimit := DecodeL2GasLimit(fee)
t.Fatalf("Cannot calculate fee: %s", err) if l2GasLimit.Cmp(decodedGasLimit) != 0 {
} t.Errorf("rollup fee check failed: expected %d, got %d", l2GasLimit.Uint64(), decodedGasLimit)
if err == nil {
decodedGasLimit := DecodeL2GasLimit(fee)
if l2GasLimit.Cmp(decodedGasLimit) != 0 {
t.Errorf("rollup fee check failed: expected %d, got %d", l2GasLimit.Uint64(), decodedGasLimit)
}
} }
}) })
} }
} }
func pow10(x int) uint64 {
return uint64(math.Pow10(x))
}
...@@ -21,6 +21,7 @@ import ( ...@@ -21,6 +21,7 @@ import (
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/gasprice" "github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/rollup/fees"
) )
// errShortRemoteTip is an error for when the remote tip is shorter than the // errShortRemoteTip is an error for when the remote tip is shorter than the
...@@ -445,13 +446,7 @@ func (s *SyncService) updateL2GasPrice(hash *common.Hash) error { ...@@ -445,13 +446,7 @@ func (s *SyncService) updateL2GasPrice(hash *common.Hash) error {
return err return err
} }
result := state.GetState(s.gpoAddress, l2GasPriceSlot) result := state.GetState(s.gpoAddress, l2GasPriceSlot)
gasPrice := result.Big() s.RollupGpo.SetL2GasPrice(result.Big())
if err := core.VerifyL2GasPrice(gasPrice); err != nil {
gp := core.RoundL2GasPrice(gasPrice)
log.Warn("Invalid gas price detected in state", "state", gasPrice, "using", gp)
gasPrice = gp
}
s.RollupGpo.SetL2GasPrice(gasPrice)
return nil return nil
} }
...@@ -728,11 +723,18 @@ func (s *SyncService) applyBatchedTransaction(tx *types.Transaction) error { ...@@ -728,11 +723,18 @@ 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 {
// Exit early if fees are enforced and the gasPrice is set to 0 if tx.GasPrice().Cmp(common.Big0) == 0 {
if s.enforceFees && tx.GasPrice().Cmp(common.Big0) == 0 { // Exit early if fees are enforced and the gasPrice is set to 0
return errors.New("cannot accept 0 gas price transaction") if s.enforceFees {
return errors.New("cannot accept 0 gas price transaction")
}
// If fees are not enforced and the gas price is 0, return early
return nil
}
// When the gas price is non zero, it must be equal to the constant
if tx.GasPrice().Cmp(fees.BigTxGasPrice) != 0 {
return fmt.Errorf("tx.gasPrice must be %d", fees.TxGasPrice)
} }
l1GasPrice, err := s.RollupGpo.SuggestL1GasPrice(context.Background()) l1GasPrice, err := s.RollupGpo.SuggestL1GasPrice(context.Background())
if err != nil { if err != nil {
return err return err
...@@ -743,22 +745,29 @@ func (s *SyncService) verifyFee(tx *types.Transaction) error { ...@@ -743,22 +745,29 @@ 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 := core.DecodeL2GasLimit(gas) l2GasLimit := fees.DecodeL2GasLimit(gas)
fee, err := core.CalculateRollupFee(tx.Data(), l1GasPrice, l2GasLimit, l2GasPrice) // 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)
if err != nil { if err != nil {
return err return err
} }
// If fees are not enforced and the gas price is 0, return early
if !s.enforceFees && tx.GasPrice().Cmp(common.Big0) == 0 {
return nil
}
// This should only happen if the transaction fee is greater than 18.44 ETH // This should only happen if the transaction fee is greater than 18.44 ETH
if !fee.IsUint64() { if !fee.IsUint64() {
return fmt.Errorf("fee overflow: %s", fee.String()) return fmt.Errorf("fee overflow: %s", fee.String())
} }
// Make sure that the fee is paid // Compute the user's fee
if tx.Gas() < fee.Uint64() { paying := new(big.Int).Mul(new(big.Int).SetUint64(tx.Gas()), tx.GasPrice())
return fmt.Errorf("fee too low: %d, use at least tx.gasLimit = %d and tx.gasPrice = 1", tx.Gas(), fee.Uint64()) // 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)
}
// 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)
} }
return nil return nil
} }
...@@ -776,7 +785,6 @@ func (s *SyncService) ValidateAndApplySequencerTransaction(tx *types.Transaction ...@@ -776,7 +785,6 @@ func (s *SyncService) ValidateAndApplySequencerTransaction(tx *types.Transaction
if err := s.verifyFee(tx); err != nil { if err := s.verifyFee(tx); err != nil {
return err return err
} }
s.txLock.Lock() s.txLock.Lock()
defer s.txLock.Unlock() defer s.txLock.Unlock()
log.Trace("Sequencer transaction validation", "hash", tx.Hash().Hex()) log.Trace("Sequencer transaction validation", "hash", tx.Hash().Hex())
......
...@@ -507,7 +507,8 @@ func TestSyncServiceL1GasPrice(t *testing.T) { ...@@ -507,7 +507,8 @@ func TestSyncServiceL1GasPrice(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if gasAfter.Cmp(core.RoundL1GasPrice(big.NewInt(1))) != 0 { expect, _ := service.client.GetL1GasPrice()
if gasAfter.Cmp(expect) != 0 {
t.Fatal("expected 100 gas price, got", gasAfter) t.Fatal("expected 100 gas price, got", gasAfter)
} }
} }
...@@ -533,7 +534,7 @@ func TestSyncServiceL2GasPrice(t *testing.T) { ...@@ -533,7 +534,7 @@ func TestSyncServiceL2GasPrice(t *testing.T) {
if err != nil { if err != nil {
t.Fatal("Cannot get state db") t.Fatal("Cannot get state db")
} }
l2GasPrice := big.NewInt(100000001) l2GasPrice := big.NewInt(100000000000)
state.SetState(service.gpoAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice)) state.SetState(service.gpoAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice))
root, _ := state.Commit(false) root, _ := state.Commit(false)
...@@ -824,7 +825,7 @@ func (m *mockClient) SyncStatus(backend Backend) (*SyncStatus, error) { ...@@ -824,7 +825,7 @@ func (m *mockClient) SyncStatus(backend Backend) (*SyncStatus, error) {
} }
func (m *mockClient) GetL1GasPrice() (*big.Int, error) { func (m *mockClient) GetL1GasPrice() (*big.Int, error) {
price := core.RoundL1GasPrice(big.NewInt(2)) price := big.NewInt(1)
return price, nil return price, nil
} }
......
...@@ -25,36 +25,29 @@ $ yarn lint ...@@ -25,36 +25,29 @@ $ yarn lint
### L2 Fees ### L2 Fees
The Layer 2 fee is encoded in `tx.gasLimit`. The Layer 2 `gasLimit` is encoded `TxGasLimit` can be used to `encode` and `decode` the L2 Gas Limit
in the lower order bits of the `tx.gasLimit`. For this scheme to work, both the locally.
L1 gas price and the L2 gas price must satisfy specific equations. There are
functions that help ensure that the correct gas prices are used.
- `roundL1GasPrice`
- `roundL2GasPrice`
The Layer 2 fee is based on both the cost of submitting the data to L1 as well
as the cost of execution on L2. To make libraries like `ethers` just work, the
return value of `eth_estimateGas` has been modified to return the fee. A new RPC
endpoint `eth_estimateExecutionGas` has been added that returns the L2 gas used.
To locally encode the `tx.gasLimit`, the `L2GasLimit` methods `encode` and
`decode` should be used.
```typescript ```typescript
import { L2GasLimit, roundL1GasPrice, roundL2GasPrice } from '@eth-optimism/core-utils' import { TxGasLimit } from '@eth-optimism/core-utils'
import { JsonRpcProvider } from 'ethers' import { JsonRpcProvider } from 'ethers'
const provider = new JsonRpcProvider('https://mainnet.optimism.io') const L2Provider = new JsonRpcProvider('https://mainnet.optimism.io')
const gasLimit = await provider.send('eth_estimateExecutionGas', [tx]) const L1Provider = new JsonRpcProvider('http://127.0.0.1:8545')
const encoded = L2GasLimit.encode({ const l2GasLimit = await L2Provider.send('eth_estimateExecutionGas', [tx])
const l1GasPrice = await L1Provider.getGasPrice()
const encoded = TxGasLimit.encode({
data: '0x', data: '0x',
l1GasPrice: roundL1GasPrice(1), l1GasPrice,
l2GasLimit: gasLimit, l2GasLimit,
l2GasPrice: roundL2GasPrice(1), l2GasPrice: 10000000,
}) })
const decoded = L2GasLimit.decode(encoded) const decoded = TxGasLimit.decode(encoded)
assert(decoded.eq(gasLimit)) assert(decoded.eq(gasLimit))
const estimate = await L2Provider.estimateGas()
assert(estimate.eq(encoded))
``` ```
...@@ -6,9 +6,11 @@ import { BigNumber } from 'ethers' ...@@ -6,9 +6,11 @@ import { BigNumber } from 'ethers'
import { remove0x } from './common' import { remove0x } from './common'
const hundredMillion = BigNumber.from(100_000_000) const hundredMillion = BigNumber.from(100_000_000)
const feeScalar = 1000
export const TxGasPrice = BigNumber.from(feeScalar + feeScalar / 2)
const txDataZeroGas = 4 const txDataZeroGas = 4
const txDataNonZeroGasEIP2028 = 16 const txDataNonZeroGasEIP2028 = 16
const overhead = 4200 const overhead = 4200 + 200 * txDataNonZeroGasEIP2028
export interface EncodableL2GasLimit { export interface EncodableL2GasLimit {
data: Buffer | string data: Buffer | string
...@@ -29,17 +31,15 @@ function encode(input: EncodableL2GasLimit): BigNumber { ...@@ -29,17 +31,15 @@ function encode(input: EncodableL2GasLimit): BigNumber {
if (typeof l2GasPrice === 'number') { if (typeof l2GasPrice === 'number') {
l2GasPrice = BigNumber.from(l2GasPrice) l2GasPrice = BigNumber.from(l2GasPrice)
} }
if (!verifyL2GasPrice(l2GasPrice)) {
throw new Error(`Invalid L2 Gas Price: ${l2GasPrice.toString()}`)
}
if (!verifyL1GasPrice(l1GasPrice)) {
throw new Error(`Invalid L1 Gas Price: ${l1GasPrice.toString()}`)
}
const l1GasLimit = calculateL1GasLimit(data) const l1GasLimit = calculateL1GasLimit(data)
const l1Fee = l1GasPrice.mul(l1GasLimit) const l1Fee = l1GasLimit.mul(l1GasPrice)
const l2Fee = l2GasLimit.mul(l2GasPrice) const l2Fee = l2GasLimit.mul(l2GasPrice)
return l1Fee.add(l2Fee) const sum = l1Fee.add(l2Fee)
const scaled = sum.div(feeScalar)
const remainder = scaled.mod(hundredMillion)
const scaledSum = scaled.add(hundredMillion)
const rounded = scaledSum.sub(remainder)
return rounded.add(l2GasLimit)
} }
function decode(fee: BigNumber | number): BigNumber { function decode(fee: BigNumber | number): BigNumber {
...@@ -49,39 +49,17 @@ function decode(fee: BigNumber | number): BigNumber { ...@@ -49,39 +49,17 @@ function decode(fee: BigNumber | number): BigNumber {
return fee.mod(hundredMillion) return fee.mod(hundredMillion)
} }
export const L2GasLimit = { export const TxGasLimit = {
encode, encode,
decode, decode,
} }
export function verifyL2GasPrice(gasPrice: BigNumber | number): boolean { export function calculateL1GasLimit(data: string | Buffer): BigNumber {
if (typeof gasPrice === 'number') {
gasPrice = BigNumber.from(gasPrice)
}
// If the gas price is not equal to 0 and the gas price mod
// one hundred million is not one
if (!gasPrice.eq(0) && !gasPrice.mod(hundredMillion).eq(1)) {
return false
}
if (gasPrice.eq(0)) {
return false
}
return true
}
export function verifyL1GasPrice(gasPrice: BigNumber | number): boolean {
if (typeof gasPrice === 'number') {
gasPrice = BigNumber.from(gasPrice)
}
return gasPrice.mod(hundredMillion).eq(0)
}
export function calculateL1GasLimit(data: string | Buffer): number {
const [zeroes, ones] = zeroesAndOnes(data) const [zeroes, ones] = zeroesAndOnes(data)
const zeroesCost = zeroes * txDataZeroGas const zeroesCost = zeroes * txDataZeroGas
const onesCost = ones * txDataNonZeroGasEIP2028 const onesCost = ones * txDataNonZeroGasEIP2028
const gasLimit = zeroesCost + onesCost + overhead const gasLimit = zeroesCost + onesCost + overhead
return gasLimit return BigNumber.from(gasLimit)
} }
export function zeroesAndOnes(data: Buffer | string): Array<number> { export function zeroesAndOnes(data: Buffer | string): Array<number> {
...@@ -99,34 +77,3 @@ export function zeroesAndOnes(data: Buffer | string): Array<number> { ...@@ -99,34 +77,3 @@ export function zeroesAndOnes(data: Buffer | string): Array<number> {
} }
return [zeros, ones] return [zeros, ones]
} }
export function roundL1GasPrice(gasPrice: BigNumber | number): BigNumber {
if (typeof gasPrice === 'number') {
gasPrice = BigNumber.from(gasPrice)
}
return ceilModOneHundredMillion(gasPrice)
}
function ceilModOneHundredMillion(num: BigNumber): BigNumber {
if (num.mod(hundredMillion).eq(0)) {
return num
}
const sum = num.add(hundredMillion)
const mod = num.mod(hundredMillion)
return sum.sub(mod)
}
export function roundL2GasPrice(gasPrice: BigNumber | number): BigNumber {
if (typeof gasPrice === 'number') {
gasPrice = BigNumber.from(gasPrice)
}
if (gasPrice.eq(0)) {
return BigNumber.from(1)
}
if (gasPrice.eq(1)) {
return hundredMillion.add(1)
}
const gp = gasPrice.sub(1)
const mod = ceilModOneHundredMillion(gp)
return mod.add(1)
}
import { expect } from '../setup' import { expect } from '../setup'
import * as fees from '../../src/fees' import * as fees from '../../src/fees'
import { BigNumber } from 'ethers' import { BigNumber, utils } from 'ethers'
const hundredBillion = 10 ** 11
const million = 10 ** 6
describe('Fees', () => { describe('Fees', () => {
it('should count zeros and ones', () => { it('should count zeros and ones', () => {
...@@ -18,115 +21,99 @@ describe('Fees', () => { ...@@ -18,115 +21,99 @@ describe('Fees', () => {
} }
}) })
describe('Round L1 Gas Price', () => {
const roundL1GasPriceTests = [
{ input: 10, expect: 10 ** 8, name: 'simple' },
{ input: 10 ** 8 + 1, expect: 2 * 10 ** 8, name: 'one-over' },
{ input: 10 ** 8, expect: 10 ** 8, name: 'exact' },
{ input: 10 ** 8 - 1, expect: 10 ** 8, name: 'one-under' },
{ input: 3, expect: 10 ** 8, name: 'small' },
{ input: 2, expect: 10 ** 8, name: 'two' },
{ input: 1, expect: 10 ** 8, name: 'one' },
{ input: 0, expect: 0, name: 'zero' },
]
for (const test of roundL1GasPriceTests) {
it(`should pass for ${test.name} case`, () => {
const got = fees.roundL1GasPrice(test.input)
const expected = BigNumber.from(test.expect)
expect(got).to.deep.equal(expected)
})
}
})
describe('Round L2 Gas Price', () => {
const roundL2GasPriceTests = [
{ input: 10, expect: 10 ** 8 + 1, name: 'simple' },
{ input: 10 ** 8 + 2, expect: 2 * 10 ** 8 + 1, name: 'one-over' },
{ input: 10 ** 8 + 1, expect: 10 ** 8 + 1, name: 'exact' },
{ input: 10 ** 8, expect: 10 ** 8 + 1, name: 'one-under' },
{ input: 3, expect: 10 ** 8 + 1, name: 'small' },
{ input: 2, expect: 10 ** 8 + 1, name: 'two' },
{ input: 1, expect: 10 ** 8 + 1, name: 'one' },
{ input: 0, expect: 1, name: 'zero' },
]
for (const test of roundL2GasPriceTests) {
it(`should pass for ${test.name} case`, () => {
const got = fees.roundL2GasPrice(test.input)
const expected = BigNumber.from(test.expect)
expect(got).to.deep.equal(expected)
})
}
})
describe('Rollup Fees', () => { describe('Rollup Fees', () => {
const rollupFeesTests = [ const rollupFeesTests = [
{ {
name: 'simple', name: 'simple',
dataLen: 10, dataLen: 10,
l1GasPrice: 100_000_000, l1GasPrice: utils.parseUnits('1', 'gwei'),
l2GasPrice: 100_000_001, l2GasPrice: utils.parseUnits('1', 'gwei'),
l2GasLimit: 437118, l2GasLimit: 437118,
error: false, },
{
name: 'small-gasprices-max-gaslimit',
dataLen: 10,
l1GasPrice: utils.parseUnits('1', 'wei'),
l2GasPrice: utils.parseUnits('1', 'wei'),
l2GasLimit: 0x4ffffff,
},
{
name: 'large-gasprices-max-gaslimit',
dataLen: 10,
l1GasPrice: utils.parseUnits('1', 'ether'),
l2GasPrice: utils.parseUnits('1', 'ether'),
l2GasLimit: 0x4ffffff,
},
{
name: 'small-gasprices-max-gaslimit',
dataLen: 10,
l1GasPrice: utils.parseUnits('1', 'ether'),
l2GasPrice: utils.parseUnits('1', 'ether'),
l2GasLimit: 1,
},
{
name: 'max-gas-limit',
dataLen: 10,
l1GasPrice: utils.parseUnits('5', 'ether'),
l2GasPrice: utils.parseUnits('5', 'ether'),
l2GasLimit: 10 ** 8 - 1,
}, },
{ {
name: 'zero-l2-gasprice', name: 'zero-l2-gasprice',
dataLen: 10, dataLen: 10,
l1GasPrice: 100_000_000, l1GasPrice: hundredBillion,
l2GasPrice: 0, l2GasPrice: 0,
l2GasLimit: 196205, l2GasLimit: 196205,
error: true,
}, },
{ {
name: 'one-l2-gasprice', name: 'one-l2-gasprice',
dataLen: 10, dataLen: 10,
l1GasPrice: 100_000_000, l1GasPrice: hundredBillion,
l2GasPrice: 1, l2GasPrice: 1,
l2GasLimit: 196205, l2GasLimit: 196205,
error: false,
}, },
{ {
name: 'zero-l1-gasprice', name: 'zero-l1-gasprice',
dataLen: 10, dataLen: 10,
l1GasPrice: 0, l1GasPrice: 0,
l2GasPrice: 100_000_001, l2GasPrice: hundredBillion,
l2GasLimit: 196205, l2GasLimit: 196205,
error: false,
}, },
{ {
name: 'one-l1-gasprice', name: 'one-l1-gasprice',
dataLen: 10, dataLen: 10,
l1GasPrice: 1, l1GasPrice: 1,
l2GasPrice: 23254, l2GasPrice: hundredBillion,
l2GasLimit: 23255,
},
{
name: 'zero-gasprices',
dataLen: 10,
l1GasPrice: 0,
l2GasPrice: 0,
l2GasLimit: 23255, l2GasLimit: 23255,
error: true, },
{
name: 'larger-divisor',
dataLen: 10,
l1GasPrice: 0,
l2GasLimit: 10,
l2GasPrice: 0,
}, },
] ]
for (const test of rollupFeesTests) { for (const test of rollupFeesTests) {
it(`should pass for ${test.name} case`, () => { it(`should pass for ${test.name} case`, () => {
const data = Buffer.alloc(test.dataLen) const data = Buffer.alloc(test.dataLen)
const got = fees.TxGasLimit.encode({
data,
l1GasPrice: test.l1GasPrice,
l2GasPrice: test.l2GasPrice,
l2GasLimit: test.l2GasLimit,
})
let got const decoded = fees.TxGasLimit.decode(got)
let err = false expect(decoded).to.deep.eq(BigNumber.from(test.l2GasLimit))
try {
got = fees.L2GasLimit.encode({
data,
l1GasPrice: test.l1GasPrice,
l2GasPrice: test.l2GasPrice,
l2GasLimit: test.l2GasLimit,
})
} catch (e) {
err = true
}
expect(err).to.equal(test.error)
if (!err) {
const decoded = fees.L2GasLimit.decode(got)
expect(decoded).to.deep.eq(BigNumber.from(test.l2GasLimit))
}
}) })
} }
}) })
......
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