Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

tests: replica syncing #981

Merged
merged 14 commits into from
Jun 21, 2021
6 changes: 6 additions & 0 deletions .changeset/tidy-rivers-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@eth-optimism/integration-tests': patch
'@eth-optimism/data-transport-layer': patch
---

Add replica sync test to integration tests; handle 0 L2 blocks in DTL
1 change: 1 addition & 0 deletions .github/workflows/sync-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,5 @@ jobs:
working-directory: ./integration-tests
run: |
yarn
yarn build:integration
yarn test:sync
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import chai, { expect } from 'chai'
import { Wallet, BigNumber, providers } from 'ethers'
import { injectL2Context } from '@eth-optimism/core-utils'

import { sleep, l2Provider, verifierProvider } from '../test/shared/utils'
import {
sleep,
l2Provider,
verifierProvider,
waitForL2Geth,
} from '../test/shared/utils'
import { OptimismEnv } from '../test/shared/env'
import { DockerComposeNetwork } from '../test/shared/docker-compose'

Expand Down Expand Up @@ -33,14 +38,7 @@ describe('Syncing a verifier', () => {
verifier = new DockerComposeNetwork(['verifier'])
await verifier.up({ commandOptions: ['--scale', 'verifier=1'] })

// Wait for verifier to be looping
let logs = await verifier.logs()
while (!logs.out.includes('Starting Sequencer Loop')) {
await sleep(500)
logs = await verifier.logs()
}

provider = injectL2Context(verifierProvider)
provider = await waitForL2Geth(verifierProvider)
}

const syncVerifier = async (sequencerBlockNumber: number) => {
Expand Down
119 changes: 119 additions & 0 deletions integration-tests/sync-tests/2-sync-replica.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import chai, { expect } from 'chai'
import { Wallet, Contract, ContractFactory, providers } from 'ethers'
import { ethers } from 'hardhat'
import { injectL2Context } from '@eth-optimism/core-utils'

import {
sleep,
l2Provider,
replicaProvider,
waitForL2Geth,
} from '../test/shared/utils'
import { OptimismEnv } from '../test/shared/env'
import { DockerComposeNetwork } from '../test/shared/docker-compose'

describe('Syncing a replica', () => {
let env: OptimismEnv
let wallet: Wallet
let replica: DockerComposeNetwork
let provider: providers.JsonRpcProvider

const sequencerProvider = injectL2Context(l2Provider)

/* Helper functions */

const startReplica = async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is basically the same as startVerifier - it should be possible to have a common file where both the verifier and replica can use that deduplicates the code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^agreed, will refactor this!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tynes i switched both to poll the endpoint instead of waiting for docker logs! as a long term vision when both verifier and replicas are stable, i think these tests can go for a larger refactor in which they share the same test suite of transactions and we just use a verifier or replica switch to run each.

as of now though, the replica has matching state roots for pretty much everything but the verifier is the opposite, so i'm keeping them as separate files (also not in the test folder for now) so we could buff up the replica tests before uniswap launch.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok! Thats a good idea!

// Bring up new replica
replica = new DockerComposeNetwork(['replica'])
await replica.up({
commandOptions: ['--scale', 'replica=1'],
})

provider = await waitForL2Geth(replicaProvider)
}

const syncReplica = async (sequencerBlockNumber: number) => {
// Wait until replica has caught up to the sequencer
let latestReplicaBlock = (await provider.getBlock('latest')) as any
while (latestReplicaBlock.number < sequencerBlockNumber) {
await sleep(500)
latestReplicaBlock = (await provider.getBlock('latest')) as any
}

return provider.getBlock(sequencerBlockNumber)
}

before(async () => {
env = await OptimismEnv.new()
wallet = env.l2Wallet
})

after(async () => {
await replica.stop('replica')
await replica.rm()
})

describe('Basic transactions and ERC20s', () => {
const initialAmount = 1000
const tokenName = 'OVM Test'
const tokenDecimals = 8
const TokenSymbol = 'OVM'

let other: Wallet
let Factory__ERC20: ContractFactory
let ERC20: Contract

before(async () => {
other = Wallet.createRandom().connect(ethers.provider)
Factory__ERC20 = await ethers.getContractFactory('ERC20', wallet)
})

it('should sync dummy transaction', async () => {
const tx = {
to: '0x' + '1234'.repeat(10),
gasLimit: 4000000,
gasPrice: 0,
data: '0x',
value: 0,
}
const result = await wallet.sendTransaction(tx)
await result.wait()

const latestSequencerBlock = (await sequencerProvider.getBlock(
'latest'
)) as any

await startReplica()

const matchingReplicaBlock = (await syncReplica(
latestSequencerBlock.number
)) as any

expect(matchingReplicaBlock.stateRoot).to.eq(
latestSequencerBlock.stateRoot
)
})

it('should sync ERC20 deployment and transfer', async () => {
ERC20 = await Factory__ERC20.deploy(
initialAmount,
tokenName,
tokenDecimals,
TokenSymbol
)

const transfer = await ERC20.transfer(other.address, 100)
await transfer.wait()

const latestSequencerBlock = (await provider.getBlock('latest')) as any

const matchingReplicaBlock = (await syncReplica(
latestSequencerBlock.number
)) as any

expect(matchingReplicaBlock.stateRoot).to.eq(
latestSequencerBlock.stateRoot
)
})
})
})
1 change: 1 addition & 0 deletions integration-tests/test/shared/docker-compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type ServiceNames =
| 'l2geth'
| 'relayer'
| 'verifier'
| 'replica'

const OPS_DIRECTORY = path.join(process.cwd(), '../ops')
const DEFAULT_SERVICES: ServiceNames[] = [
Expand Down
22 changes: 21 additions & 1 deletion integration-tests/test/shared/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
getContractFactory,
getContractInterface,
} from '@eth-optimism/contracts'
import { remove0x, Watcher } from '@eth-optimism/core-utils'
import { injectL2Context, remove0x, Watcher } from '@eth-optimism/core-utils'
import {
Contract,
Wallet,
Expand All @@ -22,9 +22,11 @@ const env = cleanEnv(process.env, {
L1_URL: str({ default: 'http://localhost:9545' }),
L2_URL: str({ default: 'http://localhost:8545' }),
VERIFIER_URL: str({ default: 'http://localhost:8547' }),
REPLICA_URL: str({ default: 'http://localhost:8549' }),
L1_POLLING_INTERVAL: num({ default: 10 }),
L2_POLLING_INTERVAL: num({ default: 10 }),
VERIFIER_POLLING_INTERVAL: num({ default: 10 }),
REPLICA_POLLING_INTERVAL: num({ default: 10 }),
PRIVATE_KEY: str({
default:
'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
Expand All @@ -44,6 +46,9 @@ l2Provider.pollingInterval = env.L2_POLLING_INTERVAL
export const verifierProvider = new providers.JsonRpcProvider(env.VERIFIER_URL)
verifierProvider.pollingInterval = env.VERIFIER_POLLING_INTERVAL

export const replicaProvider = new providers.JsonRpcProvider(env.REPLICA_URL)
replicaProvider.pollingInterval = env.REPLICA_POLLING_INTERVAL

// The sequencer private key which is funded on L1
export const l1Wallet = new Wallet(env.PRIVATE_KEY, l1Provider)

Expand Down Expand Up @@ -111,3 +116,18 @@ const abiCoder = new utils.AbiCoder()
export const encodeSolidityRevertMessage = (_reason: string): string => {
return '0x08c379a0' + remove0x(abiCoder.encode(['string'], [_reason]))
}

export const waitForL2Geth = async (
provider: providers.JsonRpcProvider
): Promise<providers.JsonRpcProvider> => {
let ready: boolean = false
while (!ready) {
try {
await provider.getNetwork()
ready = true
} catch (error) {
await sleep(1000)
}
}
return injectL2Context(provider)
}
6 changes: 4 additions & 2 deletions ops/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ docker-compose \
up --build --detach
```

Optionally, run a verifier along the rest of the stack.
Optionally, run a verifier along the rest of the stack. Run a replica with the same command by switching the service name!
```
docker-compose up --scale verifier=1 \
docker-compose up --scale \
verifier=1 \
--build --detach
```


A Makefile has been provided for convience. The following targets are available.
- make up
- make down
Expand Down
32 changes: 28 additions & 4 deletions ops/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ services:
# connect to the 2 layers
DATA_TRANSPORT_LAYER__L1_RPC_ENDPOINT: http://l1_chain:8545
DATA_TRANSPORT_LAYER__L2_RPC_ENDPOINT: http://l2geth:8545
DATA_TRANSPORT_LAYER__SYNC_FROM_L2: 'true'
annieke marked this conversation as resolved.
Show resolved Hide resolved
DATA_TRANSPORT_LAYER__L2_CHAIN_ID: 420
ports:
- ${DTL_PORT:-7878}:7878
Expand Down Expand Up @@ -141,23 +142,46 @@ services:
build:
context: ..
dockerfile: ./ops/docker/Dockerfile.geth
# override with the geth script and the env vars required for it
entrypoint: sh ./geth.sh
env_file:
- ./envs/geth.env
environment:
ETH1_HTTP: http://l1_chain:8545
ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json
# used for getting the addresses
URL: http://deployer:8081/addresses.json
# connecting to the DTL
ROLLUP_CLIENT_HTTP: http://dtl:7878
ROLLUP_BACKEND: 'l1'
ETH1_CTC_DEPLOYMENT_HEIGHT: 8
RETRIES: 60
IS_VERIFIER: "true"
ROLLUP_VERIFIER_ENABLE: 'true'
ports:
- ${VERIFIER_HTTP_PORT:-8547}:8545
- ${VERIFIER_WS_PORT:-8548}:8546

replica:
depends_on:
- dtl
image: ethereumoptimism/l2geth
deploy:
replicas: 0
build:
context: ..
dockerfile: ./ops/docker/Dockerfile.geth
entrypoint: sh ./geth.sh
env_file:
- ./envs/geth.env
environment:
ETH1_HTTP: http://l1_chain:8545
ROLLUP_STATE_DUMP_PATH: http://deployer:8081/state-dump.latest.json
URL: http://deployer:8081/addresses.json
ROLLUP_CLIENT_HTTP: http://dtl:7878
ROLLUP_BACKEND: 'l2'
ROLLUP_VERIFIER_ENABLE: 'true'
ETH1_CTC_DEPLOYMENT_HEIGHT: 8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be any reason for us to be supplying this parameter. It just happens to be that the CTC is deployed at block 8 right now. Let's make an issue for this.

RETRIES: 60
ports:
- ${L2GETH_HTTP_PORT:-8549}:8545
- ${L2GETH_WS_PORT:-8550}:8546

integration_tests:
image: ethereumoptimism/integration-tests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ export class L2IngestionService extends BaseService<L2IngestionServiceOptions> {
)

// We're already at the head, so no point in attempting to sync.
if (highestSyncedL2BlockNumber === targetL2Block) {
// Also wait on edge case of no L2 transactions
if (
highestSyncedL2BlockNumber === targetL2Block ||
currentL2Block === 0
) {
await sleep(this.options.pollingInterval)
continue
}
Expand Down