diff --git a/.changeset/cool-starfishes-peel.md b/.changeset/cool-starfishes-peel.md new file mode 100644 index 0000000000000..dd6b5de031865 --- /dev/null +++ b/.changeset/cool-starfishes-peel.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/data-transport-layer': patch +--- + +Updates DTL to correctly parse L1 to L2 tx timestamps after the first bss hardfork diff --git a/.changeset/forty-dancers-try.md b/.changeset/forty-dancers-try.md new file mode 100644 index 0000000000000..12b6c13554fef --- /dev/null +++ b/.changeset/forty-dancers-try.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/integration-tests': patch +--- + +Updates integration tests to include a test for syncing a Verifier from L1 diff --git a/.changeset/green-donuts-bathe.md b/.changeset/green-donuts-bathe.md new file mode 100644 index 0000000000000..72aef390b4f3a --- /dev/null +++ b/.changeset/green-donuts-bathe.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/l2geth': patch +--- + +Fixes incorrect timestamp handling for L1 syncing verifiers diff --git a/.changeset/quick-drinks-tease.md b/.changeset/quick-drinks-tease.md new file mode 100644 index 0000000000000..646abace1380b --- /dev/null +++ b/.changeset/quick-drinks-tease.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/batch-submitter': patch +--- + +Updates batch submitter to also include separate timestamps for deposit transactions" diff --git a/.changeset/smooth-bags-applaud.md b/.changeset/smooth-bags-applaud.md new file mode 100644 index 0000000000000..3dab683549925 --- /dev/null +++ b/.changeset/smooth-bags-applaud.md @@ -0,0 +1,5 @@ +--- +'@eth-optimism/integration-tests': patch +--- + +Add verifier integration tests diff --git a/.dockerignore b/.dockerignore index 0d9649358bc45..0d754c92cd831 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,8 @@ .github node_modules +.env +**/.env test **/*_test.go diff --git a/integration-tests/test/shared/env.ts b/integration-tests/test/shared/env.ts index efbd6d96a98ab..9821787fd2589 100644 --- a/integration-tests/test/shared/env.ts +++ b/integration-tests/test/shared/env.ts @@ -11,6 +11,7 @@ import { l1Provider, l2Provider, replicaProvider, + verifierProvider, l1Wallet, l2Wallet, gasPriceOracleWallet, @@ -57,6 +58,7 @@ export class OptimismEnv { l1Provider: providers.JsonRpcProvider l2Provider: providers.JsonRpcProvider replicaProvider: providers.JsonRpcProvider + verifierProvider: providers.JsonRpcProvider constructor(args: any) { this.addressManager = args.addressManager @@ -74,6 +76,7 @@ export class OptimismEnv { this.l1Provider = args.l1Provider this.l2Provider = args.l2Provider this.replicaProvider = args.replicaProvider + this.verifierProvider = args.verifierProvider this.ctc = args.ctc this.scc = args.scc } @@ -140,6 +143,7 @@ export class OptimismEnv { l2Wallet, l1Provider, l2Provider, + verifierProvider, replicaProvider, }) } diff --git a/integration-tests/test/shared/utils.ts b/integration-tests/test/shared/utils.ts index dc44eaf759e76..dc77f1df70147 100644 --- a/integration-tests/test/shared/utils.ts +++ b/integration-tests/test/shared/utils.ts @@ -62,6 +62,8 @@ const procEnv = cleanEnv(process.env, { REPLICA_URL: str({ default: 'http://localhost:8549' }), REPLICA_POLLING_INTERVAL: num({ default: 10 }), + VERIFIER_URL: str({ default: 'http://localhost:8547' }), + PRIVATE_KEY: str({ default: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', @@ -96,6 +98,9 @@ const procEnv = cleanEnv(process.env, { RUN_NIGHTLY_TESTS: bool({ default: false, }), + RUN_VERIFIER_TESTS: bool({ + default: true, + }), MOCHA_TIMEOUT: num({ default: 120_000, @@ -121,6 +126,11 @@ export const replicaProvider = injectL2Context( ) 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 export const l1Wallet = new Wallet(procEnv.PRIVATE_KEY, l1Provider) diff --git a/integration-tests/test/verifier.spec.ts b/integration-tests/test/verifier.spec.ts new file mode 100644 index 0000000000000..86715127f64c8 --- /dev/null +++ b/integration-tests/test/verifier.spec.ts @@ -0,0 +1,103 @@ +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) + }) + }) +}) diff --git a/l2geth/miner/worker.go b/l2geth/miner/worker.go index e731ef8d54e3d..fbc96dcd6819a 100644 --- a/l2geth/miner/worker.go +++ b/l2geth/miner/worker.go @@ -931,18 +931,7 @@ func (w *worker) commitNewTx(tx *types.Transaction) error { // Preserve liveliness as best as possible. Must panic on L1 to L2 // transactions as the timestamp cannot be malleated if parent.Time() > tx.L1Timestamp() { - log.Error("Monotonicity violation", "index", num) - 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") - } + log.Error("Monotonicity violation", "index", num, "parent", parent.Time(), "tx", tx.L1Timestamp()) } // Fill in the index field in the tx meta if it is `nil`. diff --git a/l2geth/rollup/sync_service.go b/l2geth/rollup/sync_service.go index 138c8b1d2a0fb..55d297edb30b5 100644 --- a/l2geth/rollup/sync_service.go +++ b/l2geth/rollup/sync_service.go @@ -825,13 +825,14 @@ func (s *SyncService) applyTransactionToTip(tx *types.Transaction) error { if now.Sub(current) > s.timestampRefreshThreshold { current = now } + log.Info("Updating latest timestamp", "timestamp", current, "unix", current.Unix()) tx.SetL1Timestamp(uint64(current.Unix())) } else if tx.L1Timestamp() == 0 && s.verifier { // This should never happen 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 - 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() diff --git a/ops/docker-compose.yml b/ops/docker-compose.yml index 4a81acfa25a50..cf605e3c072a2 100644 --- a/ops/docker-compose.yml +++ b/ops/docker-compose.yml @@ -136,8 +136,9 @@ services: - l1_chain - deployer - dtl + - l2geth deploy: - replicas: 0 + replicas: 1 build: context: .. dockerfile: ./ops/docker/Dockerfile.geth @@ -146,6 +147,7 @@ services: - ./envs/geth.env environment: ETH1_HTTP: http://l1_chain:8545 + SEQUENCER_CLIENT_HTTP: http://l2geth:8545 ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json ROLLUP_CLIENT_HTTP: http://dtl:7878 ROLLUP_BACKEND: 'l1' diff --git a/ops/envs/dtl.env b/ops/envs/dtl.env index 314ed53bf4013..ffc1b59a862a7 100644 --- a/ops/envs/dtl.env +++ b/ops/envs/dtl.env @@ -9,6 +9,7 @@ DATA_TRANSPORT_LAYER__LOGS_PER_POLLING_INTERVAL=2000 DATA_TRANSPORT_LAYER__DANGEROUSLY_CATCH_ALL_ERRORS=true DATA_TRANSPORT_LAYER__SERVER_HOSTNAME=0.0.0.0 DATA_TRANSPORT_LAYER__L1_START_HEIGHT=1 +DATA_TRANSPORT_LAYER__BSS_HARDFORK_1_INDEX=0 DATA_TRANSPORT_LAYER__ADDRESS_MANAGER= DATA_TRANSPORT_LAYER__L1_RPC_ENDPOINT= diff --git a/packages/data-transport-layer/src/db/transport-db.ts b/packages/data-transport-layer/src/db/transport-db.ts index 4ed0d6a75677e..e4772892baaea 100644 --- a/packages/data-transport-layer/src/db/transport-db.ts +++ b/packages/data-transport-layer/src/db/transport-db.ts @@ -31,11 +31,17 @@ interface Indexed { index: number } +interface ExtraTransportDBOptions { + bssHardfork1Index?: number +} + export class TransportDB { public db: SimpleDB + public opts: ExtraTransportDBOptions - constructor(leveldb: LevelUp) { + constructor(leveldb: LevelUp, opts?: ExtraTransportDBOptions) { this.db = new SimpleDB(leveldb) + this.opts = opts || {} } public async putEnqueueEntries(entries: EnqueueEntry[]): Promise { @@ -254,26 +260,7 @@ export class TransportDB { return null } - if (transaction.queueOrigin === 'l1') { - 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 - } + return this._makeFullTransaction(transaction) } public async getLatestFullTransaction(): Promise { @@ -293,31 +280,46 @@ export class TransportDB { const fullTransactions = [] for (const transaction of transactions) { - if (transaction.queueOrigin === 'l1') { - 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) - } + fullTransactions.push(await this._makeFullTransaction(transaction)) } return fullTransactions } + private async _makeFullTransaction( + transaction: TransactionEntry + ): Promise { + // 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 { return this.db.get(`${key}:latest`, 0) || 0 } diff --git a/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts b/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts index 2b830d975aae8..763a5fc79c115 100644 --- a/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts +++ b/packages/data-transport-layer/src/services/l1-ingestion/handlers/sequencer-batch-appended.ts @@ -143,7 +143,7 @@ export const handleEventsSequencerBatchAppended: EventHandlerSet< .toNumber(), batchIndex: extraData.batchIndex.toNumber(), blockNumber: BigNumber.from(0).toNumber(), - timestamp: BigNumber.from(0).toNumber(), + timestamp: context.timestamp, gasLimit: BigNumber.from(0).toString(), target: constants.AddressZero, origin: constants.AddressZero, diff --git a/packages/data-transport-layer/src/services/l1-ingestion/service.ts b/packages/data-transport-layer/src/services/l1-ingestion/service.ts index 62738e9b09a0a..d5a64e95b2553 100644 --- a/packages/data-transport-layer/src/services/l1-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l1-ingestion/service.ts @@ -104,7 +104,9 @@ export class L1IngestionService extends BaseService { } = {} as any protected async _init(): Promise { - 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) diff --git a/packages/data-transport-layer/src/services/l2-ingestion/service.ts b/packages/data-transport-layer/src/services/l2-ingestion/service.ts index 8a1704ec1487f..9e73327877167 100644 --- a/packages/data-transport-layer/src/services/l2-ingestion/service.ts +++ b/packages/data-transport-layer/src/services/l2-ingestion/service.ts @@ -84,7 +84,9 @@ export class L2IngestionService extends BaseService { 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 = typeof this.options.l2RpcProvider === 'string' diff --git a/packages/data-transport-layer/src/services/main/service.ts b/packages/data-transport-layer/src/services/main/service.ts index 33449af1aa066..e96a1d2b2cb75 100644 --- a/packages/data-transport-layer/src/services/main/service.ts +++ b/packages/data-transport-layer/src/services/main/service.ts @@ -36,6 +36,7 @@ export interface L1DataTransportServiceOptions { defaultBackend: string l1GasPriceBackend: string l1StartHeight?: number + bssHardfork1Index?: number } const optionSettings = { diff --git a/packages/data-transport-layer/src/services/run.ts b/packages/data-transport-layer/src/services/run.ts index fdd54ab30f997..60e3f4000d5a9 100644 --- a/packages/data-transport-layer/src/services/run.ts +++ b/packages/data-transport-layer/src/services/run.ts @@ -51,6 +51,7 @@ type ethNetwork = 'mainnet' | 'kovan' | 'goerli' useSentry: config.bool('use-sentry', false), sentryDsn: config.str('sentry-dsn'), sentryTraceRate: config.ufloat('sentry-trace-rate', 0.05), + bssHardfork1Index: config.uint('bss-hardfork-1-index', null), }) const stop = async (signal) => { diff --git a/packages/data-transport-layer/src/services/server/service.ts b/packages/data-transport-layer/src/services/server/service.ts index 877053944e7d6..c5647cdc8cfd6 100644 --- a/packages/data-transport-layer/src/services/server/service.ts +++ b/packages/data-transport-layer/src/services/server/service.ts @@ -87,7 +87,10 @@ export class L1TransportServer extends BaseService { 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 = typeof this.options.l1RpcProvider === 'string' ? new JsonRpcProvider(this.options.l1RpcProvider)