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

l2geth: updated calculate rollup fee (#906)

* l2geth: updated calculate rollup fee

* l2geth: implement the gasprice serialization

* lint: fix

* rollup-test: fix

* l2geth: gaslimit encoding state polling (#927)

* l2geth: rollup gas price oracle state polling

* l2geth: read l2 gasprice from the tip

* l2geth: add config options for L2 gas price

* l2geth: comment to remove code in future

* l2geth: handle 0 and 1 fees

* l2geth: enable 0 and 1 fee tests

* feat: gaslimit encoding end to end (#932)

* core-utils: add fees package

* integration-tests: refactor for new fees

* l2geth: end to end fee spec

* l2geth: use new env var

* deps: regenerate

* lint: fix

* l2geth: fee verification

* tests: update gas prices

* tests: update gas prices

* tests: cleanup

* l2geth: small cleanup

* l2geth: fix max

* feat: fix fee calculations with bigints

* tests: fix

* tests: lint

* core-utils: rename fees to L2GasLimit

* l2geth: fix comment

* l2geth: fix name of env var

* l2geth: delete extra print statement

* l2geth: fix logline

* tests: fix typo

* l2geth: improve readability

* chore: add changeset

* l2geth: fix compiler error

* feat: clean up and fix fees

* lint: fix

* core-utils: refactor api to be more friendly

* lint: fix

* comments: fix

* refactor: clean up style and common language
Co-authored-by: default avatarGeorgios Konstantopoulos <me@gakonst.com>
parent 1dbde1a8
---
'@eth-optimism/integration-tests': patch
'@eth-optimism/l2geth': patch
'@eth-optimism/core-utils': patch
---
End to end fee integration with recoverable L2 gas limit
...@@ -63,11 +63,8 @@ describe('Basic ERC20 interactions', async () => { ...@@ -63,11 +63,8 @@ describe('Basic ERC20 interactions', async () => {
const transfer = await ERC20.transfer(other.address, 100) const transfer = await ERC20.transfer(other.address, 100)
const receipt = await transfer.wait() const receipt = await transfer.wait()
// The expected fee paid is the value returned by eth_estimateGas gas multiplied // The expected fee paid is the value returned by eth_estimateGas
// by 1 gwei, since that's the value eth_gasPrice always returns const expectedFeePaid = await ERC20.estimateGas.transfer(other.address, 100)
const expectedFeePaid = (
await ERC20.estimateGas.transfer(other.address, 100)
).mul(GWEI)
// 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,6 +3,7 @@ import chaiAsPromised from 'chai-as-promised' ...@@ -3,6 +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'
describe('Fee Payment Integration Tests', async () => { describe('Fee Payment Integration Tests', async () => {
let env: OptimismEnv let env: OptimismEnv
...@@ -12,14 +13,38 @@ describe('Fee Payment Integration Tests', async () => { ...@@ -12,14 +13,38 @@ describe('Fee Payment Integration Tests', async () => {
env = await OptimismEnv.new() env = await OptimismEnv.new()
}) })
it('Should return a gasPrice of 1 wei', async () => {
const gasPrice = await env.l2Wallet.getGasPrice()
expect(gasPrice.eq(1))
})
it('Should estimateGas with recoverable L2 gasLimit', async () => {
const gas = await env.ovmEth.estimateGas.transfer(
other,
utils.parseEther('0.5')
)
const tx = await env.ovmEth.populateTransaction.transfer(
other,
utils.parseEther('0.5')
)
const executionGas = await (env.ovmEth
.provider as any).send('eth_estimateExecutionGas', [tx])
const decoded = L2GasLimit.decode(gas)
expect(BigNumber.from(executionGas)).deep.eq(decoded)
})
it('Paying a nonzero but acceptable gasPrice fee', async () => { it('Paying a nonzero but acceptable gasPrice fee', async () => {
const amount = utils.parseEther('0.5') const amount = utils.parseEther('0.5')
const balanceBefore = await env.l2Wallet.getBalance() const balanceBefore = await env.l2Wallet.getBalance()
const tx = await env.ovmEth.transfer(other, amount) expect(balanceBefore.gt(amount))
await tx.wait()
const gas = await env.ovmEth.estimateGas.transfer(other, amount)
const tx = await env.ovmEth.transfer(other, amount, { gasPrice: 1 })
const receipt = await tx.wait()
expect(receipt.status).to.eq(1)
const balanceAfter = await env.l2Wallet.getBalance() const balanceAfter = await env.l2Wallet.getBalance()
// TODO: The fee paid MUST be the receipt.gasUsed, and not the tx.gasLimit // The fee paid MUST be the receipt.gasUsed, and not the tx.gasLimit
// https://github.com/ethereum-optimism/optimism/blob/0de7a2f9c96a7c4860658822231b2d6da0fefb1d/packages/contracts/contracts/optimistic-ethereum/OVM/accounts/OVM_ECDSAContractAccount.sol#L103 // https://github.com/ethereum-optimism/optimism/blob/0de7a2f9c96a7c4860658822231b2d6da0fefb1d/packages/contracts/contracts/optimistic-ethereum/OVM/accounts/OVM_ECDSAContractAccount.sol#L103
expect(balanceBefore.sub(balanceAfter)).to.be.deep.eq( expect(balanceBefore.sub(balanceAfter)).to.be.deep.eq(
tx.gasPrice.mul(tx.gasLimit).add(amount) tx.gasPrice.mul(tx.gasLimit).add(amount)
......
...@@ -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(21000)) expect(gas).to.be.deep.eq(BigNumber.from(0x23284d28fe6d))
}) })
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(21000)) expect(gas).to.be.deep.eq(BigNumber.from(0x207ad91a77b4))
}) })
}) })
......
import { injectL2Context } from '@eth-optimism/core-utils' import {
injectL2Context,
L2GasLimit,
roundL1GasPrice,
} 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'
...@@ -19,7 +23,7 @@ describe('Basic RPC tests', () => { ...@@ -19,7 +23,7 @@ describe('Basic RPC tests', () => {
const DEFAULT_TRANSACTION = { const DEFAULT_TRANSACTION = {
to: '0x' + '1234'.repeat(10), to: '0x' + '1234'.repeat(10),
gasLimit: 4000000, gasLimit: 33600000000001,
gasPrice: 0, gasPrice: 0,
data: '0x', data: '0x',
value: 0, value: 0,
...@@ -95,7 +99,7 @@ describe('Basic RPC tests', () => { ...@@ -95,7 +99,7 @@ describe('Basic RPC tests', () => {
...DEFAULT_TRANSACTION, ...DEFAULT_TRANSACTION,
chainId: await env.l2Wallet.getChainId(), chainId: await env.l2Wallet.getChainId(),
data: '0x', data: '0x',
value: ethers.utils.parseEther('5'), value: ethers.utils.parseEther('0.1'),
} }
const balanceBefore = await provider.getBalance(env.l2Wallet.address) const balanceBefore = await provider.getBalance(env.l2Wallet.address)
...@@ -104,7 +108,7 @@ describe('Basic RPC tests', () => { ...@@ -104,7 +108,7 @@ describe('Basic RPC tests', () => {
expect(receipt.status).to.deep.equal(1) expect(receipt.status).to.deep.equal(1)
expect(await provider.getBalance(env.l2Wallet.address)).to.deep.equal( expect(await provider.getBalance(env.l2Wallet.address)).to.deep.equal(
balanceBefore.sub(ethers.utils.parseEther('5')) balanceBefore.sub(ethers.utils.parseEther('0.1'))
) )
}) })
...@@ -122,6 +126,18 @@ describe('Basic RPC tests', () => { ...@@ -122,6 +126,18 @@ describe('Basic RPC tests', () => {
) )
}) })
it('should reject a transaction with too low of a fee', async () => {
const tx = {
...DEFAULT_TRANSACTION,
gasLimit: 1,
gasPrice: 1,
}
await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
'fee too low: 1, use at least tx.gasLimit = 33600000000001 and tx.gasPrice = 1'
)
})
it('should correctly report OOG for contract creations', async () => { it('should correctly report OOG for contract creations', async () => {
const factory = await ethers.getContractFactory('TestOOG') const factory = await ethers.getContractFactory('TestOOG')
...@@ -136,7 +152,7 @@ describe('Basic RPC tests', () => { ...@@ -136,7 +152,7 @@ describe('Basic RPC tests', () => {
await expect( await expect(
provider.call({ provider.call({
...revertingTx, ...revertingTx,
gasLimit: 21_000, gasLimit: 1,
}) })
).to.be.rejectedWith('out of gas') ).to.be.rejectedWith('out of gas')
}) })
...@@ -159,7 +175,7 @@ describe('Basic RPC tests', () => { ...@@ -159,7 +175,7 @@ describe('Basic RPC tests', () => {
await expect( await expect(
provider.call({ provider.call({
...revertingDeployTx, ...revertingDeployTx,
gasLimit: 30_000, gasLimit: 1,
}) })
).to.be.rejectedWith('out of gas') ).to.be.rejectedWith('out of gas')
}) })
...@@ -182,7 +198,7 @@ describe('Basic RPC tests', () => { ...@@ -182,7 +198,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: 8_999_999, // override gas estimation gasLimit: 934111908999999, // override gas estimation
} }
const tx = await wallet.sendTransaction(req) const tx = await wallet.sendTransaction(req)
...@@ -205,7 +221,7 @@ describe('Basic RPC tests', () => { ...@@ -205,7 +221,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: 8_999_999, // override gas estimation gasLimit: 1051391908999999, // override gas estimation
} }
const tx = await wallet.sendTransaction(req) const tx = await wallet.sendTransaction(req)
...@@ -296,7 +312,7 @@ describe('Basic RPC tests', () => { ...@@ -296,7 +312,7 @@ describe('Basic RPC tests', () => {
describe('eth_gasPrice', () => { describe('eth_gasPrice', () => {
it('gas price should be 1 gwei', async () => { it('gas price should be 1 gwei', async () => {
expect(await provider.getGasPrice()).to.be.deep.equal(GWEI) expect(await provider.getGasPrice()).to.be.deep.equal(1)
}) })
}) })
...@@ -306,35 +322,41 @@ describe('Basic RPC tests', () => { ...@@ -306,35 +322,41 @@ describe('Basic RPC tests', () => {
to: DEFAULT_TRANSACTION.to, to: DEFAULT_TRANSACTION.to,
value: 0, value: 0,
}) })
expect(estimate).to.be.eq(21000) expect(estimate).to.be.eq(33600000119751)
}) })
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 () => {
const dataLen = [0, 2, 8, 64, 256] const dataLen = [0, 2, 8, 64, 256]
const l1GasPrice = await env.l1Wallet.provider.getGasPrice() const l1GasPrice = await env.l1Wallet.provider.getGasPrice()
// 96 bytes * 16 per non zero byte
const onesCost = BigNumber.from(96).mul(16)
const expectedCost = dataLen
.map((len) => BigNumber.from(len).mul(4))
.map((zerosCost) => zerosCost.add(onesCost))
// Repeat this test for a series of possible transaction sizes. // Repeat this test for a series of possible transaction sizes.
for (let i = 0; i < dataLen.length; i++) { for (const data of dataLen) {
const estimate = await l2Provider.estimateGas({ const tx = {
...DEFAULT_TRANSACTION, to: '0x' + '1234'.repeat(10),
data: '0x' + '00'.repeat(dataLen[i]), gasPrice: '0x1',
value: '0x0',
data: '0x' + '00'.repeat(data),
from: '0x' + '1234'.repeat(10), from: '0x' + '1234'.repeat(10),
})
// we normalize by gwei here because the RPC does it as well, since the
// user provides a 1gwei gas price when submitting txs via the eth_gasPrice
// rpc call. The smallest possible value for the expected cost is 21000
let expected = expectedCost[i].mul(l1GasPrice).div(GWEI)
if (expected.lt(BigNumber.from(21000))) {
expected = BigNumber.from(21000)
} }
expect(estimate).to.be.deep.eq(expected) const estimate = await l2Provider.estimateGas(tx)
const l2Gaslimit = await l2Provider.send('eth_estimateExecutionGas', [
tx,
])
const decoded = L2GasLimit.decode(estimate)
expect(decoded).to.deep.eq(BigNumber.from(l2Gaslimit))
expect(estimate.toString().endsWith(l2Gaslimit.toString()))
// The L2GasPrice should be fetched from the L2GasPrice oracle contract,
// but it does not yet exist. Use the default value for now
const l2GasPrice = BigNumber.from(1)
const expected = L2GasLimit.encode({
data: tx.data,
l1GasPrice: roundL1GasPrice(l1GasPrice),
l2GasLimit: BigNumber.from(l2Gaslimit),
l2GasPrice,
})
expect(expected).to.deep.eq(estimate)
} }
}) })
......
...@@ -156,7 +156,6 @@ var ( ...@@ -156,7 +156,6 @@ var (
utils.Eth1ETHGatewayAddressFlag, utils.Eth1ETHGatewayAddressFlag,
utils.Eth1ChainIdFlag, utils.Eth1ChainIdFlag,
utils.RollupClientHttpFlag, utils.RollupClientHttpFlag,
// Enable verifier mode
utils.RollupEnableVerifierFlag, utils.RollupEnableVerifierFlag,
utils.RollupAddressManagerOwnerAddressFlag, utils.RollupAddressManagerOwnerAddressFlag,
utils.RollupTimstampRefreshFlag, utils.RollupTimstampRefreshFlag,
...@@ -166,6 +165,9 @@ var ( ...@@ -166,6 +165,9 @@ var (
utils.RollupMaxCalldataSizeFlag, utils.RollupMaxCalldataSizeFlag,
utils.RollupDataPriceFlag, utils.RollupDataPriceFlag,
utils.RollupExecutionPriceFlag, utils.RollupExecutionPriceFlag,
utils.RollupEnableL2GasPollingFlag,
utils.RollupGasPriceOracleAddressFlag,
utils.RollupEnforceFeesFlag,
} }
rpcFlags = []cli.Flag{ rpcFlags = []cli.Flag{
......
...@@ -80,6 +80,9 @@ var AppHelpFlagGroups = []flagGroup{ ...@@ -80,6 +80,9 @@ var AppHelpFlagGroups = []flagGroup{
utils.RollupMaxCalldataSizeFlag, utils.RollupMaxCalldataSizeFlag,
utils.RollupDataPriceFlag, utils.RollupDataPriceFlag,
utils.RollupExecutionPriceFlag, utils.RollupExecutionPriceFlag,
utils.RollupEnableL2GasPollingFlag,
utils.RollupGasPriceOracleAddressFlag,
utils.RollupEnforceFeesFlag,
}, },
}, },
{ {
......
...@@ -893,6 +893,22 @@ var ( ...@@ -893,6 +893,22 @@ var (
Value: eth.DefaultConfig.Rollup.ExecutionPrice, Value: eth.DefaultConfig.Rollup.ExecutionPrice,
EnvVar: "ROLLUP_EXECUTIONPRICE", EnvVar: "ROLLUP_EXECUTIONPRICE",
} }
RollupGasPriceOracleAddressFlag = cli.StringFlag{
Name: "rollup.gaspriceoracleaddress",
Usage: "Address of the rollup gas price oracle",
Value: "0x0000000000000000000000000000000000000000",
EnvVar: "ROLLUP_GAS_PRICE_ORACLE_ADDRESS",
}
RollupEnableL2GasPollingFlag = cli.BoolFlag{
Name: "rollup.enablel2gaspolling",
Usage: "Poll for the L2 gas price from the L2 state",
EnvVar: "ROLLUP_ENABLE_L2_GAS_POLLING",
}
RollupEnforceFeesFlag = cli.BoolFlag{
Name: "rollup.enforcefeesflag",
Usage: "Disable transactions with 0 gas price",
EnvVar: "ROLLUP_ENFORCE_FEES",
}
) )
// MakeDataDir retrieves the currently requested data directory, terminating // MakeDataDir retrieves the currently requested data directory, terminating
...@@ -1172,6 +1188,16 @@ func setRollup(ctx *cli.Context, cfg *rollup.Config) { ...@@ -1172,6 +1188,16 @@ func setRollup(ctx *cli.Context, cfg *rollup.Config) {
if ctx.GlobalIsSet(RollupExecutionPriceFlag.Name) { if ctx.GlobalIsSet(RollupExecutionPriceFlag.Name) {
cfg.ExecutionPrice = GlobalBig(ctx, RollupExecutionPriceFlag.Name) cfg.ExecutionPrice = GlobalBig(ctx, RollupExecutionPriceFlag.Name)
} }
if ctx.GlobalIsSet(RollupGasPriceOracleAddressFlag.Name) {
addr := ctx.GlobalString(RollupGasPriceOracleAddressFlag.Name)
cfg.GasPriceOracleAddress = common.HexToAddress(addr)
}
if ctx.GlobalIsSet(RollupEnableL2GasPollingFlag.Name) {
cfg.EnableL2GasPolling = true
}
if ctx.GlobalIsSet(RollupEnforceFeesFlag.Name) {
cfg.EnforceFees = true
}
} }
// 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.
......
package core package core
import ( import (
"errors"
"fmt"
"math/big" "math/big"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/params"
) )
// RollupBaseTxSize is the encoded rollup transaction's compressed size excluding // overhead represents the fixed cost of batch submission of a single
// the variable length data. // transaction in gas
// Ref: https://github.com/ethereum-optimism/optimism/blob/91a9a3dcddf534ae1c906133b6d8e015a23c463b/packages/contracts/contracts/optimistic-ethereum/OVM/predeploys/OVM_SequencerEntrypoint.sol#L47 const overhead uint64 = 4200
const RollupBaseTxSize uint64 = 96
// 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 // CalculateFee calculates the fee that must be paid to the Rollup sequencer, taking into
// account the cost of publishing data to L1. // account the cost of publishing data to L1.
// Returns: (4 * zeroDataBytes + 16 * (nonZeroDataBytes + RollupBaseTxSize)) * dataPrice + executionPrice * gasUsed // l2_gas_price * l2_gas_limit + l1_gas_price * l1_gas_used
func CalculateRollupFee(data []byte, gasUsed uint64, dataPrice, executionPrice *big.Int) *big.Int { // 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) zeroes, ones := zeroesAndOnes(data)
zeroesCost := new(big.Int).SetUint64(zeroes * params.TxDataZeroGas) zeroesCost := zeroes * params.TxDataZeroGas
onesCost := new(big.Int).SetUint64((RollupBaseTxSize + ones) * params.TxDataNonZeroGasEIP2028) onesCost := ones * params.TxDataNonZeroGasEIP2028
dataCost := new(big.Int).Add(zeroesCost, onesCost) gasLimit := zeroesCost + onesCost + overhead
return new(big.Int).SetUint64(gasLimit)
// get the data fee }
dataFee := new(big.Int).Mul(dataPrice, dataCost)
executionFee := new(big.Int).Mul(executionPrice, new(big.Int).SetUint64(gasUsed)) // ceilModOneHundredMillion rounds the input integer up to the nearest modulus
fee := new(big.Int).Add(dataFee, executionFee) // of one hundred million
return fee 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) { func zeroesAndOnes(data []byte) (uint64, uint64) {
......
package core package core
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 {
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 {
data []byte
overhead uint64
expect *big.Int
}{
"simple": {[]byte{}, 0, big.NewInt(0)},
"simple-overhead": {[]byte{}, 10, big.NewInt(10)},
"zeros": {[]byte{0x00, 0x00, 0x00, 0x00}, 10, big.NewInt(26)},
"ones": {[]byte{0x01, 0x02, 0x03, 0x04}, 200, big.NewInt(16*4 + 200)},
}
func TestL1GasLimit(t *testing.T) {
for name, tt := range l1GasLimitTests {
t.Run(name, func(t *testing.T) {
got := calculateL1GasLimit(tt.data, tt.overhead)
if got.Cmp(tt.expect) != 0 {
t.Fatal("Calculated gas limit does not match")
}
})
}
}
var feeTests = map[string]struct { var feeTests = map[string]struct {
dataLen int dataLen int
gasUsed uint64 l1GasPrice uint64
dataPrice int64 l2GasLimit uint64
executionPrice int64 l2GasPrice uint64
err error
}{ }{
"simple": {10000, 10, 20, 30}, "simple": {100, 100_000_000, 437118, 100_000_001, nil},
"zero gas used": {10000, 0, 20, 30}, "zero-l2-gasprice": {10, 100_000_000, 196205, 0, errInvalidGasPrice},
"zero data price": {10000, 0, 0, 30}, "one-l2-gasprice": {10, 100_000_000, 196205, 1, nil},
"zero execution price": {10000, 0, 0, 0}, "zero-l1-gasprice": {10, 0, 196205, 100_000_001, nil},
"one-l1-gasprice": {10, 1, 23255, 23254, errInvalidGasPrice},
} }
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, 0, tt.dataLen)
fee := CalculateRollupFee(data, tt.gasUsed, big.NewInt(tt.dataPrice), big.NewInt(tt.executionPrice)) l1GasPrice := new(big.Int).SetUint64(tt.l1GasPrice)
l2GasLimit := new(big.Int).SetUint64(tt.l2GasLimit)
zeroes, ones := zeroesAndOnes(data) l2GasPrice := new(big.Int).SetUint64(tt.l2GasPrice)
zeroesCost := zeroes * 4
onesCost := (96 + ones) * 16 fee, err := CalculateRollupFee(data, l1GasPrice, l2GasLimit, l2GasPrice)
dataCost := zeroesCost + onesCost if !errors.Is(err, tt.err) {
dataFee := int64(dataCost) * tt.dataPrice t.Fatalf("Cannot calculate fee: %s", err)
}
executionFee := uint64(tt.executionPrice) * tt.gasUsed
expectedFee := uint64(dataFee) + executionFee if err == nil {
if fee.Cmp(big.NewInt(int64(expectedFee))) != 0 { decodedGasLimit := DecodeL2GasLimit(fee)
t.Errorf("rollup fee check failed: expected %d, got %s", expectedFee, fee.String()) 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))
}
...@@ -542,7 +542,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error { ...@@ -542,7 +542,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
// Ensure the transaction doesn't exceed the current block limit gas. // Ensure the transaction doesn't exceed the current block limit gas.
// We skip this condition check if the transaction's gasPrice is set to 1gwei, // We skip this condition check if the transaction's gasPrice is set to 1gwei,
// which indicates a "rollup" transaction that's paying for its data. // which indicates a "rollup" transaction that's paying for its data.
if pool.currentMaxGas < tx.Gas() && tx.GasPrice().Cmp(gwei) != 0 { if pool.currentMaxGas < tx.L2Gas() && tx.GasPrice().Cmp(gwei) != 0 {
return ErrGasLimit return ErrGasLimit
} }
......
...@@ -225,6 +225,7 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error { ...@@ -225,6 +225,7 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {
func (tx *Transaction) Data() []byte { return common.CopyBytes(tx.data.Payload) } func (tx *Transaction) Data() []byte { return common.CopyBytes(tx.data.Payload) }
func (tx *Transaction) Gas() uint64 { return tx.data.GasLimit } func (tx *Transaction) Gas() uint64 { return tx.data.GasLimit }
func (tx *Transaction) L2Gas() uint64 { return tx.data.GasLimit % 100_000_000 }
func (tx *Transaction) GasPrice() *big.Int { return new(big.Int).Set(tx.data.Price) } func (tx *Transaction) GasPrice() *big.Int { return new(big.Int).Set(tx.data.Price) }
func (tx *Transaction) Value() *big.Int { return new(big.Int).Set(tx.data.Amount) } func (tx *Transaction) Value() *big.Int { return new(big.Int).Set(tx.data.Amount) }
func (tx *Transaction) Nonce() uint64 { return tx.data.AccountNonce } func (tx *Transaction) Nonce() uint64 { return tx.data.AccountNonce }
......
...@@ -389,20 +389,20 @@ func (b *EthAPIBackend) SuggestPrice(ctx context.Context) (*big.Int, error) { ...@@ -389,20 +389,20 @@ func (b *EthAPIBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPrice(ctx) return b.gpo.SuggestPrice(ctx)
} }
func (b *EthAPIBackend) SuggestDataPrice(ctx context.Context) (*big.Int, error) { func (b *EthAPIBackend) SuggestL1GasPrice(ctx context.Context) (*big.Int, error) {
return b.rollupGpo.SuggestDataPrice(ctx) return b.rollupGpo.SuggestL1GasPrice(ctx)
} }
func (b *EthAPIBackend) SuggestExecutionPrice(ctx context.Context) (*big.Int, error) { func (b *EthAPIBackend) SuggestL2GasPrice(ctx context.Context) (*big.Int, error) {
return b.rollupGpo.SuggestExecutionPrice(ctx) return b.rollupGpo.SuggestL2GasPrice(ctx)
} }
func (b *EthAPIBackend) SetDataPrice(ctx context.Context, gasPrice *big.Int) { func (b *EthAPIBackend) SetL1GasPrice(ctx context.Context, gasPrice *big.Int) error {
b.rollupGpo.SetDataPrice(gasPrice) return b.rollupGpo.SetL1GasPrice(gasPrice)
} }
func (b *EthAPIBackend) SetExecutionPrice(ctx context.Context, gasPrice *big.Int) { func (b *EthAPIBackend) SetL2GasPrice(ctx context.Context, gasPrice *big.Int) error {
b.rollupGpo.SetExecutionPrice(gasPrice) return b.rollupGpo.SetL2GasPrice(gasPrice)
} }
func (b *EthAPIBackend) ChainDb() ethdb.Database { func (b *EthAPIBackend) ChainDb() ethdb.Database {
......
...@@ -4,8 +4,12 @@ import ( ...@@ -4,8 +4,12 @@ import (
"context" "context"
"math/big" "math/big"
"sync" "sync"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/log"
) )
// RollupOracle holds the L1 and L2 gas prices for fee calculation
type RollupOracle struct { type RollupOracle struct {
dataPrice *big.Int dataPrice *big.Int
executionPrice *big.Int executionPrice *big.Int
...@@ -13,6 +17,7 @@ type RollupOracle struct { ...@@ -13,6 +17,7 @@ type RollupOracle struct {
executionPriceLock sync.RWMutex executionPriceLock sync.RWMutex
} }
// NewRollupOracle returns an initialized RollupOracle
func NewRollupOracle(dataPrice *big.Int, executionPrice *big.Int) *RollupOracle { func NewRollupOracle(dataPrice *big.Int, executionPrice *big.Int) *RollupOracle {
return &RollupOracle{ return &RollupOracle{
dataPrice: dataPrice, dataPrice: dataPrice,
...@@ -20,32 +25,42 @@ func NewRollupOracle(dataPrice *big.Int, executionPrice *big.Int) *RollupOracle ...@@ -20,32 +25,42 @@ func NewRollupOracle(dataPrice *big.Int, executionPrice *big.Int) *RollupOracle
} }
} }
/// SuggestDataPrice 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) SuggestDataPrice(ctx context.Context) (*big.Int, error) { func (gpo *RollupOracle) SuggestL1GasPrice(ctx context.Context) (*big.Int, error) {
gpo.dataPriceLock.RLock() gpo.dataPriceLock.RLock()
price := gpo.dataPrice defer gpo.dataPriceLock.RUnlock()
gpo.dataPriceLock.RUnlock() return gpo.dataPrice, nil
return price, nil
} }
func (gpo *RollupOracle) SetDataPrice(dataPrice *big.Int) { // SetL1GasPrice returns the current L1 gas price
func (gpo *RollupOracle) SetL1GasPrice(dataPrice *big.Int) error {
gpo.dataPriceLock.Lock() gpo.dataPriceLock.Lock()
defer gpo.dataPriceLock.Unlock()
if err := core.VerifyL1GasPrice(dataPrice); err != nil {
return err
}
gpo.dataPrice = dataPrice gpo.dataPrice = dataPrice
gpo.dataPriceLock.Unlock() log.Info("Set L1 Gas Price", "gasprice", gpo.dataPrice)
return nil
} }
/// SuggestExecutionPrice 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) SuggestExecutionPrice(ctx context.Context) (*big.Int, error) { func (gpo *RollupOracle) SuggestL2GasPrice(ctx context.Context) (*big.Int, error) {
gpo.executionPriceLock.RLock() gpo.executionPriceLock.RLock()
price := gpo.executionPrice defer gpo.executionPriceLock.RUnlock()
gpo.executionPriceLock.RUnlock() return gpo.executionPrice, nil
return price, nil
} }
func (gpo *RollupOracle) SetExecutionPrice(executionPrice *big.Int) { // SetL2GasPrice returns the current L2 gas price
func (gpo *RollupOracle) SetL2GasPrice(executionPrice *big.Int) error {
gpo.executionPriceLock.Lock() gpo.executionPriceLock.Lock()
defer gpo.executionPriceLock.Unlock()
if err := core.VerifyL2GasPrice(executionPrice); err != nil {
return err
}
gpo.executionPrice = executionPrice gpo.executionPrice = executionPrice
gpo.executionPriceLock.Unlock() log.Info("Set L2 Gas Price", "gasprice", gpo.executionPrice)
return nil
} }
...@@ -50,7 +50,7 @@ import ( ...@@ -50,7 +50,7 @@ import (
) )
const ( const (
defaultGasPrice = params.GWei defaultGasPrice = params.Wei
) )
var errOVMUnsupported = errors.New("OVM: Unsupported RPC Method") var errOVMUnsupported = errors.New("OVM: Unsupported RPC Method")
...@@ -1040,29 +1040,30 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash ...@@ -1040,29 +1040,30 @@ func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash
// 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
dataPrice, err := b.SuggestDataPrice(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
executionPrice, err := b.SuggestExecutionPrice(ctx) l2GasPrice, err := b.SuggestL2GasPrice(ctx)
if err != nil { if err != nil {
return 0, err return 0, err
} }
// 3. calculate the fee and normalize by the default gas price
var data []byte var data []byte
if args.Data == nil { if args.Data == nil {
data = []byte{} data = []byte{}
} else { } else {
data = *args.Data data = *args.Data
} }
fee := core.CalculateRollupFee(data, uint64(gasUsed), dataPrice, executionPrice).Uint64() / defaultGasPrice // 3. calculate the fee
if fee < 21000 { l2GasLimit := new(big.Int).SetUint64(uint64(gasUsed))
fee = 21000 fee, err := core.CalculateRollupFee(data, l1GasPrice, l2GasLimit, l2GasPrice)
if err != nil {
return 0, err
} }
return (hexutil.Uint64)(fee), nil return (hexutil.Uint64)(fee.Uint64()), nil
} }
func legacyDoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap *big.Int) (hexutil.Uint64, error) { func legacyDoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap *big.Int) (hexutil.Uint64, error) {
...@@ -1140,12 +1141,20 @@ func legacyDoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrO ...@@ -1140,12 +1141,20 @@ func legacyDoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrO
} }
// EstimateGas returns an estimate of the amount of gas needed to execute the // EstimateGas returns an estimate of the amount of gas needed to execute the
// given transaction against the current pending block. // given transaction against the current pending block. This is modified to
// encode the fee in wei as gas price is always 1
func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) { func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) {
blockNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber) blockNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
return DoEstimateGas(ctx, s.b, args, blockNrOrHash, s.b.RPCGasCap()) return DoEstimateGas(ctx, s.b, args, blockNrOrHash, s.b.RPCGasCap())
} }
// EstimateExecutionGas returns an estimate of the amount of gas needed to execute the
// given transaction against the current pending block.
func (s *PublicBlockChainAPI) EstimateExecutionGas(ctx context.Context, args CallArgs) (hexutil.Uint64, error) {
blockNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
return legacyDoEstimateGas(ctx, s.b, args, blockNrOrHash, s.b.RPCGasCap())
}
// ExecutionResult groups all structured logs emitted by the EVM // ExecutionResult groups all structured logs emitted by the EVM
// while replaying a transaction in debug mode as well as transaction // while replaying a transaction in debug mode as well as transaction
// execution status, the amount of gas used and the return value // execution status, the amount of gas used and the return value
...@@ -1996,15 +2005,15 @@ func NewPrivateRollupAPI(b Backend) *PrivateRollupAPI { ...@@ -1996,15 +2005,15 @@ func NewPrivateRollupAPI(b Backend) *PrivateRollupAPI {
return &PrivateRollupAPI{b: b} return &PrivateRollupAPI{b: b}
} }
// SetDataPrice sets the gas price to be used when quoting calldata publishing costs // SetL1GasPrice sets the gas price to be used when quoting calldata publishing costs
// to users // to users
func (api *PrivateRollupAPI) SetDataPrice(ctx context.Context, gasPrice hexutil.Big) { func (api *PrivateRollupAPI) SetL1GasPrice(ctx context.Context, gasPrice hexutil.Big) error {
api.b.SetDataPrice(ctx, (*big.Int)(&gasPrice)) return api.b.SetL1GasPrice(ctx, (*big.Int)(&gasPrice))
} }
// SetExecutionPrice sets the gas price to be used when executing transactions on // SetL2GasPrice sets the gas price to be used when executing transactions on
func (api *PrivateRollupAPI) SetExecutionPrice(ctx context.Context, gasPrice hexutil.Big) { func (api *PrivateRollupAPI) SetL2GasPrice(ctx context.Context, gasPrice hexutil.Big) error {
api.b.SetExecutionPrice(ctx, (*big.Int)(&gasPrice)) return api.b.SetL2GasPrice(ctx, (*big.Int)(&gasPrice))
} }
// PublicDebugAPI is the collection of Ethereum APIs exposed over the public // PublicDebugAPI is the collection of Ethereum APIs exposed over the public
......
...@@ -94,10 +94,10 @@ type Backend interface { ...@@ -94,10 +94,10 @@ type Backend interface {
GetRollupContext() (uint64, uint64, uint64) GetRollupContext() (uint64, uint64, uint64)
GasLimit() uint64 GasLimit() uint64
GetDiff(*big.Int) (diffdb.Diff, error) GetDiff(*big.Int) (diffdb.Diff, error)
SuggestDataPrice(ctx context.Context) (*big.Int, error) SuggestL1GasPrice(ctx context.Context) (*big.Int, error)
SetDataPrice(context.Context, *big.Int) SetL1GasPrice(context.Context, *big.Int) error
SuggestExecutionPrice(context.Context) (*big.Int, error) SuggestL2GasPrice(context.Context) (*big.Int, error)
SetExecutionPrice(context.Context, *big.Int) SetL2GasPrice(context.Context, *big.Int) error
IngestTransactions([]*types.Transaction) error IngestTransactions([]*types.Transaction) error
} }
......
...@@ -286,22 +286,22 @@ func (b *LesApiBackend) SuggestPrice(ctx context.Context) (*big.Int, error) { ...@@ -286,22 +286,22 @@ func (b *LesApiBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
} }
// NB: Non sequencer nodes cannot suggest L1 gas prices. // NB: Non sequencer nodes cannot suggest L1 gas prices.
func (b *LesApiBackend) SuggestDataPrice(ctx context.Context) (*big.Int, error) { func (b *LesApiBackend) SuggestL1GasPrice(ctx context.Context) (*big.Int, error) {
panic("SuggestDataPrice not implemented") panic("SuggestL1GasPrice not implemented")
} }
// NB: Non sequencer nodes cannot suggest L2 execution gas prices. // NB: Non sequencer nodes cannot suggest L2 execution gas prices.
func (b *LesApiBackend) SuggestExecutionPrice(ctx context.Context) (*big.Int, error) { func (b *LesApiBackend) SuggestL2GasPrice(ctx context.Context) (*big.Int, error) {
panic("SuggestExecutionPrice not implemented") panic("SuggestL2GasPrice not implemented")
} }
// NB: Non sequencer nodes cannot set L1 gas prices. // NB: Non sequencer nodes cannot set L1 gas prices.
func (b *LesApiBackend) SetDataPrice(ctx context.Context, gasPrice *big.Int) { func (b *LesApiBackend) SetL1GasPrice(ctx context.Context, gasPrice *big.Int) error {
panic("SetDataPrice is not implemented") panic("SetDataPrice is not implemented")
} }
// NB: Non sequencer nodes cannot set L2 execution prices. // NB: Non sequencer nodes cannot set L2 execution prices.
func (b *LesApiBackend) SetExecutionPrice(ctx context.Context, gasPrice *big.Int) { func (b *LesApiBackend) SetL2GasPrice(ctx context.Context, gasPrice *big.Int) error {
panic("SetExecutionPrice is not implemented") panic("SetExecutionPrice is not implemented")
} }
......
...@@ -25,6 +25,9 @@ type Config struct { ...@@ -25,6 +25,9 @@ type Config struct {
L1CrossDomainMessengerAddress common.Address L1CrossDomainMessengerAddress common.Address
AddressManagerOwnerAddress common.Address AddressManagerOwnerAddress common.Address
L1ETHGatewayAddress common.Address L1ETHGatewayAddress common.Address
GasPriceOracleAddress common.Address
// Turns on checking of state for L2 gas price
EnableL2GasPolling bool
// Deployment Height of the canonical transaction chain // Deployment Height of the canonical transaction chain
CanonicalTransactionChainDeployHeight *big.Int CanonicalTransactionChainDeployHeight *big.Int
// Path to the state dump // Path to the state dump
...@@ -37,4 +40,6 @@ type Config struct { ...@@ -37,4 +40,6 @@ type Config struct {
DataPrice *big.Int DataPrice *big.Int
// The gas price to use for L2 congestion costs // The gas price to use for L2 congestion costs
ExecutionPrice *big.Int ExecutionPrice *big.Int
// Only accept transactions with fees
EnforceFees bool
} }
...@@ -10,7 +10,9 @@ import ( ...@@ -10,7 +10,9 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
...@@ -28,6 +30,10 @@ type OVMContext struct { ...@@ -28,6 +30,10 @@ type OVMContext struct {
timestamp uint64 timestamp uint64
} }
// 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))
// SyncService implements the verifier functionality as well as the reorg // SyncService implements the verifier functionality as well as the reorg
// protection for the sequencer. // protection for the sequencer.
type SyncService struct { type SyncService struct {
...@@ -49,6 +55,9 @@ type SyncService struct { ...@@ -49,6 +55,9 @@ type SyncService struct {
confirmationDepth uint64 confirmationDepth uint64
pollInterval time.Duration pollInterval time.Duration
timestampRefreshThreshold time.Duration timestampRefreshThreshold time.Duration
gpoAddress common.Address
enableL2GasPolling bool
enforceFees bool
} }
// NewSyncService returns an initialized sync service // NewSyncService returns an initialized sync service
...@@ -85,6 +94,7 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co ...@@ -85,6 +94,7 @@ 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)
service := SyncService{ service := SyncService{
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
...@@ -99,6 +109,9 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co ...@@ -99,6 +109,9 @@ func NewSyncService(ctx context.Context, cfg Config, txpool *core.TxPool, bc *co
db: db, db: db,
pollInterval: pollInterval, pollInterval: pollInterval,
timestampRefreshThreshold: timestampRefreshThreshold, timestampRefreshThreshold: timestampRefreshThreshold,
gpoAddress: cfg.GasPriceOracleAddress,
enableL2GasPolling: cfg.EnableL2GasPolling,
enforceFees: cfg.EnforceFees,
} }
// Initial sync service setup if it is enabled. This code depends on // Initial sync service setup if it is enabled. This code depends on
...@@ -183,6 +196,8 @@ func (s *SyncService) Start() error { ...@@ -183,6 +196,8 @@ 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)
s.updateL1GasPrice()
// When a sequencer, be sure to sync to the tip of the ctc before allowing // When a sequencer, be sure to sync to the tip of the ctc before allowing
// user transactions. // user transactions.
...@@ -302,6 +317,9 @@ func (s *SyncService) VerifierLoop() { ...@@ -302,6 +317,9 @@ 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 {
log.Error("Cannot update L2 gas price", "msg", err)
}
time.Sleep(s.pollInterval) time.Sleep(s.pollInterval)
} }
} }
...@@ -356,6 +374,9 @@ func (s *SyncService) SequencerLoop() { ...@@ -356,6 +374,9 @@ func (s *SyncService) SequencerLoop() {
} }
s.txLock.Unlock() s.txLock.Unlock()
if err := s.updateL2GasPrice(nil); err != nil {
log.Error("Cannot update L2 gas price", "msg", err)
}
if s.updateContext() != nil { if s.updateContext() != nil {
log.Error("Could not update execution context", "error", err) log.Error("Could not update execution context", "error", err)
} }
...@@ -447,8 +468,37 @@ func (s *SyncService) updateL1GasPrice() error { ...@@ -447,8 +468,37 @@ func (s *SyncService) updateL1GasPrice() error {
if err != nil { if err != nil {
return err return err
} }
s.RollupGpo.SetDataPrice(l1GasPrice) s.RollupGpo.SetL1GasPrice(l1GasPrice)
log.Info("Adjusted L1 Gas Price", "gasprice", l1GasPrice) 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 {
// TODO(mark): this is temporary and will be able to be rmoved when the
// OVM_GasPriceOracle is moved into the predeploy contracts
if !s.enableL2GasPolling {
return nil
}
var state *state.StateDB
var err error
if hash != nil {
state, err = s.bc.StateAt(*hash)
} else {
state, err = s.bc.State()
}
if err != nil {
return err
}
result := state.GetState(s.gpoAddress, l2GasPriceSlot)
gasPrice := 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
} }
...@@ -710,6 +760,43 @@ func (s *SyncService) applyTransaction(tx *types.Transaction) error { ...@@ -710,6 +760,43 @@ func (s *SyncService) applyTransaction(tx *types.Transaction) error {
return nil return nil
} }
// verifyFee will verify that a valid fee is being paid.
func (s *SyncService) verifyFee(tx *types.Transaction) error {
// Exit early if fees are enforced and the gasPrice is set to 0
if s.enforceFees && tx.GasPrice().Cmp(common.Big0) == 0 {
return errors.New("cannot accept 0 gas price transaction")
}
l1GasPrice, err := s.RollupGpo.SuggestL1GasPrice(context.Background())
if err != nil {
return err
}
l2GasPrice, err := s.RollupGpo.SuggestL2GasPrice(context.Background())
if err != nil {
return err
}
// Calculate the fee based on decoded L2 gas limit
gas := new(big.Int).SetUint64(tx.Gas())
l2GasLimit := core.DecodeL2GasLimit(gas)
fee, err := core.CalculateRollupFee(tx.Data(), l1GasPrice, l2GasLimit, l2GasPrice)
if err != nil {
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
if !fee.IsUint64() {
return fmt.Errorf("fee overflow: %s", fee.String())
}
// Make sure that the fee is paid
if tx.Gas() < fee.Uint64() {
return fmt.Errorf("fee too low: %d, use at least tx.gasLimit = %d and tx.gasPrice = 1", tx.Gas(), fee.Uint64())
}
return nil
}
// Higher level API for applying transactions. Should only be called for // Higher level API for applying transactions. Should only be called for
// queue origin sequencer transactions, as the contracts on L1 manage the same // queue origin sequencer transactions, as the contracts on L1 manage the same
// validity checks that are done here. // validity checks that are done here.
...@@ -717,6 +804,9 @@ func (s *SyncService) ApplyTransaction(tx *types.Transaction) error { ...@@ -717,6 +804,9 @@ func (s *SyncService) ApplyTransaction(tx *types.Transaction) error {
if tx == nil { if tx == nil {
return fmt.Errorf("nil transaction passed to ApplyTransaction") return fmt.Errorf("nil transaction passed to ApplyTransaction")
} }
if err := s.verifyFee(tx); err != nil {
return err
}
log.Debug("Sending transaction to sync service", "hash", tx.Hash().Hex()) log.Debug("Sending transaction to sync service", "hash", tx.Hash().Hex())
s.txLock.Lock() s.txLock.Lock()
......
...@@ -154,13 +154,12 @@ func TestSyncServiceTransactionEnqueued(t *testing.T) { ...@@ -154,13 +154,12 @@ func TestSyncServiceTransactionEnqueued(t *testing.T) {
func TestSyncServiceL1GasPrice(t *testing.T) { func TestSyncServiceL1GasPrice(t *testing.T) {
service, _, _, err := newTestSyncService(true) service, _, _, err := newTestSyncService(true)
setupMockClient(service, map[string]interface{}{}) setupMockClient(service, map[string]interface{}{})
service.RollupGpo = gasprice.NewRollupOracle(big.NewInt(0), big.NewInt(0))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
gasBefore, err := service.RollupGpo.SuggestDataPrice(context.Background()) gasBefore, err := service.RollupGpo.SuggestL1GasPrice(context.Background())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
...@@ -172,16 +171,53 @@ func TestSyncServiceL1GasPrice(t *testing.T) { ...@@ -172,16 +171,53 @@ func TestSyncServiceL1GasPrice(t *testing.T) {
// Update the gas price // Update the gas price
service.updateL1GasPrice() service.updateL1GasPrice()
gasAfter, err := service.RollupGpo.SuggestDataPrice(context.Background()) gasAfter, err := service.RollupGpo.SuggestL1GasPrice(context.Background())
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if gasAfter.Cmp(big.NewInt(100*int64(params.GWei))) != 0 { if gasAfter.Cmp(core.RoundL1GasPrice(big.NewInt(1))) != 0 {
t.Fatal("expected 100 gas price, got", gasAfter) t.Fatal("expected 100 gas price, got", gasAfter)
} }
} }
func TestSyncServiceL2GasPrice(t *testing.T) {
service, _, _, err := newTestSyncService(true)
if err != nil {
t.Fatal(err)
}
service.enableL2GasPolling = true
service.gpoAddress = common.HexToAddress("0xF20b338752976878754518183873602902360704")
price, err := service.RollupGpo.SuggestL2GasPrice(context.Background())
if err != nil {
t.Fatal("Cannot fetch execution price")
}
if price.Cmp(common.Big0) != 0 {
t.Fatal("Incorrect gas price")
}
state, err := service.bc.State()
if err != nil {
t.Fatal("Cannot get state db")
}
l2GasPrice := big.NewInt(100000001)
state.SetState(service.gpoAddress, l2GasPriceSlot, common.BigToHash(l2GasPrice))
root, _ := state.Commit(false)
service.updateL2GasPrice(&root)
post, err := service.RollupGpo.SuggestL2GasPrice(context.Background())
if err != nil {
t.Fatal("Cannot fetch execution price")
}
if l2GasPrice.Cmp(post) != 0 {
t.Fatal("Gas price not updated")
}
}
// 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)
...@@ -327,6 +363,7 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e ...@@ -327,6 +363,7 @@ func newTestSyncService(isVerifier bool) (*SyncService, chan core.NewTxsEvent, e
return nil, nil, nil, fmt.Errorf("Cannot initialize syncservice: %w", err) return nil, nil, nil, fmt.Errorf("Cannot initialize syncservice: %w", err)
} }
service.RollupGpo = gasprice.NewRollupOracle(big.NewInt(0), big.NewInt(0))
txCh := make(chan core.NewTxsEvent, 1) txCh := make(chan core.NewTxsEvent, 1)
sub := service.SubscribeNewTxsEvent(txCh) sub := service.SubscribeNewTxsEvent(txCh)
...@@ -443,5 +480,6 @@ func (m *mockClient) SyncStatus() (*SyncStatus, error) { ...@@ -443,5 +480,6 @@ func (m *mockClient) SyncStatus() (*SyncStatus, error) {
} }
func (m *mockClient) GetL1GasPrice() (*big.Int, error) { func (m *mockClient) GetL1GasPrice() (*big.Int, error) {
return big.NewInt(100 * int64(params.GWei)), nil price := core.RoundL1GasPrice(big.NewInt(2))
return price, nil
} }
...@@ -6,6 +6,8 @@ ETH1_CONFIRMATION_DEPTH=0 ...@@ -6,6 +6,8 @@ ETH1_CONFIRMATION_DEPTH=0
ROLLUP_CLIENT_HTTP= ROLLUP_CLIENT_HTTP=
ROLLUP_STATE_DUMP_PATH= ROLLUP_STATE_DUMP_PATH=
ROLLUP_POLL_INTERVAL_FLAG=500ms ROLLUP_POLL_INTERVAL_FLAG=500ms
ROLLUP_ENABLE_L2_GAS_POLLING=true
# ROLLUP_ENFORCE_FEES=
RPC_ENABLE=true RPC_ENABLE=true
RPC_ADDR=0.0.0.0 RPC_ADDR=0.0.0.0
......
/**
* Fee related serialization and deserialization
*/
import { BigNumber } from 'ethers'
import { remove0x } from './common'
const hundredMillion = BigNumber.from(100_000_000)
const txDataZeroGas = 4
const txDataNonZeroGasEIP2028 = 16
const overhead = 4200
export interface EncodableL2GasLimit {
data: Buffer | string
l1GasPrice: BigNumber | number
l2GasLimit: BigNumber | number
l2GasPrice: BigNumber | number
}
function encode(input: EncodableL2GasLimit): BigNumber {
const { data } = input
let { l1GasPrice, l2GasLimit, l2GasPrice } = input
if (typeof l1GasPrice === 'number') {
l1GasPrice = BigNumber.from(l1GasPrice)
}
if (typeof l2GasLimit === 'number') {
l2GasLimit = BigNumber.from(l2GasLimit)
}
if (typeof l2GasPrice === 'number') {
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 l1Fee = l1GasPrice.mul(l1GasLimit)
const l2Fee = l2GasLimit.mul(l2GasPrice)
return l1Fee.add(l2Fee)
}
function decode(fee: BigNumber | number): BigNumber {
if (typeof fee === 'number') {
fee = BigNumber.from(fee)
}
return fee.mod(hundredMillion)
}
export const L2GasLimit = {
encode,
decode,
}
export function verifyL2GasPrice(gasPrice: BigNumber | number): boolean {
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 zeroesCost = zeroes * txDataZeroGas
const onesCost = ones * txDataNonZeroGasEIP2028
const gasLimit = zeroesCost + onesCost + overhead
return gasLimit
}
export function zeroesAndOnes(data: Buffer | string): Array<number> {
if (typeof data === 'string') {
data = Buffer.from(remove0x(data), 'hex')
}
let zeros = 0
let ones = 0
for (const byte of data) {
if (byte === 0) {
zeros++
} else {
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)
}
...@@ -5,3 +5,4 @@ export * from './l2context' ...@@ -5,3 +5,4 @@ export * from './l2context'
export * from './events' export * from './events'
export * from './batches' export * from './batches'
export * from './bcfg' export * from './bcfg'
export * from './fees'
import { expect } from '../setup'
import * as fees from '../../src/fees'
import { BigNumber } from 'ethers'
describe('Fees', () => {
it('should count zeros and ones', () => {
const cases = [
{ input: Buffer.from('0001', 'hex'), zeros: 1, ones: 1 },
{ input: '0x0001', zeros: 1, ones: 1 },
{ input: '0x', zeros: 0, ones: 0 },
{ input: '0x1111', zeros: 0, ones: 2 },
]
for (const test of cases) {
const [zeros, ones] = fees.zeroesAndOnes(test.input)
zeros.should.eq(test.zeros)
ones.should.eq(test.ones)
}
})
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', () => {
const rollupFeesTests = [
{
name: 'simple',
dataLen: 10,
l1GasPrice: 100_000_000,
l2GasPrice: 100_000_001,
l2GasLimit: 437118,
error: false,
},
{
name: 'zero-l2-gasprice',
dataLen: 10,
l1GasPrice: 100_000_000,
l2GasPrice: 0,
l2GasLimit: 196205,
error: true,
},
{
name: 'one-l2-gasprice',
dataLen: 10,
l1GasPrice: 100_000_000,
l2GasPrice: 1,
l2GasLimit: 196205,
error: false,
},
{
name: 'zero-l1-gasprice',
dataLen: 10,
l1GasPrice: 0,
l2GasPrice: 100_000_001,
l2GasLimit: 196205,
error: false,
},
{
name: 'one-l1-gasprice',
dataLen: 10,
l1GasPrice: 1,
l2GasPrice: 23254,
l2GasLimit: 23255,
error: true,
},
]
for (const test of rollupFeesTests) {
it(`should pass for ${test.name} case`, () => {
const data = Buffer.alloc(test.dataLen)
let got
let err = false
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))
}
})
}
})
})
...@@ -165,13 +165,11 @@ export const smockit = async ( ...@@ -165,13 +165,11 @@ export const smockit = async (
// We attach a wallet to the contract so that users can send transactions *from* a smock. // We attach a wallet to the contract so that users can send transactions *from* a smock.
await hre.network.provider.request({ await hre.network.provider.request({
method: 'hardhat_impersonateAccount', method: 'hardhat_impersonateAccount',
params: [contract.address] params: [contract.address],
}) })
// Now we actually get the signer and attach it to the mock. // Now we actually get the signer and attach it to the mock.
contract.wallet = await (hre as any).ethers.getSigner( contract.wallet = await (hre as any).ethers.getSigner(contract.address)
contract.address
)
// Start by smocking the fallback. // Start by smocking the fallback.
contract.smocked = { contract.smocked = {
......
...@@ -11,16 +11,14 @@ describe('[smock]: sending transactions from smock contracts', () => { ...@@ -11,16 +11,14 @@ describe('[smock]: sending transactions from smock contracts', () => {
let TestHelpers_SenderAssertions: Contract let TestHelpers_SenderAssertions: Contract
before(async () => { before(async () => {
TestHelpers_SenderAssertions = await( TestHelpers_SenderAssertions = await (
await ethers.getContractFactory( await ethers.getContractFactory('TestHelpers_SenderAssertions')
'TestHelpers_SenderAssertions'
)
).deploy() ).deploy()
}) })
it('should attach a signer for a mock with a random address', async () => { it('should attach a signer for a mock with a random address', async () => {
const mock = await smockit('TestHelpers_BasicReturnContract') const mock = await smockit('TestHelpers_BasicReturnContract')
expect( expect(
await TestHelpers_SenderAssertions.connect(mock.wallet).getSender() await TestHelpers_SenderAssertions.connect(mock.wallet).getSender()
).to.equal(mock.address) ).to.equal(mock.address)
...@@ -28,7 +26,7 @@ describe('[smock]: sending transactions from smock contracts', () => { ...@@ -28,7 +26,7 @@ describe('[smock]: sending transactions from smock contracts', () => {
it('should attach a signer for a mock with a fixed address', async () => { it('should attach a signer for a mock with a fixed address', async () => {
const mock = await smockit('TestHelpers_BasicReturnContract', { const mock = await smockit('TestHelpers_BasicReturnContract', {
address: '0x1234123412341234123412341234123412341234' address: '0x1234123412341234123412341234123412341234',
}) })
expect( expect(
......
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