Commit 42932378 authored by Georgios Konstantopoulos's avatar Georgios Konstantopoulos Committed by GitHub

feat(l2geth): sequencer fee pricing (#45)

* feat(l2geth): sequencer fee pricing

https://github.com/ethereum-optimism/go-ethereum/pull/273
https://github.com/ethereum-optimism/go-ethereum/pull/306
https://github.com/ethereum-optimism/go-ethereum/pull/305

* test(l1-l2-comms): fund l2 address before starting the test

* test(rpc): ensure that fee paid grows with data size

* test: add helper class for instantiation optimistic envs

* refactor: use the env container for l1<>l2 tests

* refactor: use the env container for queue ingestion and proxy tests

* fix(erc20-test): take fee payments into account

* refactor: use the env container for fee payment tests

* refactor: use the env container for native eth tests
parent 0de7a2f9
import { expect } from 'chai'
/* Imports: External */
import { Contract, ContractFactory, Wallet, providers } from 'ethers'
import { Watcher } from '@eth-optimism/core-utils'
import {
initWatcher,
waitForXDomainTransaction,
Direction,
} from './shared/watcher-utils'
import { getContractFactory } from '@eth-optimism/contracts'
import { Contract, ContractFactory, utils } from 'ethers'
import { Direction } from './shared/watcher-utils'
/* Imports: Internal */
import l1SimpleStorageJson from '../artifacts/contracts/SimpleStorage.sol/SimpleStorage.json'
import l2SimpleStorageJson from '../artifacts-ovm/contracts/SimpleStorage.sol/SimpleStorage.json'
import { OptimismEnv } from './shared/env'
describe('Basic L1<>L2 Communication', async () => {
let l1Wallet: Wallet
let l2Wallet: Wallet
let l1Provider: providers.JsonRpcProvider
let l2Provider: providers.JsonRpcProvider
let AddressManager: Contract
let Factory__L1SimpleStorage: ContractFactory
let Factory__L2SimpleStorage: ContractFactory
let L1CrossDomainMessenger: Contract
let L2CrossDomainMessenger: Contract
let watcher: Watcher
let L1SimpleStorage: Contract
let L2SimpleStorage: Contract
let env: OptimismEnv
before(async () => {
const httpPort = 8545
const l1HttpPort = 9545
l1Provider = new providers.JsonRpcProvider(`http://localhost:${l1HttpPort}`)
l2Provider = new providers.JsonRpcProvider(`http://localhost:${httpPort}`)
l1Wallet = new Wallet(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
l1Provider
)
l2Wallet = Wallet.createRandom().connect(l2Provider)
const addressManagerAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
AddressManager = getContractFactory('Lib_AddressManager')
.connect(l1Wallet)
.attach(addressManagerAddress)
env = await OptimismEnv.new()
Factory__L1SimpleStorage = new ContractFactory(
l1SimpleStorageJson.abi,
l1SimpleStorageJson.bytecode,
l1Wallet
env.l1Wallet
)
Factory__L2SimpleStorage = new ContractFactory(
l2SimpleStorageJson.abi,
l2SimpleStorageJson.bytecode,
l2Wallet
env.l2Wallet
)
watcher = await initWatcher(l1Provider, l2Provider, AddressManager)
L1CrossDomainMessenger = getContractFactory('iOVM_L1CrossDomainMessenger')
.connect(l1Wallet)
.attach(watcher.l1.messengerAddress)
L2CrossDomainMessenger = getContractFactory('iOVM_L2CrossDomainMessenger')
.connect(l2Wallet)
.attach(watcher.l2.messengerAddress)
})
beforeEach(async () => {
......@@ -78,19 +41,16 @@ describe('Basic L1<>L2 Communication', async () => {
const value = `0x${'77'.repeat(32)}`
// Send L2 -> L1 message.
const transaction = await L2CrossDomainMessenger.sendMessage(
const transaction = await env.l2Messenger.sendMessage(
L1SimpleStorage.address,
L1SimpleStorage.interface.encodeFunctionData('setValue', [value]),
5000000,
{ gasLimit: 7000000 }
5000000
)
await waitForXDomainTransaction(watcher, transaction, Direction.L2ToL1)
await env.waitForXDomainTransaction(transaction, Direction.L2ToL1)
expect(await L1SimpleStorage.msgSender()).to.equal(
L1CrossDomainMessenger.address
)
expect(await L1SimpleStorage.xDomainSender()).to.equal(l2Wallet.address)
expect(await L1SimpleStorage.msgSender()).to.equal(env.l1Messenger.address)
expect(await L1SimpleStorage.xDomainSender()).to.equal(env.l2Wallet.address)
expect(await L1SimpleStorage.value()).to.equal(value)
expect((await L1SimpleStorage.totalCount()).toNumber()).to.equal(1)
})
......@@ -99,19 +59,16 @@ describe('Basic L1<>L2 Communication', async () => {
const value = `0x${'42'.repeat(32)}`
// Send L1 -> L2 message.
const transaction = await L1CrossDomainMessenger.sendMessage(
const transaction = await env.l1Messenger.sendMessage(
L2SimpleStorage.address,
L2SimpleStorage.interface.encodeFunctionData('setValue', [value]),
5000000,
{ gasLimit: 7000000 }
5000000
)
await waitForXDomainTransaction(watcher, transaction, Direction.L1ToL2)
await env.waitForXDomainTransaction(transaction, Direction.L1ToL2)
expect(await L2SimpleStorage.msgSender()).to.equal(
L2CrossDomainMessenger.address
)
expect(await L2SimpleStorage.xDomainSender()).to.equal(l1Wallet.address)
expect(await L2SimpleStorage.msgSender()).to.equal(env.l2Messenger.address)
expect(await L2SimpleStorage.xDomainSender()).to.equal(env.l1Wallet.address)
expect(await L2SimpleStorage.value()).to.equal(value)
expect((await L2SimpleStorage.totalCount()).toNumber()).to.equal(1)
})
......
import { Contract, ContractFactory, Wallet } from 'ethers'
import { ethers } from 'hardhat'
import { expect } from 'chai'
import { GWEI } from './shared/utils'
import { OptimismEnv } from './shared/env'
describe('Basic ERC20 interactions', async () => {
const initialAmount = 1000
......@@ -14,7 +16,8 @@ describe('Basic ERC20 interactions', async () => {
let ERC20: Contract
before(async () => {
wallet = Wallet.createRandom().connect(ethers.provider)
const env = await OptimismEnv.new()
wallet = env.l2Wallet
other = Wallet.createRandom().connect(ethers.provider)
Factory__ERC20 = await ethers.getContractFactory('ERC20', wallet)
})
......@@ -57,10 +60,16 @@ describe('Basic ERC20 interactions', async () => {
const transfer = await ERC20.transfer(other.address, 100)
const receipt = await transfer.wait()
// The expected fee paid is the value returned by eth_estimateGas gas multiplied
// by 1 gwei, since that's the value eth_gasPrice always returns
const expectedFeePaid = (
await ERC20.estimateGas.transfer(other.address, 100)
).mul(GWEI)
// There are two events from the transfer with the first being
// the fee of value 0 and the second of the value transfered (100)
// the ETH fee paid and the second of the value transfered (100)
expect(receipt.events.length).to.equal(2)
expect(receipt.events[0].args._value.toNumber()).to.equal(0)
expect(receipt.events[0].args._value).to.deep.equal(expectedFeePaid)
expect(receipt.events[1].args._from).to.equal(wallet.address)
expect(receipt.events[1].args._value.toNumber()).to.equal(100)
......
import chai, { expect } from 'chai'
import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
import { BigNumber, Contract, utils } from 'ethers'
import {
l1Provider,
l2Provider,
l1Wallet,
l2Wallet,
getGateway,
getAddressManager,
getOvmEth,
fundUser,
} from './shared/utils'
import { initWatcher } from './shared/watcher-utils'
import { BigNumber, utils } from 'ethers'
import { OptimismEnv } from './shared/env'
describe('Fee Payment Integration Tests', async () => {
let OVM_L1ETHGateway: Contract
let OVM_ETH: Contract
let AddressManager: Contract
let env: OptimismEnv
const other = '0x1234123412341234123412341234123412341234'
const amount = utils.parseEther('1')
before(async () => {
AddressManager = getAddressManager(l1Wallet)
OVM_L1ETHGateway = await getGateway(l1Wallet, AddressManager)
OVM_ETH = getOvmEth(l2Wallet)
const watcher = await initWatcher(l1Provider, l2Provider, AddressManager)
await fundUser(watcher, OVM_L1ETHGateway, amount)
env = await OptimismEnv.new()
})
it('Paying a nonzero but acceptable gasPrice fee', async () => {
// manually set the gas price because otherwise it's returned as 0
const gasPrice = BigNumber.from(1_000_000)
const amt = amount.div(2)
const amount = utils.parseEther('0.5')
const balanceBefore = await l2Wallet.getBalance()
const tx = await OVM_ETH.transfer(other, amt, { gasPrice })
const balanceBefore = await env.l2Wallet.getBalance()
const tx = await env.ovmEth.transfer(other, amount)
await tx.wait()
const balanceAfter = await l2Wallet.getBalance()
const balanceAfter = await env.l2Wallet.getBalance()
// TODO: 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
expect(balanceBefore.sub(balanceAfter)).to.be.deep.eq(
gasPrice.mul(tx.gasLimit).add(amt)
tx.gasPrice.mul(tx.gasLimit).add(amount)
)
})
it('sequencer rejects transaction with a non-multiple-of-1M gasPrice', async () => {
const gasPrice = BigNumber.from(1_000_000 - 1)
await expect(
OVM_ETH.transfer(other, 0, { gasPrice })
env.ovmEth.transfer(other, 0, { gasPrice })
).to.be.eventually.rejectedWith(
'Gas price must be a multiple of 1,000,000 wei'
)
......
import { expect } from 'chai'
import { BigNumber, Contract, Wallet, constants, providers } from 'ethers'
import {
getContractInterface,
getContractFactory,
} from '@eth-optimism/contracts'
import { Watcher } from '@eth-optimism/core-utils'
import {
Direction,
initWatcher,
waitForXDomainTransaction,
} from './shared/watcher-utils'
import {
l1Provider,
l2Provider,
l1Wallet,
l2Wallet,
getGateway,
getAddressManager,
getOvmEth,
PROXY_SEQUENCER_ENTRYPOINT_ADDRESS,
} from './shared/utils'
import { Wallet, utils, BigNumber } from 'ethers'
import { Direction } from './shared/watcher-utils'
describe('Native ETH Integration Tests', async () => {
let OVM_L1ETHGateway: Contract
let OVM_ETH: Contract
let AddressManager: Contract
let watcher: Watcher
import { PROXY_SEQUENCER_ENTRYPOINT_ADDRESS } from './shared/utils'
import { OptimismEnv } from './shared/env'
const BOB_PRIV_KEY =
'0x1234123412341234123412341234123412341234123412341234123412341234'
const l1bob = new Wallet(BOB_PRIV_KEY, l1Provider)
const l2bob = new Wallet(BOB_PRIV_KEY, l2Provider)
describe('Native ETH Integration Tests', async () => {
let env: OptimismEnv
let l1Bob: Wallet
let l2Bob: Wallet
const getBalances = async () => {
const l1UserBalance = await l1Wallet.getBalance()
const l2UserBalance = await l2Wallet.getBalance()
const getBalances = async (_env: OptimismEnv) => {
const l1UserBalance = await _env.l1Wallet.getBalance()
const l2UserBalance = await _env.l2Wallet.getBalance()
const l1BobBalance = await l1bob.getBalance()
const l2BobBalance = await l2bob.getBalance()
const l1BobBalance = await l1Bob.getBalance()
const l2BobBalance = await l2Bob.getBalance()
const sequencerBalance = await OVM_ETH.balanceOf(
const sequencerBalance = await _env.ovmEth.balanceOf(
PROXY_SEQUENCER_ENTRYPOINT_ADDRESS
)
const l1GatewayBalance = await l1Provider.getBalance(
OVM_L1ETHGateway.address
const l1GatewayBalance = await _env.l1Wallet.provider.getBalance(
_env.gateway.address
)
return {
......@@ -60,25 +35,21 @@ describe('Native ETH Integration Tests', async () => {
}
before(async () => {
AddressManager = getAddressManager(l1Wallet)
OVM_L1ETHGateway = await getGateway(l1Wallet, AddressManager)
OVM_ETH = getOvmEth(l2Wallet)
watcher = await initWatcher(l1Provider, l2Provider, AddressManager)
env = await OptimismEnv.new()
l1Bob = Wallet.createRandom().connect(env.l1Wallet.provider)
l2Bob = l1Bob.connect(env.l2Wallet.provider)
})
it('deposit', async () => {
const depositAmount = 10
const preBalances = await getBalances()
const { tx, receipt } = await waitForXDomainTransaction(
watcher,
OVM_L1ETHGateway.deposit({
value: depositAmount,
}),
const preBalances = await getBalances(env)
const { tx, receipt } = await env.waitForXDomainTransaction(
env.gateway.deposit({ value: depositAmount }),
Direction.L1ToL2
)
const l1FeePaid = receipt.gasUsed.mul(tx.gasPrice)
const postBalances = await getBalances()
const postBalances = await getBalances(env)
expect(postBalances.l1GatewayBalance).to.deep.eq(
preBalances.l1GatewayBalance.add(depositAmount)
......@@ -93,10 +64,9 @@ describe('Native ETH Integration Tests', async () => {
it('depositTo', async () => {
const depositAmount = 10
const preBalances = await getBalances()
const depositReceipts = await waitForXDomainTransaction(
watcher,
OVM_L1ETHGateway.depositTo(l2bob.address, {
const preBalances = await getBalances(env)
const depositReceipts = await env.waitForXDomainTransaction(
env.gateway.depositTo(l2Bob.address, {
value: depositAmount,
}),
Direction.L1ToL2
......@@ -105,7 +75,7 @@ describe('Native ETH Integration Tests', async () => {
const l1FeePaid = depositReceipts.receipt.gasUsed.mul(
depositReceipts.tx.gasPrice
)
const postBalances = await getBalances()
const postBalances = await getBalances(env)
expect(postBalances.l1GatewayBalance).to.deep.eq(
preBalances.l1GatewayBalance.add(depositAmount)
)
......@@ -118,26 +88,26 @@ describe('Native ETH Integration Tests', async () => {
})
it('withdraw', async () => {
const withdrawAmount = 3
const preBalances = await getBalances()
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances(env)
expect(
preBalances.l2UserBalance.gt(0),
'Cannot run withdrawal test before any deposits...'
)
await waitForXDomainTransaction(
watcher,
OVM_ETH.withdraw(withdrawAmount),
const receipts = await env.waitForXDomainTransaction(
env.ovmEth.withdraw(withdrawAmount),
Direction.L2ToL1
)
const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice)
const postBalances = await getBalances()
const postBalances = await getBalances(env)
expect(postBalances.l1GatewayBalance).to.deep.eq(
preBalances.l1GatewayBalance.sub(withdrawAmount)
)
expect(postBalances.l2UserBalance).to.deep.eq(
preBalances.l2UserBalance.sub(withdrawAmount)
preBalances.l2UserBalance.sub(withdrawAmount.add(fee))
)
expect(postBalances.l1UserBalance).to.deep.eq(
preBalances.l1UserBalance.add(withdrawAmount)
......@@ -145,28 +115,28 @@ describe('Native ETH Integration Tests', async () => {
})
it('withdrawTo', async () => {
const withdrawAmount = 3
const withdrawAmount = BigNumber.from(3)
const preBalances = await getBalances()
const preBalances = await getBalances(env)
expect(
preBalances.l2UserBalance.gt(0),
'Cannot run withdrawal test before any deposits...'
)
await waitForXDomainTransaction(
watcher,
OVM_ETH.withdrawTo(l1bob.address, withdrawAmount),
const receipts = await env.waitForXDomainTransaction(
env.ovmEth.withdrawTo(l1Bob.address, withdrawAmount),
Direction.L2ToL1
)
const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice)
const postBalances = await getBalances()
const postBalances = await getBalances(env)
expect(postBalances.l1GatewayBalance).to.deep.eq(
preBalances.l1GatewayBalance.sub(withdrawAmount)
)
expect(postBalances.l2UserBalance).to.deep.eq(
preBalances.l2UserBalance.sub(withdrawAmount)
preBalances.l2UserBalance.sub(withdrawAmount.add(fee))
)
expect(postBalances.l1BobBalance).to.deep.eq(
preBalances.l1BobBalance.add(withdrawAmount)
......@@ -174,29 +144,38 @@ describe('Native ETH Integration Tests', async () => {
})
it('deposit, transfer, withdraw', async () => {
const roundTripAmount = 3
const preBalances = await getBalances()
await waitForXDomainTransaction(
watcher,
OVM_L1ETHGateway.deposit({
value: roundTripAmount,
// 1. deposit
const amount = utils.parseEther('1')
await env.waitForXDomainTransaction(
env.gateway.deposit({
value: amount,
}),
Direction.L1ToL2
)
await OVM_ETH.transfer(l2bob.address, roundTripAmount)
// 2. trnsfer to another address
const other = Wallet.createRandom().connect(env.l2Wallet.provider)
const tx = await env.ovmEth.transfer(other.address, amount)
await tx.wait()
const l1BalanceBefore = await other
.connect(env.l1Wallet.provider)
.getBalance()
await waitForXDomainTransaction(
watcher,
OVM_ETH.connect(l2bob).withdraw(roundTripAmount),
// 3. do withdrawal
const withdrawnAmount = utils.parseEther('0.95')
const receipts = await env.waitForXDomainTransaction(
env.ovmEth.connect(other).withdraw(withdrawnAmount),
Direction.L2ToL1
)
const postBalances = await getBalances()
expect(postBalances.l1BobBalance).to.deep.eq(
preBalances.l1BobBalance.add(roundTripAmount)
)
// check that correct amount was withdrawn and that fee was charged
const fee = receipts.tx.gasLimit.mul(receipts.tx.gasPrice)
const l1BalanceAfter = await other
.connect(env.l1Wallet.provider)
.getBalance()
const l2BalanceAfter = await other.getBalance()
expect(l1BalanceAfter).to.deep.eq(l1BalanceBefore.add(withdrawnAmount))
expect(l2BalanceAfter).to.deep.eq(amount.sub(withdrawnAmount).sub(fee))
})
})
/* Imports: Internal */
import { getContractFactory } from '@eth-optimism/contracts'
import { injectL2Context } from './shared/l2provider'
import { sleep } from './shared/utils'
import { OptimismEnv } from './shared/env'
/* Imports: External */
import { Contract, Signer, Wallet, providers } from 'ethers'
import { providers } from 'ethers'
import { expect } from 'chai'
import { sleep } from './shared/utils'
// This test ensures that the transactions which get `enqueue`d get
// added to the L2 blocks by the Sync Service (which queries the DTL)
......@@ -14,36 +14,13 @@ describe('Queue Ingestion', () => {
const numTxs = 5
let startBlock: number
let endBlock: number
let l1Signer: Signer
let env: OptimismEnv
let l2Provider: providers.JsonRpcProvider
let addressResolver: Contract
let canonicalTransactionChain: Contract
const receipts = []
before(async () => {
const httpPort = 8545
const l1HttpPort = 9545
l2Provider = injectL2Context(
new providers.JsonRpcProvider(`http://localhost:${httpPort}`)
)
l1Signer = new Wallet(
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
new providers.JsonRpcProvider(`http://localhost:${l1HttpPort}`)
)
const addressResolverAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
addressResolver = getContractFactory('Lib_AddressManager')
.connect(l1Signer)
.attach(addressResolverAddress)
const ctcAddress = await addressResolver.getAddress(
'OVM_CanonicalTransactionChain'
)
canonicalTransactionChain = getContractFactory(
'OVM_CanonicalTransactionChain'
).attach(ctcAddress)
before(async () => {
env = await OptimismEnv.new()
l2Provider = injectL2Context(env.l2Wallet.provider as any)
})
// The transactions are enqueue'd with a `to` address of i.repeat(40)
......@@ -61,14 +38,11 @@ describe('Queue Ingestion', () => {
// the transaction to Layer 1
for (let i = 0; i < numTxs; i++) {
const input = ['0x' + `${i}`.repeat(40), 500_000, `0x0${i}`]
const calldata = canonicalTransactionChain.interface.encodeFunctionData(
'enqueue',
input
)
const calldata = env.ctc.interface.encodeFunctionData('enqueue', input)
const txResponse = await l1Signer.sendTransaction({
const txResponse = await env.l1Wallet.sendTransaction({
data: calldata,
to: canonicalTransactionChain.address,
to: env.ctc.address,
})
const receipt = await txResponse.wait()
......@@ -97,7 +71,7 @@ describe('Queue Ingestion', () => {
)
}
const from = await l1Signer.getAddress()
const from = await env.l1Wallet.getAddress()
// Keep track of an index into the receipts list and
// increment it for each block fetched.
let receiptIndex = 0
......
......@@ -2,15 +2,14 @@ import { expect } from 'chai'
import { ethers } from 'hardhat'
/* Imports: External */
import { Contract, Wallet, providers } from 'ethers'
import { Contract, Wallet } from 'ethers'
import { OptimismEnv } from './shared/env'
describe('Reading events from proxy contracts', () => {
let l2Provider: providers.JsonRpcProvider
let l2Wallet: Wallet
before(async () => {
const httpPort = 8545
l2Provider = new providers.JsonRpcProvider(`http://localhost:${httpPort}`)
l2Wallet = Wallet.createRandom().connect(l2Provider)
const env = await OptimismEnv.new()
l2Wallet = env.l2Wallet
})
// helper to query the transfers
......
import { ethers } from 'hardhat'
import { Wallet, BigNumber } from 'ethers'
import chai, { expect } from 'chai'
import { sleep } from './shared/utils'
import { sleep, l1Provider, l2Provider, GWEI } from './shared/utils'
import { injectL2Context } from './shared/l2provider'
import chaiAsPromised from 'chai-as-promised'
chai.use(chaiAsPromised)
......@@ -15,7 +14,7 @@ describe('Basic RPC tests', () => {
value: 0,
}
const provider = injectL2Context(ethers.provider)
const provider = injectL2Context(l2Provider)
const wallet = Wallet.createRandom().connect(provider)
describe('eth_sendRawTransaction', () => {
......@@ -115,25 +114,25 @@ describe('Basic RPC tests', () => {
})
describe('eth_gasPrice', () => {
it('gas price should be 0', async () => {
const expected = 0
const price = await provider.getGasPrice()
expect(price.toNumber()).to.equal(expected)
it('gas price should be 1 gwei', async () => {
expect(await provider.getGasPrice()).to.be.deep.equal(GWEI)
})
})
describe('eth_estimateGas', () => {
it('should return a gas estimate', async () => {
describe('eth_estimateGas (returns the fee)', () => {
it('should return a gas estimate that grows with the size of data', async () => {
const dataLen = [0, 2, 8, 64, 256]
let last = BigNumber.from(0)
// Repeat this test for a series of possible transaction sizes.
for (const size of [0, 2, 8, 64, 256]) {
const estimate = await provider.estimateGas({
for (const len of dataLen) {
const estimate = await l2Provider.estimateGas({
...DEFAULT_TRANSACTION,
data: '0x' + '00'.repeat(size),
data: '0x' + '00'.repeat(len),
})
// Ths gas estimation is set to always be the max gas limit - 1.
expect(estimate.toNumber()).to.be.eq(8999999)
expect(estimate.gt(last)).to.be.true
last = estimate
}
})
})
......
import { getContractFactory } from '@eth-optimism/contracts'
import { Watcher } from '@eth-optimism/core-utils'
import { Contract, utils, Wallet } from 'ethers'
import {
getAddressManager,
l1Provider,
l2Provider,
l1Wallet,
l2Wallet,
fundUser,
getOvmEth,
getGateway,
} from './utils'
import {
initWatcher,
CrossDomainMessagePair,
Direction,
waitForXDomainTransaction,
} from './watcher-utils'
import { TransactionResponse } from '@ethersproject/providers'
/// Helper class for instantiating a test environment with a funded account
export class OptimismEnv {
// L1 Contracts
addressManager: Contract
gateway: Contract
l1Messenger: Contract
ctc: Contract
// L2 Contracts
ovmEth: Contract
l2Messenger: Contract
// The L1 <> L2 State watcher
watcher: Watcher
// The wallets
l1Wallet: Wallet
l2Wallet: Wallet
constructor(args: any) {
this.addressManager = args.addressManager
this.gateway = args.gateway
this.l1Messenger = args.l1Messenger
this.ovmEth = args.ovmEth
this.l2Messenger = args.l2Messenger
this.watcher = args.watcher
this.l1Wallet = args.l1Wallet
this.l2Wallet = args.l2Wallet
this.ctc = args.ctc
}
static async new(): Promise<OptimismEnv> {
const addressManager = getAddressManager(l1Wallet)
const watcher = await initWatcher(l1Provider, l2Provider, addressManager)
const gateway = await getGateway(l1Wallet, addressManager)
// fund the user if needed
const balance = await l2Wallet.getBalance()
if (balance.isZero()) {
await fundUser(watcher, gateway, utils.parseEther('10'))
}
const ovmEth = getOvmEth(l2Wallet)
const l1Messenger = getContractFactory('iOVM_L1CrossDomainMessenger')
.connect(l1Wallet)
.attach(watcher.l1.messengerAddress)
const l2Messenger = getContractFactory('iOVM_L2CrossDomainMessenger')
.connect(l2Wallet)
.attach(watcher.l2.messengerAddress)
const ctcAddress = await addressManager.getAddress(
'OVM_CanonicalTransactionChain'
)
const ctc = getContractFactory('OVM_CanonicalTransactionChain')
.connect(l1Wallet)
.attach(ctcAddress)
return new OptimismEnv({
addressManager,
gateway,
ctc,
l1Messenger,
ovmEth,
l2Messenger,
watcher,
l1Wallet,
l2Wallet,
})
}
async waitForXDomainTransaction(
tx: Promise<TransactionResponse> | TransactionResponse,
direction: Direction
): Promise<CrossDomainMessagePair> {
return waitForXDomainTransaction(this.watcher, tx, direction)
}
}
......@@ -13,6 +13,8 @@ import {
} from 'ethers'
import { Direction, waitForXDomainTransaction } from './watcher-utils'
export const GWEI = BigNumber.from(1e9)
// The hardhat instance
const l1HttpPort = 9545
export const l1Provider = new providers.JsonRpcProvider(
......
......@@ -3,7 +3,7 @@ import {
TransactionReceipt,
TransactionResponse,
} from '@ethersproject/providers'
import { Watcher, Layer } from '@eth-optimism/core-utils'
import { Watcher } from '@eth-optimism/core-utils'
import { Contract, Transaction } from 'ethers'
......
......@@ -31,7 +31,7 @@ import (
)
const (
ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rollup:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0"
ipcAPIs = "admin:1.0 debug:1.0 eth:1.0 ethash:1.0 miner:1.0 net:1.0 personal:1.0 rollup:1.0 rollup_personal:1.0 rpc:1.0 shh:1.0 txpool:1.0 web3:1.0"
httpAPIs = "eth:1.0 net:1.0 rpc:1.0 web3:1.0"
)
......
......@@ -164,6 +164,7 @@ var (
utils.RollupStateDumpPathFlag,
utils.RollupDiffDbFlag,
utils.RollupMaxCalldataSizeFlag,
utils.RollupL1GasPriceFlag,
}
rpcFlags = []cli.Flag{
......
......@@ -78,6 +78,7 @@ var AppHelpFlagGroups = []flagGroup{
utils.RollupStateDumpPathFlag,
utils.RollupDiffDbFlag,
utils.RollupMaxCalldataSizeFlag,
utils.RollupL1GasPriceFlag,
},
},
{
......
......@@ -879,6 +879,12 @@ var (
Value: eth.DefaultConfig.Rollup.MaxCallDataSize,
EnvVar: "ROLLUP_MAX_CALLDATA_SIZE",
}
RollupL1GasPriceFlag = BigFlag{
Name: "rollup.l1gasprice",
Usage: "The L1 gas price to use for the sequencer fees",
Value: eth.DefaultConfig.Rollup.L1GasPrice,
EnvVar: "ROLLUP_L1_GASPRICE",
}
)
// MakeDataDir retrieves the currently requested data directory, terminating
......@@ -1152,6 +1158,9 @@ func setRollup(ctx *cli.Context, cfg *rollup.Config) {
if ctx.GlobalIsSet(RollupTimstampRefreshFlag.Name) {
cfg.TimestampRefreshThreshold = ctx.GlobalDuration(RollupTimstampRefreshFlag.Name)
}
if ctx.GlobalIsSet(RollupL1GasPriceFlag.Name) {
cfg.L1GasPrice = GlobalBig(ctx, RollupL1GasPriceFlag.Name)
}
}
// setLes configures the les server and ultra light client settings from the command line flags.
......
package core
import (
"math/big"
)
/// ROLLUP_BASE_TX_SIZE is the encoded rollup transaction's compressed size excluding
/// the variable length data.
/// Ref: https://github.com/ethereum-optimism/contracts/blob/409f190518b90301db20d0d4f53760021bc203a8/contracts/optimistic-ethereum/OVM/precompiles/OVM_SequencerEntrypoint.sol#L47
const ROLLUP_BASE_TX_SIZE int = 96
/// CalculateFee calculates the fee that must be paid to the Rollup sequencer, taking into
/// account the cost of publishing data to L1.
/// Returns: (ROLLUP_BASE_TX_SIZE + len(data)) * dataPrice + executionPrice * gasUsed
func CalculateRollupFee(data []byte, gasUsed uint64, dataPrice, executionPrice *big.Int) *big.Int {
dataLen := int64(ROLLUP_BASE_TX_SIZE + len(data))
// get the data fee
dataFee := new(big.Int).Mul(dataPrice, big.NewInt(dataLen))
executionFee := new(big.Int).Mul(executionPrice, new(big.Int).SetUint64(gasUsed))
fee := new(big.Int).Add(dataFee, executionFee)
return fee
}
package core
import (
"math/big"
"testing"
)
var feeTests = map[string]struct {
dataLen int
gasUsed uint64
dataPrice int64
executionPrice int64
}{
"simple": {10000, 10, 20, 30},
"zero gas used": {10000, 0, 20, 30},
"zero data price": {10000, 0, 0, 30},
"zero execution price": {10000, 0, 0, 0},
}
func TestCalculateRollupFee(t *testing.T) {
for name, tt := range feeTests {
t.Run(name, func(t *testing.T) {
data := make([]byte, 0, tt.dataLen)
fee := CalculateRollupFee(data, tt.gasUsed, big.NewInt(tt.dataPrice), big.NewInt(tt.executionPrice))
dataFee := uint64((ROLLUP_BASE_TX_SIZE + len(data)) * int(tt.dataPrice))
executionFee := uint64(tt.executionPrice) * tt.gasUsed
expectedFee := dataFee + executionFee
if fee.Cmp(big.NewInt(int64(expectedFee))) != 0 {
t.Errorf("rollup fee check failed: expected %d, got %s", expectedFee, fee.String())
}
})
}
}
......@@ -90,7 +90,7 @@ func ApplyTransaction(config *params.ChainConfig, bc ChainContext, author *commo
}
} else {
decompressor := config.StateDump.Accounts["OVM_SequencerEntrypoint"]
msg, err = AsOvmMessage(tx, types.MakeSigner(config, header.Number), decompressor.Address)
msg, err = AsOvmMessage(tx, types.MakeSigner(config, header.Number), decompressor.Address, header.GasLimit)
if err != nil {
return nil, err
}
......
......@@ -58,7 +58,7 @@ func toExecutionManagerRun(evm *vm.EVM, msg Message) (Message, error) {
return outputmsg, nil
}
func AsOvmMessage(tx *types.Transaction, signer types.Signer, decompressor common.Address) (Message, error) {
func AsOvmMessage(tx *types.Transaction, signer types.Signer, decompressor common.Address, gasLimit uint64) (Message, error) {
msg, err := tx.AsMessage(signer)
if err != nil {
// This should only be allowed to pass if the transaction is in the ctc
......@@ -85,7 +85,7 @@ func AsOvmMessage(tx *types.Transaction, signer types.Signer, decompressor commo
msg.From(),
&decompressor,
tx.GetMeta().RawTransaction,
msg.Gas(),
gasLimit,
)
if err != nil {
......
......@@ -94,6 +94,7 @@ 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
)
var (
......@@ -537,10 +538,14 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if tx.Value().Sign() < 0 {
return ErrNegativeValue
}
// Ensure the transaction doesn't exceed the current block limit gas.
if pool.currentMaxGas < tx.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.Gas() && tx.GasPrice().Cmp(gwei) != 0 {
return ErrGasLimit
}
// Make sure the transaction is signed properly
from, err := types.Sender(pool.signer, tx)
if err != nil {
......@@ -565,7 +570,6 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if pool.currentState.GetBalance(from).Cmp(tx.Cost()) < 0 {
return ErrInsufficientFunds
}
}
// Ensure the transaction has more gas than the basic tx fee.
intrGas, err := IntrinsicGas(tx.Data(), tx.To() == nil, true, pool.istanbul)
if err != nil {
......@@ -574,6 +578,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
if tx.Gas() < intrGas {
return ErrIntrinsicGas
}
}
return nil
}
......@@ -585,6 +590,7 @@ func (pool *TxPool) validateTx(tx *types.Transaction, local bool) error {
// whitelisted, preventing any associated transaction from being dropped out of the pool
// due to pricing constraints.
func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err error) {
log.Debug("received tx", "gas", tx.Gas(), "gasprice", tx.GasPrice().Uint64())
// If the transaction is already known, discard it
hash := tx.Hash()
if pool.all.Get(hash) != nil {
......@@ -592,6 +598,7 @@ func (pool *TxPool) add(tx *types.Transaction, local bool) (replaced bool, err e
knownTxMeter.Mark(1)
return false, fmt.Errorf("known transaction: %x", hash)
}
// If the transaction fails basic validation, discard it
if err := pool.validateTx(tx, local); err != nil {
log.Trace("Discarding invalid transaction", "hash", hash, "err", err)
......
......@@ -46,6 +46,7 @@ type EthAPIBackend struct {
extRPCEnabled bool
eth *Ethereum
gpo *gasprice.Oracle
l1gpo *gasprice.L1Oracle
verifier bool
gasLimit uint64
UsingOVM bool
......@@ -371,6 +372,14 @@ func (b *EthAPIBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPrice(ctx)
}
func (b *EthAPIBackend) SuggestDataPrice(ctx context.Context) (*big.Int, error) {
return b.l1gpo.SuggestDataPrice(ctx)
}
func (b *EthAPIBackend) SetL1GasPrice(ctx context.Context, gasPrice *big.Int) {
b.l1gpo.SetL1GasPrice(gasPrice)
}
func (b *EthAPIBackend) ChainDb() ethdb.Database {
return b.eth.ChainDb()
}
......
......@@ -15,6 +15,7 @@ func TestGasLimit(t *testing.T) {
extRPCEnabled: false,
eth: nil,
gpo: nil,
l1gpo: nil,
verifier: false,
gasLimit: 0,
UsingOVM: true,
......
......@@ -809,7 +809,7 @@ func (api *PrivateDebugAPI) computeTxEnv(blockHash common.Hash, txIndex int, ree
if !vm.UsingOVM {
msg, _ = tx.AsMessage(signer)
} else {
msg, err = core.AsOvmMessage(tx, signer, common.HexToAddress("0x4200000000000000000000000000000000000005"))
msg, err = core.AsOvmMessage(tx, signer, common.HexToAddress("0x4200000000000000000000000000000000000005"), block.Header().GasLimit)
if err != nil {
return nil, vm.Context{}, nil, err
}
......
......@@ -226,13 +226,17 @@ func New(ctx *node.ServiceContext, config *Config) (*Ethereum, error) {
eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock)
eth.miner.SetExtra(makeExtraData(config.Miner.ExtraData))
log.Info("Backend Config", "max-calldata-size", config.Rollup.MaxCallDataSize, "gas-limit", config.Rollup.GasLimit, "is-verifier", config.Rollup.IsVerifier, "using-ovm", vm.UsingOVM)
eth.APIBackend = &EthAPIBackend{ctx.ExtRPCEnabled(), eth, nil, config.Rollup.IsVerifier, config.Rollup.GasLimit, vm.UsingOVM, config.Rollup.MaxCallDataSize}
log.Info("Backend Config", "max-calldata-size", config.Rollup.MaxCallDataSize, "gas-limit", config.Rollup.GasLimit, "is-verifier", config.Rollup.IsVerifier, "using-ovm", vm.UsingOVM, "l1-gasprice", config.Rollup.L1GasPrice)
eth.APIBackend = &EthAPIBackend{ctx.ExtRPCEnabled(), eth, nil, nil, config.Rollup.IsVerifier, config.Rollup.GasLimit, vm.UsingOVM, config.Rollup.MaxCallDataSize}
gpoParams := config.GPO
if gpoParams.Default == nil {
gpoParams.Default = config.Miner.GasPrice
}
eth.APIBackend.gpo = gasprice.NewOracle(eth.APIBackend, gpoParams)
// create the L1 GPO and allow the API backend and the sync service to access it
l1Gpo := gasprice.NewL1Oracle(config.Rollup.L1GasPrice)
eth.APIBackend.l1gpo = l1Gpo
eth.syncService.L1gpo = l1Gpo
return eth, nil
}
......
......@@ -81,6 +81,7 @@ var DefaultConfig = Config{
// is additional overhead that is unaccounted. Round down to 127000 for
// safety.
MaxCallDataSize: 127000,
L1GasPrice: big.NewInt(100 * params.GWei),
},
DiffDbCache: 256,
}
......
package gasprice
import (
"context"
"math/big"
)
type L1Oracle struct {
gasPrice *big.Int
}
func NewL1Oracle(gasPrice *big.Int) *L1Oracle {
return &L1Oracle{gasPrice}
}
/// SuggestDataPrice returns the gas price which should be charged per byte of published
/// data by the sequencer.
func (gpo *L1Oracle) SuggestDataPrice(ctx context.Context) (*big.Int, error) {
return gpo.gasPrice, nil
}
func (gpo *L1Oracle) SetL1GasPrice(gasPrice *big.Int) {
gpo.gasPrice = gasPrice
}
......@@ -32,6 +32,7 @@ require (
github.com/huin/goupnp v1.0.0
github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458
github.com/jarcoal/httpmock v1.0.8
github.com/jmoiron/sqlx v1.2.0
github.com/julienschmidt/httprouter v1.1.1-0.20170430222011-975b5c4c7c21
github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356
......
......@@ -108,6 +108,7 @@ github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883 h1:FSeK4fZCo
github.com/influxdata/influxdb v1.2.3-0.20180221223340-01288bdb0883/go.mod h1:qZna6X/4elxqT3yI9iZYdZrWWdeFOOprn86kgg4+IzY=
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 h1:6OvNmYgJyexcZ3pYbTI9jWx5tHo1Dee/tWbLMfPe2TA=
github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
......
......@@ -63,10 +63,9 @@ func NewPublicEthereumAPI(b Backend) *PublicEthereumAPI {
return &PublicEthereumAPI{b}
}
// GasPrice returns a suggestion for a gas price.
// GasPrice always returns 1 gwei. See `DoEstimateGas` below for context.
func (s *PublicEthereumAPI) GasPrice(ctx context.Context) (*hexutil.Big, error) {
price, err := s.b.SuggestPrice(ctx)
return (*hexutil.Big)(price), err
return (*hexutil.Big)(big.NewInt(defaultGasPrice)), nil
}
// ProtocolVersion returns the current Ethereum protocol version this node supports
......@@ -996,74 +995,101 @@ func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOr
return (hexutil.Bytes)(result), err
}
// Optimism note: The gasPrice in Optimism is modified to always return 1 gwei. We
// use the gasLimit field to communicate the entire user fee. This is done for
// for compatibility reasons with the existing Ethereum toolchain, so that the user
// fees can compensate for the additional costs the sequencer pays for publishing the
// transaction calldata
func DoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap *big.Int) (hexutil.Uint64, error) {
// // Binary search the gas requirement, as it may be higher than the amount used
// var (
// lo uint64 = params.TxGas - 1
// hi uint64
// cap uint64
// )
// if args.Gas != nil && uint64(*args.Gas) >= params.TxGas {
// hi = uint64(*args.Gas)
// } else {
// // Retrieve the block to act as the gas ceiling
// block, err := b.BlockByNumberOrHash(ctx, blockNrOrHash)
// if err != nil {
// return 0, err
// }
// hi = block.GasLimit()
// }
// if gasCap != nil && hi > gasCap.Uint64() {
// log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
// hi = gasCap.Uint64()
// }
// cap = hi
// // Set sender address or use a default if none specified
// if args.From == nil {
// if wallets := b.AccountManager().Wallets(); len(wallets) > 0 {
// if accounts := wallets[0].Accounts(); len(accounts) > 0 {
// args.From = &accounts[0].Address
// }
// }
// }
// // Use zero-address if none other is available
// if args.From == nil {
// args.From = &common.Address{}
// }
// // Create a helper to check if a gas allowance results in an executable transaction
// executable := func(gas uint64) bool {
// args.Gas = (*hexutil.Uint64)(&gas)
// _, _, failed, err := DoCall(ctx, b, args, blockNrOrHash, nil, vm.Config{}, 0, gasCap)
// if err != nil || failed {
// return false
// }
// return true
// }
// // Execute the binary search and hone in on an executable gas limit
// for lo+1 < hi {
// mid := (hi + lo) / 2
// if !executable(mid) {
// lo = mid
// } else {
// hi = mid
// }
// }
// // Reject the transaction as invalid if it still fails at the highest allowance
// if hi == cap {
// if !executable(hi) {
// return 0, fmt.Errorf("gas required exceeds allowance (%d) or always failing transaction", cap)
// }
// }
// return hexutil.Uint64(hi), nil
if args.Data == nil {
return 0, errors.New("transaction data cannot be nil")
}
// 1. get the gas that would be used by the transaction
gasUsed, err := legacyDoEstimateGas(ctx, b, args, blockNrOrHash, gasCap)
if err != nil {
return 0, err
}
// 2a. fetch the data price, depends on how the sequencer has chosen to update their values based on the
// l1 gas prices
dataPrice, err := b.SuggestDataPrice(ctx)
if err != nil {
return 0, err
}
// 2b. fetch the execution gas price, by the typical mempool dynamics
executionPrice, err := b.SuggestPrice(ctx)
if err != nil {
return 0, err
}
// 3. calculate the fee and normalize by the default gas price
fee := core.CalculateRollupFee(*args.Data, uint64(gasUsed), dataPrice, executionPrice).Uint64() / defaultGasPrice
return (hexutil.Uint64)(fee), nil
}
func legacyDoEstimateGas(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, gasCap *big.Int) (hexutil.Uint64, error) {
// Binary search the gas requirement, as it may be higher than the amount used
var (
lo uint64 = params.TxGas - 1
hi uint64
cap uint64
)
if args.Gas != nil && uint64(*args.Gas) >= params.TxGas {
hi = uint64(*args.Gas)
} else {
// Retrieve the block to act as the gas ceiling
block, err := b.BlockByNumberOrHash(ctx, blockNrOrHash)
if err != nil {
return 0, err
}
return hexutil.Uint64(block.GasLimit() - 1), nil
hi = block.GasLimit()
}
if gasCap != nil && hi > gasCap.Uint64() {
log.Warn("Caller gas above allowance, capping", "requested", hi, "cap", gasCap)
hi = gasCap.Uint64()
}
cap = hi
// Set sender address or use a default if none specified
if args.From == nil {
if wallets := b.AccountManager().Wallets(); len(wallets) > 0 {
if accounts := wallets[0].Accounts(); len(accounts) > 0 {
args.From = &accounts[0].Address
}
}
}
// Use zero-address if none other is available
if args.From == nil {
args.From = &common.Address{}
}
// Create a helper to check if a gas allowance results in an executable transaction
executable := func(gas uint64) bool {
args.Gas = (*hexutil.Uint64)(&gas)
_, _, failed, err := DoCall(ctx, b, args, blockNrOrHash, nil, vm.Config{}, 0, gasCap)
if err != nil || failed {
return false
}
return true
}
// Execute the binary search and hone in on an executable gas limit
for lo+1 < hi {
mid := (hi + lo) / 2
if !executable(mid) {
lo = mid
} else {
hi = mid
}
}
// Reject the transaction as invalid if it still fails at the highest allowance
if hi == cap {
if !executable(hi) {
return 0, fmt.Errorf("gas required exceeds allowance (%d) or always failing transaction", cap)
}
}
return hexutil.Uint64(hi), nil
}
// EstimateGas returns an estimate of the amount of gas needed to execute the
......@@ -1922,6 +1948,24 @@ func (api *PublicRollupAPI) GetInfo(ctx context.Context) rollupInfo {
}
}
// PrivatelRollupAPI provides private RPC methods to control the sequencer.
// These methods can be abused by external users and must be considered insecure for use by untrusted users.
type PrivateRollupAPI struct {
b Backend
}
// NewPrivateRollupAPI creates a new API definition for the rollup methods of the
// Ethereum service.
func NewPrivateRollupAPI(b Backend) *PrivateRollupAPI {
return &PrivateRollupAPI{b: b}
}
// SetGasPrice sets the gas price to be used when quoting calldata publishing costs
// to users
func (api *PrivateRollupAPI) SetL1GasPrice(ctx context.Context, gasPrice hexutil.Big) {
api.b.SetL1GasPrice(ctx, (*big.Int)(&gasPrice))
}
// PublicDebugAPI is the collection of Ethereum APIs exposed over the public
// debugging endpoint.
type PublicDebugAPI struct {
......
......@@ -94,6 +94,8 @@ type Backend interface {
GetRollupContext() (uint64, uint64)
GasLimit() uint64
GetDiff(*big.Int) (diffdb.Diff, error)
SuggestDataPrice(ctx context.Context) (*big.Int, error)
SetL1GasPrice(context.Context, *big.Int)
}
func GetAPIs(apiBackend Backend) []rpc.API {
......@@ -119,6 +121,10 @@ func GetAPIs(apiBackend Backend) []rpc.API {
Version: "1.0",
Service: NewPublicRollupAPI(apiBackend),
Public: true,
}, {
Namespace: "rollup_personal",
Version: "1.0",
Service: NewPrivateRollupAPI(apiBackend),
}, {
Namespace: "txpool",
Version: "1.0",
......
......@@ -281,6 +281,16 @@ func (b *LesApiBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
return b.gpo.SuggestPrice(ctx)
}
// NB: Non sequencer nodes cannot suggest L1 gas prices.
func (b *LesApiBackend) SuggestDataPrice(ctx context.Context) (*big.Int, error) {
panic("SuggestDataPrice not implemented")
}
// NB: Non sequencer nodes cannot set L1 gas prices.
func (b *LesApiBackend) SetL1GasPrice(ctx context.Context, gasPrice *big.Int) {
panic("SetL1GasPrice is not implemented")
}
func (b *LesApiBackend) ChainDb() ethdb.Database {
return b.eth.chainDb
}
......
......@@ -863,22 +863,25 @@ func (w *worker) commitNewTx(tx *types.Transaction) error {
tstart := time.Now()
parent := w.chain.CurrentBlock()
// The L1Timestamp will always be set for a transaction
// coming from a batch submission because the transaction
// has been included in the canonical transaction chain.
// The only time that L1Timestamp is zero is for queue
// origin sequencer transactions that have yet to be included
// in the canonical transaction chain, meaning this code
// path is only relevant for the sequencer.
if tx.L1Timestamp() == 0 {
ts := w.eth.SyncService().GetLatestL1Timestamp()
bn := w.eth.SyncService().GetLatestL1BlockNumber()
tx.SetL1Timestamp(ts)
tx.SetL1BlockNumber(bn)
}
timestamp := tx.L1Timestamp()
num := parent.Number()
// Preserve liveliness as best as possible. Must panic on L1 to L2
// transactions as the timestamp cannot be malleated
if parent.Time() > timestamp {
log.Error("Monotonicity violation", "index", num)
if tx.QueueOrigin().Uint64() == uint64(types.QueueOriginSequencer) {
tx.SetL1Timestamp(parent.Time())
prev := parent.Transactions()
if len(prev) != 1 {
panic("Cannot recover L1BlockNumber")
}
tx.SetL1BlockNumber(prev[0].L1BlockNumber().Uint64())
} else {
panic("Monotonicity violation")
}
}
// Fill in the index field in the tx meta if it is `nil`.
// This should only ever happen in the case of the sequencer
// receiving a queue origin sequencer transaction. The verifier
......
......@@ -42,6 +42,10 @@ type SyncStatus struct {
CurrentTransactionIndex uint64 `json:"currentTransactionIndex"`
}
type L1GasPrice struct {
GasPrice string `json:"gasPrice"`
}
type transaction struct {
Index uint64 `json:"index"`
BatchIndex uint64 `json:"batchIndex"`
......@@ -92,6 +96,7 @@ type RollupClient interface {
GetLatestEthContext() (*EthContext, error)
GetLastConfirmedEnqueue() (*types.Transaction, error)
SyncStatus() (*SyncStatus, error)
GetL1GasPrice() (*big.Int, error)
}
type Client struct {
......@@ -439,3 +444,25 @@ func (c *Client) SyncStatus() (*SyncStatus, error) {
return status, nil
}
func (c *Client) GetL1GasPrice() (*big.Int, error) {
response, err := c.client.R().
SetResult(&L1GasPrice{}).
Get("/eth/gasprice")
if err != nil {
return nil, fmt.Errorf("Cannot fetch L1 gas price: %w", err)
}
gasPriceResp, ok := response.Result().(*L1GasPrice)
if !ok {
return nil, fmt.Errorf("Cannot parse L1 gas price response")
}
gasPrice, ok := new(big.Int).SetString(gasPriceResp.GasPrice, 10)
if !ok {
return nil, fmt.Errorf("Cannot parse response as big number")
}
return gasPrice, nil
}
package rollup
import (
"fmt"
"math/big"
"testing"
"github.com/jarcoal/httpmock"
)
func TestRollupClientGetL1GasPrice(t *testing.T) {
url := "http://localhost:9999"
endpoint := fmt.Sprintf("%s/eth/gasprice", url)
// url/chain-id does not matter, we'll mock the responses
client := NewClient(url, big.NewInt(1))
// activate the mock
httpmock.ActivateNonDefault(client.client.GetClient())
// The API responds with a string value
expectedGasPrice, _ := new(big.Int).SetString("123132132151245817421893", 10)
body := map[string]interface{}{
"gasPrice": expectedGasPrice.String(),
}
response, _ := httpmock.NewJsonResponder(
200,
body,
)
httpmock.RegisterResponder(
"GET",
endpoint,
response,
)
gasPrice, err := client.GetL1GasPrice()
if err != nil {
t.Fatal("could not get mocked gas price", err)
}
if gasPrice.Cmp(expectedGasPrice) != 0 {
t.Fatal("gasPrice is not parsed properly in the client")
}
}
......@@ -33,4 +33,6 @@ type Config struct {
PollInterval time.Duration
// Interval for updating the timestamp
TimestampRefreshThreshold time.Duration
// The gas price to use when estimating L1 calldata publishing costs
L1GasPrice *big.Int
}
......@@ -19,6 +19,8 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/eth/gasprice"
)
// OVMContext represents the blocknumber and timestamp
......@@ -42,6 +44,7 @@ type SyncService struct {
eth1ChainId uint64
bc *core.BlockChain
txpool *core.TxPool
L1gpo *gasprice.L1Oracle
client RollupClient
syncing atomic.Value
OVMContext OVMContext
......@@ -357,6 +360,14 @@ func (s *SyncService) SequencerLoop() {
}
func (s *SyncService) sequence() error {
// Update to the latest L1 gas price
l1GasPrice, err := s.client.GetL1GasPrice()
if err != nil {
return err
}
s.L1gpo.SetL1GasPrice(l1GasPrice)
log.Info("Adjusted L1 Gas Price", "gasprice", l1GasPrice)
// Only the sequencer needs to poll for enqueue transactions
// and then can choose when to apply them. We choose to apply
// transactions such that it makes for efficient batch submitting.
......@@ -441,6 +452,7 @@ func (s *SyncService) updateContext() error {
if err != nil {
return err
}
current := time.Unix(int64(s.GetLatestL1Timestamp()), 0)
next := time.Unix(int64(context.Timestamp), 0)
if next.Sub(current) > s.timestampRefreshThreshold {
......@@ -598,6 +610,10 @@ func (s *SyncService) SubscribeNewTxsEvent(ch chan<- core.NewTxsEvent) event.Sub
// inspecting the local database. This is mean to prevent transactions from
// being replayed.
func (s *SyncService) maybeApplyTransaction(tx *types.Transaction) error {
if tx == nil {
return fmt.Errorf("nil transaction passed to maybeApplyTransaction")
}
log.Debug("Maybe applying transaction", "hash", tx.Hash().Hex())
index := tx.GetMeta().Index
if index == nil {
......@@ -642,6 +658,10 @@ func (s *SyncService) applyTransaction(tx *types.Transaction) error {
// queue origin sequencer transactions, as the contracts on L1 manage the same
// validity checks that are done here.
func (s *SyncService) ApplyTransaction(tx *types.Transaction) error {
if tx == nil {
return fmt.Errorf("nil transaction passed to ApplyTransaction")
}
log.Debug("Sending transaction to sync service", "hash", tx.Hash().Hex())
s.txLock.Lock()
defer s.txLock.Unlock()
......@@ -660,6 +680,13 @@ func (s *SyncService) ApplyTransaction(tx *types.Transaction) error {
return fmt.Errorf("invalid transaction: %w", err)
}
if tx.L1Timestamp() == 0 {
ts := s.GetLatestL1Timestamp()
bn := s.GetLatestL1BlockNumber()
tx.SetL1Timestamp(ts)
tx.SetL1BlockNumber(bn)
}
// Set the raw transaction data in the meta
txRaw, err := getRawTransaction(tx)
if err != nil {
......
......@@ -14,6 +14,7 @@ import (
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/eth/gasprice"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/params"
)
......@@ -150,6 +151,37 @@ func TestSyncServiceTransactionEnqueued(t *testing.T) {
}
}
func TestSyncServiceL1GasPrice(t *testing.T) {
service, _, _, err := newTestSyncService(true)
setupMockClient(service, map[string]interface{}{})
service.L1gpo = gasprice.NewL1Oracle(big.NewInt(0))
if err != nil {
t.Fatal(err)
}
gasBefore, err := service.L1gpo.SuggestDataPrice(context.Background())
if err != nil {
t.Fatal(err)
}
if gasBefore.Cmp(big.NewInt(0)) != 0 {
t.Fatal("expected 0 gas price, got", gasBefore)
}
// run 1 iteration of the eloop
service.sequence()
gasAfter, err := service.L1gpo.SuggestDataPrice(context.Background())
if err != nil {
t.Fatal(err)
}
if gasAfter.Cmp(big.NewInt(100*int64(params.GWei))) != 0 {
t.Fatal("expected 100 gas price, got", gasAfter)
}
}
// Pass true to set as a verifier
func TestSyncServiceSync(t *testing.T) {
service, txCh, sub, err := newTestSyncService(true)
......@@ -314,6 +346,7 @@ type mockClient struct {
func setupMockClient(service *SyncService, responses map[string]interface{}) {
client := newMockClient(responses)
service.client = client
service.L1gpo = gasprice.NewL1Oracle(big.NewInt(0))
}
func newMockClient(responses map[string]interface{}) *mockClient {
......@@ -400,3 +433,7 @@ func (m *mockClient) SyncStatus() (*SyncStatus, error) {
Syncing: false,
}, nil
}
func (m *mockClient) GetL1GasPrice() (*big.Int, error) {
return big.NewInt(100 * int64(params.GWei)), nil
}
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