Skip to content

Commit

Permalink
feat: fees v3 (#999)
Browse files Browse the repository at this point in the history
* core-utils: fee impl v3

* l2geth: fees v3 impl

* integration-tests: update for fees v3

* chore: add changeset

* fix: typo

* integration-tests: fix and generalize

* fees: update fee scalar

* l2geth: check gas in the mempool behind usingovm

* tests: fix up

* l2geth: remove dead var

* truffle: fix config
  • Loading branch information
tynes authored Jun 2, 2021
1 parent 8082d16 commit c2b6e14
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 46 deletions.
7 changes: 7 additions & 0 deletions .changeset/smooth-ears-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@eth-optimism/integration-tests': patch
'@eth-optimism/l2geth': patch
'@eth-optimism/core-utils': patch
---

Implement the latest fee spec such that the L2 gas limit is scaled and the tx.gasPrice/tx.gasLimit show correctly in metamask
3 changes: 2 additions & 1 deletion examples/truffle/truffle-config-ovm.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ module.exports = {
host: '127.0.0.1',
port: 8545,
gasPrice: 0,
gas: 54180127,
}
},
compilers: {
Expand All @@ -31,4 +32,4 @@ module.exports = {
}
}
}
}
}
8 changes: 4 additions & 4 deletions integration-tests/test/fee-payment.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
import { BigNumber, utils } from 'ethers'
import { OptimismEnv } from './shared/env'
import { TxGasLimit } from '@eth-optimism/core-utils'
import { TxGasLimit, TxGasPrice } from '@eth-optimism/core-utils'

describe('Fee Payment Integration Tests', async () => {
let env: OptimismEnv
Expand All @@ -13,9 +13,9 @@ describe('Fee Payment Integration Tests', async () => {
env = await OptimismEnv.new()
})

it('Should return a gasPrice of 1 wei', async () => {
it(`Should return a gasPrice of ${TxGasPrice.toString()} wei`, async () => {
const gasPrice = await env.l2Wallet.getGasPrice()
expect(gasPrice.eq(1))
expect(gasPrice).to.deep.eq(TxGasPrice)
})

it('Should estimateGas with recoverable L2 gasLimit', async () => {
Expand All @@ -28,7 +28,7 @@ describe('Fee Payment Integration Tests', async () => {
utils.parseEther('0.5')
)
const executionGas = await (env.ovmEth
.provider as any).send('eth_estimateExecutionGas', [tx])
.provider as any).send('eth_estimateExecutionGas', [tx, true])
const decoded = TxGasLimit.decode(gas)
expect(BigNumber.from(executionGas)).deep.eq(decoded)
})
Expand Down
4 changes: 2 additions & 2 deletions integration-tests/test/native-eth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ describe('Native ETH Integration Tests', async () => {
const amount = utils.parseEther('0.5')
const addr = '0x' + '1234'.repeat(10)
const gas = await env.ovmEth.estimateGas.transfer(addr, amount)
expect(gas).to.be.deep.eq(BigNumber.from(0x0ef897216d))
expect(gas).to.be.deep.eq(BigNumber.from(6430020))
})

it('Should estimate gas for ETH withdraw', async () => {
const amount = utils.parseEther('0.5')
const gas = await env.ovmEth.estimateGas.withdraw(amount)
expect(gas).to.be.deep.eq(BigNumber.from(61400489396))
expect(gas).to.be.deep.eq(BigNumber.from(6140049))
})
})

Expand Down
9 changes: 5 additions & 4 deletions integration-tests/test/rpc.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe('Basic RPC tests', () => {
gasPrice: TxGasPrice,
}
const fee = tx.gasPrice.mul(tx.gasLimit)
const gasLimit = 59300000001
const gasLimit = 5920001

await expect(env.l2Wallet.sendTransaction(tx)).to.be.rejectedWith(
`fee too low: ${fee}, use at least tx.gasLimit = ${gasLimit} and tx.gasPrice = ${TxGasPrice.toString()}`
Expand Down Expand Up @@ -213,7 +213,7 @@ describe('Basic RPC tests', () => {
it('correctly exposes revert data for contract calls', async () => {
const req: TransactionRequest = {
...revertingTx,
gasLimit: 59808999999, // override gas estimation
gasLimit: 5980899, // override gas estimation
}

const tx = await wallet.sendTransaction(req)
Expand All @@ -236,7 +236,7 @@ describe('Basic RPC tests', () => {
it('correctly exposes revert data for contract creations', async () => {
const req: TransactionRequest = {
...revertingDeployTx,
gasLimit: 177008999999, // override gas estimation
gasLimit: 17700899, // override gas estimation
}

const tx = await wallet.sendTransaction(req)
Expand Down Expand Up @@ -355,7 +355,7 @@ describe('Basic RPC tests', () => {
to: DEFAULT_TRANSACTION.to,
value: 0,
})
expect(estimate).to.be.eq(0x0dce9004c7)
expect(estimate).to.be.eq(5920012)
})

it('should return a gas estimate that grows with the size of data', async () => {
Expand All @@ -373,6 +373,7 @@ describe('Basic RPC tests', () => {
const estimate = await l2Provider.estimateGas(tx)
const l2Gaslimit = await l2Provider.send('eth_estimateExecutionGas', [
tx,
true,
])

const decoded = TxGasLimit.decode(estimate)
Expand Down
17 changes: 10 additions & 7 deletions l2geth/core/tx_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,8 @@ var (
)

var (
evictionInterval = time.Minute // Time interval to check for evictable transactions
statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats
gwei = big.NewInt(params.GWei) // 1 gwei, used as a flag for "rollup" transactions
evictionInterval = time.Minute // Time interval to check for evictable transactions
statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats
)

var (
Expand Down Expand Up @@ -540,10 +539,14 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
}

// 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,
// which indicates a "rollup" transaction that's paying for its data.
if pool.currentMaxGas < tx.L2Gas() && tx.GasPrice().Cmp(gwei) != 0 {
return ErrGasLimit
if vm.UsingOVM {
if pool.currentMaxGas < tx.L2Gas() {
return ErrGasLimit
}
} else {
if pool.currentMaxGas < tx.Gas() {
return ErrGasLimit
}
}

// Make sure the transaction is signed properly
Expand Down
3 changes: 2 additions & 1 deletion l2geth/core/types/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rollup/fees"
)

//go:generate gencodec -type txdata -field-override txdataMarshaling -out gen_tx_json.go
Expand Down Expand Up @@ -225,7 +226,7 @@ func (tx *Transaction) UnmarshalJSON(input []byte) error {

func (tx *Transaction) Data() []byte { return common.CopyBytes(tx.data.Payload) }
func (tx *Transaction) Gas() uint64 { return tx.data.GasLimit }
func (tx *Transaction) L2Gas() uint64 { return tx.data.GasLimit % 100_000_000 }
func (tx *Transaction) L2Gas() uint64 { return fees.DecodeL2GasLimitU64(tx.data.GasLimit) }
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) Nonce() uint64 { return tx.data.AccountNonce }
Expand Down
12 changes: 10 additions & 2 deletions l2geth/internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1148,9 +1148,17 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args CallArgs) (h

// 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) {
func (s *PublicBlockChainAPI) EstimateExecutionGas(ctx context.Context, args CallArgs, round *bool) (hexutil.Uint64, error) {
blockNrOrHash := rpc.BlockNumberOrHashWithNumber(rpc.PendingBlockNumber)
return legacyDoEstimateGas(ctx, s.b, args, blockNrOrHash, s.b.RPCGasCap())
estimate, err := legacyDoEstimateGas(ctx, s.b, args, blockNrOrHash, s.b.RPCGasCap())
if err != nil {
return estimate, err
}
if round != nil && *round {
rounded := fees.Ceilmod(new(big.Int).SetUint64(uint64(estimate)), fees.BigTenThousand)
estimate = (hexutil.Uint64)(rounded.Uint64())
}
return estimate, nil
}

// ExecutionResult groups all structured logs emitted by the EVM
Expand Down
47 changes: 33 additions & 14 deletions l2geth/rollup/fees/rollup_fee.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,17 @@ package fees
import (
"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 + 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
const feeScalar uint64 = 10_000_000

// TxGasPrice is a constant that determines the result of `eth_gasPrice`
// It is scaled upwards by 50%
Expand All @@ -26,7 +24,10 @@ 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)

const tenThousand = 10000

var BigTenThousand = new(big.Int).SetUint64(tenThousand)

// 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
Expand All @@ -40,32 +41,50 @@ var bigHundredMillion = new(big.Int).SetUint64(hundredMillion)
// 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
// roundedL2GasLimit = ceilmod(l2GasLimit, 10_000)
// l1Fee = l1GasPrice * l1GasLimit
// l2Fee = l2GasPrice * l2GasLimit
// l2Fee = l2GasPrice * roundedL2GasLimit
// sum = l1Fee + l2Fee
// scaled = sum / scalar
// rounded = ceilmod(scaled, hundredMillion)
// result = rounded + l2GasLimit
// rounded = ceilmod(scaled, tenThousand)
// roundedScaledL2GasLimit = roundedL2GasLimit / tenThousand
// result = rounded + roundedScaledL2GasLimit
// 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)
roundedL2GasLimit := Ceilmod(l2GasLimit, BigTenThousand)
l1Fee := new(big.Int).Mul(l1GasPrice, l1GasLimit)
l2Fee := new(big.Int).Mul(l2GasPrice, l2GasLimit)
l2Fee := new(big.Int).Mul(l2GasPrice, roundedL2GasLimit)
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)
rounded := Ceilmod(scaled, BigTenThousand)
roundedScaledL2GasLimit := new(big.Int).Div(roundedL2GasLimit, BigTenThousand)
result := new(big.Int).Add(rounded, roundedScaledL2GasLimit)
return result
}

func Ceilmod(a, b *big.Int) *big.Int {
remainder := new(big.Int).Mod(a, b)
if remainder.Cmp(common.Big0) == 0 {
return a
}
sum := new(big.Int).Add(a, b)
rounded := new(big.Int).Sub(sum, remainder)
return rounded
}

// 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)
scaled := new(big.Int).Mod(gasLimit, BigTenThousand)
return new(big.Int).Mul(scaled, BigTenThousand)
}

func DecodeL2GasLimitU64(gasLimit uint64) uint64 {
scaled := gasLimit % tenThousand
return scaled * tenThousand
}

// calculateL1GasLimit computes the L1 gasLimit based on the calldata and
Expand Down
5 changes: 3 additions & 2 deletions l2geth/rollup/fees/rollup_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ var feeTests = map[string]struct {
"max-gaslimit": {
dataLen: 10,
l1GasPrice: params.GWei,
l2GasLimit: 99999999,
l2GasLimit: 99_970_000,
l2GasPrice: params.GWei,
},
"larger-divisor": {
Expand All @@ -95,7 +95,8 @@ func TestCalculateRollupFee(t *testing.T) {

fee := EncodeTxGasLimit(data, l1GasPrice, l2GasLimit, l2GasPrice)
decodedGasLimit := DecodeL2GasLimit(fee)
if l2GasLimit.Cmp(decodedGasLimit) != 0 {
roundedL2GasLimit := Ceilmod(l2GasLimit, BigTenThousand)
if roundedL2GasLimit.Cmp(decodedGasLimit) != 0 {
t.Errorf("rollup fee check failed: expected %d, got %d", l2GasLimit.Uint64(), decodedGasLimit)
}
})
Expand Down
32 changes: 25 additions & 7 deletions packages/core-utils/src/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import { BigNumber } from 'ethers'
import { remove0x } from './common'

const hundredMillion = BigNumber.from(100_000_000)
const feeScalar = 1000
const feeScalar = 10_000_000
export const TxGasPrice = BigNumber.from(feeScalar + feeScalar / 2)
const txDataZeroGas = 4
const txDataNonZeroGasEIP2028 = 16
const overhead = 4200 + 200 * txDataNonZeroGasEIP2028
const tenThousand = BigNumber.from(10_000)

export interface EncodableL2GasLimit {
data: Buffer | string
Expand All @@ -32,28 +33,45 @@ function encode(input: EncodableL2GasLimit): BigNumber {
l2GasPrice = BigNumber.from(l2GasPrice)
}
const l1GasLimit = calculateL1GasLimit(data)
const roundedL2GasLimit = ceilmod(l2GasLimit, tenThousand)
const l1Fee = l1GasLimit.mul(l1GasPrice)
const l2Fee = l2GasLimit.mul(l2GasPrice)
const l2Fee = roundedL2GasLimit.mul(l2GasPrice)
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)
const rounded = ceilmod(scaled, tenThousand)
const roundedScaledL2GasLimit = roundedL2GasLimit.div(tenThousand)
return rounded.add(roundedScaledL2GasLimit)
}

function decode(fee: BigNumber | number): BigNumber {
if (typeof fee === 'number') {
fee = BigNumber.from(fee)
}
return fee.mod(hundredMillion)
const scaled = fee.mod(tenThousand)
return scaled.mul(tenThousand)
}

export const TxGasLimit = {
encode,
decode,
}

export function ceilmod(a: BigNumber | number, b: BigNumber | number) {
if (typeof a === 'number') {
a = BigNumber.from(a)
}
if (typeof b === 'number') {
b = BigNumber.from(b)
}
const remainder = a.mod(b)
if (remainder.eq(0)) {
return a
}
const sum = a.add(b)
const rounded = sum.sub(remainder)
return rounded
}

export function calculateL1GasLimit(data: string | Buffer): BigNumber {
const [zeroes, ones] = zeroesAndOnes(data)
const zeroesCost = zeroes * txDataZeroGas
Expand Down
5 changes: 3 additions & 2 deletions packages/core-utils/test/fees/fees.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe('Fees', () => {
dataLen: 10,
l1GasPrice: utils.parseUnits('5', 'ether'),
l2GasPrice: utils.parseUnits('5', 'ether'),
l2GasLimit: 10 ** 8 - 1,
l2GasLimit: 99_970_000,
},
{
name: 'zero-l2-gasprice',
Expand Down Expand Up @@ -113,7 +113,8 @@ describe('Fees', () => {
})

const decoded = fees.TxGasLimit.decode(got)
expect(decoded).to.deep.eq(BigNumber.from(test.l2GasLimit))
const roundedL2GasLimit = fees.ceilmod(test.l2GasLimit, 10_000)
expect(decoded).to.deep.eq(roundedL2GasLimit)
})
}
})
Expand Down

0 comments on commit c2b6e14

Please sign in to comment.