Skip to content

Commit

Permalink
go: add utils module
Browse files Browse the repository at this point in the history
  • Loading branch information
tynes committed Jun 17, 2021
1 parent 5c89c45 commit 96275ad
Show file tree
Hide file tree
Showing 5 changed files with 383 additions and 0 deletions.
21 changes: 21 additions & 0 deletions go/utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# utils

This package is meant to hold utilities used by
[Optimistic Ethereum](https://github.com/ethereum-optimism/optimism) written in
Golang.

## Packages

### Fees

Package fees includes helpers for dealing with fees on Optimistic Ethereum

#### `EncodeTxGasLimit(data []byte, l1GasPrice, l2GasLimit, l2GasPrice *big.Int) *big.Int`

Encodes `tx.gasLimit` based on the variables that are used to determine it.

#### `DecodeL2GasLimit(gasLimit *big.Int) *big.Int`

Accepts the return value of `eth_estimateGas` and decodes the L2 gas limit that
is encoded in the return value. This is the gas limit that is passed to the user
contract within the OVM.
113 changes: 113 additions & 0 deletions go/utils/fees/rollup_fee.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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

// feeScalar is used to scale the calculations in EncodeL2GasLimit
// to prevent them from being too large
const feeScalar uint64 = 10_000_000

// 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)

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
// 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
// roundedL2GasLimit = ceilmod(l2GasLimit, 10_000)
// l1Fee = l1GasPrice * l1GasLimit
// l2Fee = l2GasPrice * roundedL2GasLimit
// sum = l1Fee + l2Fee
// scaled = sum / scalar
// 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, roundedL2GasLimit)
sum := new(big.Int).Add(l1Fee, l2Fee)
scaled := new(big.Int).Div(sum, bigFeeScalar)
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 {
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
// 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
}
104 changes: 104 additions & 0 deletions go/utils/fees/rollup_fee_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package fees

import (
"math/big"
"testing"

"github.com/ethereum/go-ethereum/params"
)

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 {
dataLen int
l1GasPrice uint64
l2GasLimit uint64
l2GasPrice uint64
}{
"simple": {
dataLen: 10,
l1GasPrice: params.GWei,
l2GasLimit: 437118,
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: 99_970_000,
l2GasPrice: params.GWei,
},
"larger-divisor": {
dataLen: 10,
l1GasPrice: 0,
l2GasLimit: 10,
l2GasPrice: 0,
},
}

func TestCalculateRollupFee(t *testing.T) {
for name, tt := range feeTests {
t.Run(name, func(t *testing.T) {
data := make([]byte, tt.dataLen)
l1GasPrice := new(big.Int).SetUint64(tt.l1GasPrice)
l2GasLimit := new(big.Int).SetUint64(tt.l2GasLimit)
l2GasPrice := new(big.Int).SetUint64(tt.l2GasPrice)

fee := EncodeTxGasLimit(data, l1GasPrice, l2GasLimit, l2GasPrice)
decodedGasLimit := DecodeL2GasLimit(fee)
roundedL2GasLimit := Ceilmod(l2GasLimit, BigTenThousand)
if roundedL2GasLimit.Cmp(decodedGasLimit) != 0 {
t.Errorf("rollup fee check failed: expected %d, got %d", l2GasLimit.Uint64(), decodedGasLimit)
}
})
}
}
5 changes: 5 additions & 0 deletions go/utils/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/ethereum-optimism/optimism/go/utils

go 1.15

require github.com/ethereum/go-ethereum v1.9.10
Loading

0 comments on commit 96275ad

Please sign in to comment.