Commit 30ada8e6 authored by Matthew Slipper's avatar Matthew Slipper Committed by GitHub

Merge pull request #2090 from ethereum-optimism/feat/verifier-itests-patch-ctx

fix(bss,dtl): correctly submit and parse timestamp information for L1 to L2 txs
parents a6006b2b b022c49d
---
'@eth-optimism/data-transport-layer': patch
---
Updates DTL to correctly parse L1 to L2 tx timestamps after the first bss hardfork
---
'@eth-optimism/integration-tests': patch
---
Updates integration tests to include a test for syncing a Verifier from L1
---
'@eth-optimism/l2geth': patch
---
Fixes incorrect timestamp handling for L1 syncing verifiers
---
'@eth-optimism/batch-submitter': patch
---
Updates batch submitter to also include separate timestamps for deposit transactions"
---
'@eth-optimism/integration-tests': patch
---
Add verifier integration tests
.github .github
node_modules node_modules
.env
**/.env
test test
**/*_test.go **/*_test.go
......
...@@ -11,6 +11,7 @@ import { ...@@ -11,6 +11,7 @@ import {
l1Provider, l1Provider,
l2Provider, l2Provider,
replicaProvider, replicaProvider,
verifierProvider,
l1Wallet, l1Wallet,
l2Wallet, l2Wallet,
gasPriceOracleWallet, gasPriceOracleWallet,
...@@ -57,6 +58,7 @@ export class OptimismEnv { ...@@ -57,6 +58,7 @@ export class OptimismEnv {
l1Provider: providers.JsonRpcProvider l1Provider: providers.JsonRpcProvider
l2Provider: providers.JsonRpcProvider l2Provider: providers.JsonRpcProvider
replicaProvider: providers.JsonRpcProvider replicaProvider: providers.JsonRpcProvider
verifierProvider: providers.JsonRpcProvider
constructor(args: any) { constructor(args: any) {
this.addressManager = args.addressManager this.addressManager = args.addressManager
...@@ -74,6 +76,7 @@ export class OptimismEnv { ...@@ -74,6 +76,7 @@ export class OptimismEnv {
this.l1Provider = args.l1Provider this.l1Provider = args.l1Provider
this.l2Provider = args.l2Provider this.l2Provider = args.l2Provider
this.replicaProvider = args.replicaProvider this.replicaProvider = args.replicaProvider
this.verifierProvider = args.verifierProvider
this.ctc = args.ctc this.ctc = args.ctc
this.scc = args.scc this.scc = args.scc
} }
...@@ -140,6 +143,7 @@ export class OptimismEnv { ...@@ -140,6 +143,7 @@ export class OptimismEnv {
l2Wallet, l2Wallet,
l1Provider, l1Provider,
l2Provider, l2Provider,
verifierProvider,
replicaProvider, replicaProvider,
}) })
} }
......
...@@ -62,6 +62,8 @@ const procEnv = cleanEnv(process.env, { ...@@ -62,6 +62,8 @@ const procEnv = cleanEnv(process.env, {
REPLICA_URL: str({ default: 'http://localhost:8549' }), REPLICA_URL: str({ default: 'http://localhost:8549' }),
REPLICA_POLLING_INTERVAL: num({ default: 10 }), REPLICA_POLLING_INTERVAL: num({ default: 10 }),
VERIFIER_URL: str({ default: 'http://localhost:8547' }),
PRIVATE_KEY: str({ PRIVATE_KEY: str({
default: default:
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
...@@ -96,6 +98,9 @@ const procEnv = cleanEnv(process.env, { ...@@ -96,6 +98,9 @@ const procEnv = cleanEnv(process.env, {
RUN_NIGHTLY_TESTS: bool({ RUN_NIGHTLY_TESTS: bool({
default: false, default: false,
}), }),
RUN_VERIFIER_TESTS: bool({
default: true,
}),
MOCHA_TIMEOUT: num({ MOCHA_TIMEOUT: num({
default: 120_000, default: 120_000,
...@@ -121,6 +126,11 @@ export const replicaProvider = injectL2Context( ...@@ -121,6 +126,11 @@ export const replicaProvider = injectL2Context(
) )
replicaProvider.pollingInterval = procEnv.REPLICA_POLLING_INTERVAL replicaProvider.pollingInterval = procEnv.REPLICA_POLLING_INTERVAL
export const verifierProvider = injectL2Context(
new providers.JsonRpcProvider(procEnv.VERIFIER_URL)
)
verifierProvider.pollingInterval = procEnv.L2_POLLING_INTERVAL
// The sequencer private key which is funded on L1 // The sequencer private key which is funded on L1
export const l1Wallet = new Wallet(procEnv.PRIVATE_KEY, l1Provider) export const l1Wallet = new Wallet(procEnv.PRIVATE_KEY, l1Provider)
......
import { TransactionReceipt } from '@ethersproject/abstract-provider'
import { expect } from './shared/setup'
import { OptimismEnv } from './shared/env'
import {
defaultTransactionFactory,
gasPriceForL2,
sleep,
envConfig,
} from './shared/utils'
describe('Verifier Tests', () => {
let env: OptimismEnv
before(async function () {
if (!envConfig.RUN_VERIFIER_TESTS) {
this.skip()
return
}
env = await OptimismEnv.new()
})
describe('Matching blocks', () => {
it('should sync a transaction', async () => {
const tx = defaultTransactionFactory()
tx.gasPrice = await gasPriceForL2()
const result = await env.l2Wallet.sendTransaction(tx)
let receipt: TransactionReceipt
while (!receipt) {
receipt = await env.verifierProvider.getTransactionReceipt(result.hash)
await sleep(200)
}
const sequencerBlock = (await env.l2Provider.getBlock(
result.blockNumber
)) as any
const verifierBlock = (await env.verifierProvider.getBlock(
result.blockNumber
)) as any
expect(sequencerBlock.stateRoot).to.deep.eq(verifierBlock.stateRoot)
expect(sequencerBlock.hash).to.deep.eq(verifierBlock.hash)
})
it('sync an unprotected tx (eip155)', async () => {
const tx = {
...defaultTransactionFactory(),
nonce: await env.l2Wallet.getTransactionCount(),
gasPrice: await gasPriceForL2(),
chainId: null, // Disables EIP155 transaction signing.
}
const signed = await env.l2Wallet.signTransaction(tx)
const result = await env.l2Provider.sendTransaction(signed)
let receipt: TransactionReceipt
while (!receipt) {
receipt = await env.verifierProvider.getTransactionReceipt(result.hash)
await sleep(200)
}
const sequencerBlock = (await env.l2Provider.getBlock(
result.blockNumber
)) as any
const verifierBlock = (await env.verifierProvider.getBlock(
result.blockNumber
)) as any
expect(sequencerBlock.stateRoot).to.deep.eq(verifierBlock.stateRoot)
expect(sequencerBlock.hash).to.deep.eq(verifierBlock.hash)
})
it('should forward tx to sequencer', async () => {
const tx = {
...defaultTransactionFactory(),
nonce: await env.l2Wallet.getTransactionCount(),
gasPrice: await gasPriceForL2(),
}
const signed = await env.l2Wallet.signTransaction(tx)
const result = await env.verifierProvider.sendTransaction(signed)
let receipt: TransactionReceipt
while (!receipt) {
receipt = await env.verifierProvider.getTransactionReceipt(result.hash)
await sleep(200)
}
const sequencerBlock = (await env.l2Provider.getBlock(
result.blockNumber
)) as any
const verifierBlock = (await env.verifierProvider.getBlock(
result.blockNumber
)) as any
expect(sequencerBlock.stateRoot).to.deep.eq(verifierBlock.stateRoot)
expect(sequencerBlock.hash).to.deep.eq(verifierBlock.hash)
})
})
})
...@@ -931,18 +931,7 @@ func (w *worker) commitNewTx(tx *types.Transaction) error { ...@@ -931,18 +931,7 @@ func (w *worker) commitNewTx(tx *types.Transaction) error {
// Preserve liveliness as best as possible. Must panic on L1 to L2 // Preserve liveliness as best as possible. Must panic on L1 to L2
// transactions as the timestamp cannot be malleated // transactions as the timestamp cannot be malleated
if parent.Time() > tx.L1Timestamp() { if parent.Time() > tx.L1Timestamp() {
log.Error("Monotonicity violation", "index", num) log.Error("Monotonicity violation", "index", num, "parent", parent.Time(), "tx", tx.L1Timestamp())
if tx.QueueOrigin() == types.QueueOriginSequencer {
tx.SetL1Timestamp(parent.Time())
prev := parent.Transactions()
if len(prev) == 1 {
tx.SetL1BlockNumber(prev[0].L1BlockNumber().Uint64())
} else {
log.Error("Cannot recover L1 Blocknumber")
}
} else {
log.Error("Cannot recover from monotonicity violation")
}
} }
// Fill in the index field in the tx meta if it is `nil`. // Fill in the index field in the tx meta if it is `nil`.
......
...@@ -825,13 +825,14 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error { ...@@ -825,13 +825,14 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error {
if now.Sub(current) > s.timestampRefreshThreshold { if now.Sub(current) > s.timestampRefreshThreshold {
current = now current = now
} }
log.Info("Updating latest timestamp", "timestamp", current, "unix", current.Unix())
tx.SetL1Timestamp(uint64(current.Unix())) tx.SetL1Timestamp(uint64(current.Unix()))
} else if tx.L1Timestamp() == 0 && s.verifier { } else if tx.L1Timestamp() == 0 && s.verifier {
// This should never happen // This should never happen
log.Error("No tx timestamp found when running as verifier", "hash", tx.Hash().Hex()) log.Error("No tx timestamp found when running as verifier", "hash", tx.Hash().Hex())
} else if tx.L1Timestamp() < s.GetLatestL1Timestamp() { } else if tx.L1Timestamp() < ts {
// This should never happen, but sometimes does // This should never happen, but sometimes does
log.Error("Timestamp monotonicity violation", "hash", tx.Hash().Hex()) log.Error("Timestamp monotonicity violation", "hash", tx.Hash().Hex(), "latest", ts, "tx", tx.L1Timestamp())
} }
l1BlockNumber := tx.L1BlockNumber() l1BlockNumber := tx.L1BlockNumber()
......
...@@ -136,8 +136,9 @@ services: ...@@ -136,8 +136,9 @@ services:
- l1_chain - l1_chain
- deployer - deployer
- dtl - dtl
- l2geth
deploy: deploy:
replicas: 0 replicas: 1
build: build:
context: .. context: ..
dockerfile: ./ops/docker/Dockerfile.geth dockerfile: ./ops/docker/Dockerfile.geth
...@@ -146,6 +147,7 @@ services: ...@@ -146,6 +147,7 @@ services:
- ./envs/geth.env - ./envs/geth.env
environment: environment:
ETH1_HTTP: http://l1_chain:8545 ETH1_HTTP: http://l1_chain:8545
SEQUENCER_CLIENT_HTTP: http://l2geth:8545
ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json
ROLLUP_CLIENT_HTTP: http://dtl:7878 ROLLUP_CLIENT_HTTP: http://dtl:7878
ROLLUP_BACKEND: 'l1' ROLLUP_BACKEND: 'l1'
......
...@@ -9,6 +9,7 @@ DATA_TRANSPORT_LAYER__LOGS_PER_POLLING_INTERVAL=2000 ...@@ -9,6 +9,7 @@ DATA_TRANSPORT_LAYER__LOGS_PER_POLLING_INTERVAL=2000
DATA_TRANSPORT_LAYER__DANGEROUSLY_CATCH_ALL_ERRORS=true DATA_TRANSPORT_LAYER__DANGEROUSLY_CATCH_ALL_ERRORS=true
DATA_TRANSPORT_LAYER__SERVER_HOSTNAME=0.0.0.0 DATA_TRANSPORT_LAYER__SERVER_HOSTNAME=0.0.0.0
DATA_TRANSPORT_LAYER__L1_START_HEIGHT=1 DATA_TRANSPORT_LAYER__L1_START_HEIGHT=1
DATA_TRANSPORT_LAYER__BSS_HARDFORK_1_INDEX=0
DATA_TRANSPORT_LAYER__ADDRESS_MANAGER= DATA_TRANSPORT_LAYER__ADDRESS_MANAGER=
DATA_TRANSPORT_LAYER__L1_RPC_ENDPOINT= DATA_TRANSPORT_LAYER__L1_RPC_ENDPOINT=
......
...@@ -31,11 +31,17 @@ interface Indexed { ...@@ -31,11 +31,17 @@ interface Indexed {
index: number index: number
} }
interface ExtraTransportDBOptions {
bssHardfork1Index?: number
}
export class TransportDB { export class TransportDB {
public db: SimpleDB public db: SimpleDB
public opts: ExtraTransportDBOptions
constructor(leveldb: LevelUp) { constructor(leveldb: LevelUp, opts?: ExtraTransportDBOptions) {
this.db = new SimpleDB(leveldb) this.db = new SimpleDB(leveldb)
this.opts = opts || {}
} }
public async putEnqueueEntries(entries: EnqueueEntry[]): Promise<void> { public async putEnqueueEntries(entries: EnqueueEntry[]): Promise<void> {
...@@ -254,26 +260,7 @@ export class TransportDB { ...@@ -254,26 +260,7 @@ export class TransportDB {
return null return null
} }
if (transaction.queueOrigin === 'l1') { return this._makeFullTransaction(transaction)
const enqueue = await this.getEnqueueByIndex(transaction.queueIndex)
if (enqueue === null) {
return null
}
return {
...transaction,
...{
blockNumber: enqueue.blockNumber,
timestamp: enqueue.timestamp,
gasLimit: enqueue.gasLimit,
target: enqueue.target,
origin: enqueue.origin,
data: enqueue.data,
},
}
} else {
return transaction
}
} }
public async getLatestFullTransaction(): Promise<TransactionEntry> { public async getLatestFullTransaction(): Promise<TransactionEntry> {
...@@ -293,31 +280,46 @@ export class TransportDB { ...@@ -293,31 +280,46 @@ export class TransportDB {
const fullTransactions = [] const fullTransactions = []
for (const transaction of transactions) { for (const transaction of transactions) {
if (transaction.queueOrigin === 'l1') { fullTransactions.push(await this._makeFullTransaction(transaction))
const enqueue = await this.getEnqueueByIndex(transaction.queueIndex)
if (enqueue === null) {
return null
}
fullTransactions.push({
...transaction,
...{
blockNumber: enqueue.blockNumber,
timestamp: enqueue.timestamp,
gasLimit: enqueue.gasLimit,
target: enqueue.target,
origin: enqueue.origin,
data: enqueue.data,
},
})
} else {
fullTransactions.push(transaction)
}
} }
return fullTransactions return fullTransactions
} }
private async _makeFullTransaction(
transaction: TransactionEntry
): Promise<TransactionEntry> {
// We only need to do extra work for L1 to L2 transactions.
if (transaction.queueOrigin !== 'l1') {
return transaction
}
const enqueue = await this.getEnqueueByIndex(transaction.queueIndex)
if (enqueue === null) {
return null
}
let timestamp = enqueue.timestamp
if (
typeof this.opts.bssHardfork1Index === 'number' &&
transaction.index >= this.opts.bssHardfork1Index
) {
timestamp = transaction.timestamp
}
return {
...transaction,
...{
blockNumber: enqueue.blockNumber,
timestamp,
gasLimit: enqueue.gasLimit,
target: enqueue.target,
origin: enqueue.origin,
data: enqueue.data,
},
}
}
private async _getLatestEntryIndex(key: string): Promise<number> { private async _getLatestEntryIndex(key: string): Promise<number> {
return this.db.get<number>(`${key}:latest`, 0) || 0 return this.db.get<number>(`${key}:latest`, 0) || 0
} }
......
...@@ -143,7 +143,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet< ...@@ -143,7 +143,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet<
.toNumber(), .toNumber(),
batchIndex: extraData.batchIndex.toNumber(), batchIndex: extraData.batchIndex.toNumber(),
blockNumber: BigNumber.from(0).toNumber(), blockNumber: BigNumber.from(0).toNumber(),
timestamp: BigNumber.from(0).toNumber(), timestamp: context.timestamp,
gasLimit: BigNumber.from(0).toString(), gasLimit: BigNumber.from(0).toString(),
target: constants.AddressZero, target: constants.AddressZero,
origin: constants.AddressZero, origin: constants.AddressZero,
......
...@@ -104,7 +104,9 @@ export class L1IngestionService extends BaseService<L1IngestionServiceOptions> { ...@@ -104,7 +104,9 @@ export class L1IngestionService extends BaseService<L1IngestionServiceOptions> {
} = {} as any } = {} as any
protected async _init(): Promise<void> { protected async _init(): Promise<void> {
this.state.db = new TransportDB(this.options.db) this.state.db = new TransportDB(this.options.db, {
bssHardfork1Index: this.options.bssHardfork1Index,
})
this.l1IngestionMetrics = registerMetrics(this.metrics) this.l1IngestionMetrics = registerMetrics(this.metrics)
......
...@@ -84,7 +84,9 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> { ...@@ -84,7 +84,9 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
this.l2IngestionMetrics = registerMetrics(this.metrics) this.l2IngestionMetrics = registerMetrics(this.metrics)
this.state.db = new TransportDB(this.options.db) this.state.db = new TransportDB(this.options.db, {
bssHardfork1Index: this.options.bssHardfork1Index,
})
this.state.l2RpcProvider = this.state.l2RpcProvider =
typeof this.options.l2RpcProvider === 'string' typeof this.options.l2RpcProvider === 'string'
......
...@@ -36,6 +36,7 @@ export interface L1DataTransportServiceOptions { ...@@ -36,6 +36,7 @@ export interface L1DataTransportServiceOptions {
defaultBackend: string defaultBackend: string
l1GasPriceBackend: string l1GasPriceBackend: string
l1StartHeight?: number l1StartHeight?: number
bssHardfork1Index?: number
} }
const optionSettings = { const optionSettings = {
......
...@@ -51,6 +51,7 @@ type ethNetwork = 'mainnet' | 'kovan' | 'goerli' ...@@ -51,6 +51,7 @@ type ethNetwork = 'mainnet' | 'kovan' | 'goerli'
useSentry: config.bool('use-sentry', false), useSentry: config.bool('use-sentry', false),
sentryDsn: config.str('sentry-dsn'), sentryDsn: config.str('sentry-dsn'),
sentryTraceRate: config.ufloat('sentry-trace-rate', 0.05), sentryTraceRate: config.ufloat('sentry-trace-rate', 0.05),
bssHardfork1Index: config.uint('bss-hardfork-1-index', null),
}) })
const stop = async (signal) => { const stop = async (signal) => {
......
...@@ -87,7 +87,10 @@ export class L1TransportServer extends BaseService<L1TransportServerOptions> { ...@@ -87,7 +87,10 @@ export class L1TransportServer extends BaseService<L1TransportServerOptions> {
await this.options.db.open() await this.options.db.open()
} }
this.state.db = new TransportDB(this.options.db) this.state.db = new TransportDB(this.options.db, {
bssHardfork1Index: this.options.bssHardfork1Index,
})
this.state.l1RpcProvider = this.state.l1RpcProvider =
typeof this.options.l1RpcProvider === 'string' typeof this.options.l1RpcProvider === 'string'
? new JsonRpcProvider(this.options.l1RpcProvider) ? new JsonRpcProvider(this.options.l1RpcProvider)
......
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