Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,51 @@ jobs:
INTEGRATION_TEST_DECIMALS=${{matrix.config.decimals}} \
INTEGRATION_TEST_NITRO_CONTRACTS_BRANCH=${{matrix.config.nitro-contracts-branch}} \
pnpm test:integration

test-integration-anvil:
name: Test (Integration Anvil)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.30.3
run_install: false

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc

- name: Install node_modules
uses: OffchainLabs/actions/node-modules/install@main
with:
install-command: pnpm install --frozen-lockfile
cache-key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}

- name: Restore anvil RPC cache
id: anvil-rpc-cache-restore
uses: actions/cache/restore@v4
with:
path: .cache/anvil-rpc-cache.json
key: ${{ runner.os }}-anvil-rpc-cache-${{ github.ref_name }}-${{ github.run_id }}
restore-keys: |
${{ runner.os }}-anvil-rpc-cache-${{ github.ref_name }}-
${{ runner.os }}-anvil-rpc-cache-main-
${{ runner.os }}-anvil-rpc-cache-

- name: Build
run: pnpm build

- name: Test
run: pnpm test:integration:anvil

- name: Save anvil RPC cache
if: always()
uses: actions/cache/save@v4
with:
path: .cache/anvil-rpc-cache.json
key: ${{ runner.os }}-anvil-rpc-cache-${{ github.ref_name }}-${{ github.run_id }}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"test:type": "vitest --config vitest.type.config.ts",
"test:unit": "vitest --config vitest.unit.config.ts",
"test:integration": "vitest --config vitest.integration.config.ts",
"test:integration:anvil": "vitest --run --config vitest.integration.anvil.config.ts",
"postinstall": "patch-package",
"lint": "eslint . --cache",
"lint:fix": "eslint . --fix --cache",
Expand Down
34 changes: 26 additions & 8 deletions src/createRollup.integration.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,38 @@
import { describe, it, expect } from 'vitest';
import { createPublicClient, http, parseGwei, zeroAddress } from 'viem';
import { type Address, createPublicClient, http, parseGwei, zeroAddress } from 'viem';

import { nitroTestnodeL2 } from './chains';
import {
createRollupHelper,
getNitroTestnodePrivateKeyAccounts,
getInformationFromTestnode,
type PrivateKeyAccountWithPrivateKey,
} from './testHelpers';
import { createRollupFetchTransactionHash } from './createRollupFetchTransactionHash';
import { getInitializedAnvilTestStackEnv } from './integrationTestHelpers/anvilHarness';
import { isAnvilIntegrationTestMode } from './integrationTestHelpers/injectedMode';

const env = isAnvilIntegrationTestMode() ? getInitializedAnvilTestStackEnv() : undefined;

const parentChainPublicClient = createPublicClient({
chain: nitroTestnodeL2,
chain: env ? env.l2.chain : nitroTestnodeL2,
transport: http(),
});

// test inputs
const testnodeAccounts = getNitroTestnodePrivateKeyAccounts();
const l3TokenBridgeDeployer = testnodeAccounts.l3TokenBridgeDeployer;
const batchPosters = [testnodeAccounts.deployer.address];
const validators = [testnodeAccounts.deployer.address];
let l3TokenBridgeDeployer: PrivateKeyAccountWithPrivateKey;
let batchPosters: Address[];
let validators: Address[];

if (env) {
l3TokenBridgeDeployer = env.l3.accounts.tokenBridgeDeployer;
batchPosters = [env.l2.accounts.deployer.address];
validators = [env.l2.accounts.deployer.address];
} else {
const testnodeAccounts = getNitroTestnodePrivateKeyAccounts();
l3TokenBridgeDeployer = testnodeAccounts.l3TokenBridgeDeployer;
batchPosters = [testnodeAccounts.deployer.address];
validators = [testnodeAccounts.deployer.address];
}

describe(`create an AnyTrust chain that uses ETH as gas token`, async () => {
const { createRollupConfig, createRollupInformation } = await createRollupHelper({
Expand All @@ -27,6 +41,8 @@ describe(`create an AnyTrust chain that uses ETH as gas token`, async () => {
validators,
nativeToken: zeroAddress,
client: parentChainPublicClient,
customParentTimingParams: env?.l2.timingParams,
maxDataSize: env ? 104_857n : undefined,
});

it(`successfully deploys core contracts through rollup creator`, async () => {
Expand Down Expand Up @@ -58,14 +74,16 @@ describe(`create an AnyTrust chain that uses ETH as gas token`, async () => {
});

describe(`create an AnyTrust chain that uses a custom gas token`, async () => {
const nativeToken = getInformationFromTestnode().l3NativeToken;
const nativeToken = env ? env.l3.nativeToken : getInformationFromTestnode().l3NativeToken;

const { createRollupConfig, createRollupInformation } = await createRollupHelper({
deployer: l3TokenBridgeDeployer,
batchPosters,
validators,
nativeToken,
client: parentChainPublicClient,
customParentTimingParams: env?.l2.timingParams,
maxDataSize: env ? 104_857n : undefined,
});

it(`successfully deploys core contracts through rollup creator`, async () => {
Expand Down
19 changes: 13 additions & 6 deletions src/integrationTestHelpers/anvilHarness.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdtempSync, writeFileSync } from 'node:fs';
import { chmodSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

Expand Down Expand Up @@ -77,6 +77,12 @@ let cleanupHookRegistered = false;
let teardownStarted = false;
let l1RpcCachingProxy: RpcCachingProxy | undefined;

function prepareNitroRuntimeDir(runtimeDir: string) {
chmodSync(runtimeDir, 0o777);
mkdirSync(join(runtimeDir, 'nitro-data'), { recursive: true, mode: 0o777 });
chmodSync(join(runtimeDir, 'nitro-data'), 0o777);
}

export async function setupAnvilTestStack(): Promise<AnvilTestStack> {
if (envPromise) {
return envPromise;
Expand All @@ -97,6 +103,7 @@ export async function setupAnvilTestStack(): Promise<AnvilTestStack> {
cleanupStaleHarnessNetworks();

runtimeDir = mkdtempSync(join(tmpdir(), 'chain-sdk-int-test'));
prepareNitroRuntimeDir(runtimeDir);
dockerNetworkName = `chain-sdk-int-test-net-${Date.now()}`;
createSourceDockerNetwork(dockerNetworkName);

Expand Down Expand Up @@ -150,6 +157,7 @@ export async function setupAnvilTestStack(): Promise<AnvilTestStack> {
await waitForRpc({
rpcUrl: l1RpcUrl,
timeoutMs: 60_000,
failIfContainerExited: l1ContainerName,
});
console.log('L1 Anvil node is ready\n');

Expand Down Expand Up @@ -220,7 +228,7 @@ export async function setupAnvilTestStack(): Promise<AnvilTestStack> {
l2NodeConfig.node!['batch-poster']!.enable = false;
l2NodeConfig.node!.staker!.enable = false;
const nodeConfigPath = join(runtimeDir, 'source-l2-node-config.json');
writeFileSync(nodeConfigPath, JSON.stringify(l2NodeConfig, null, 2));
writeFileSync(nodeConfigPath, JSON.stringify(l2NodeConfig, null, 2), { mode: 0o644 });

// Starting L2 node (Nitro)
console.log('Starting L2 Nitro node...');
Expand Down Expand Up @@ -250,6 +258,7 @@ export async function setupAnvilTestStack(): Promise<AnvilTestStack> {
await waitForRpc({
rpcUrl: l2RpcUrl,
timeoutMs: 60_000,
failIfContainerExited: l2ContainerName,
});
console.log('L2 Nitro node is ready\n');

Expand Down Expand Up @@ -329,15 +338,13 @@ export async function setupAnvilTestStack(): Promise<AnvilTestStack> {

console.log('L2 rollup creator deployed\n');

const customGasTokenMintAmount = parseEther('10');

await (
await customGasToken.deposit({
value: customGasTokenMintAmount,
value: parseEther('10'),
...testConstants.LOW_L2_FEE_OVERRIDES,
})
).wait();
console.log('[chain-sdk-int-test] source L2 shared deployer funded');
console.log('L2 funding done\n');

const l2Chain = defineChain({
id: l2ChainId,
Expand Down
51 changes: 48 additions & 3 deletions src/integrationTestHelpers/dockerHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,18 +87,63 @@ export function createSourceDockerNetwork(networkName: string) {
docker(['network', 'create', networkName]);
}

export async function waitForRpc(params: { rpcUrl: string; timeoutMs: number }) {
const { rpcUrl, timeoutMs } = params;
function getContainerStatus(containerName: string): string | undefined {
try {
const status = docker(['inspect', '-f', '{{.State.Status}}', containerName]);
return status || undefined;
} catch {
return undefined;
}
}

function getContainerLogs(containerName: string, tail = 80): string {
try {
return docker(['logs', '--tail', String(tail), containerName]);
} catch {
return '';
}
}

export async function waitForRpc(params: {
rpcUrl: string;
timeoutMs: number;
failIfContainerExited?: string;
}) {
const { rpcUrl, timeoutMs, failIfContainerExited } = params;
const publicClient = createPublicClient({ transport: http(rpcUrl) });
const deadline = Date.now() + timeoutMs;

const poll = async (): Promise<void> => {
if (failIfContainerExited) {
const status = getContainerStatus(failIfContainerExited);
if (status && status !== 'running') {
const logs = getContainerLogs(failIfContainerExited);
throw new Error(
`Container ${failIfContainerExited} exited with status "${status}" while waiting for RPC ${rpcUrl}.${
logs ? `\n${logs}` : ''
}`,
);
}
}

try {
await publicClient.getChainId();
return;
} catch {
if (Date.now() >= deadline) {
throw new Error(`Timed out waiting for RPC ${rpcUrl}`);
const containerContext =
failIfContainerExited && getContainerStatus(failIfContainerExited)
? ` (container ${failIfContainerExited} status: ${getContainerStatus(
failIfContainerExited,
)})`
: '';
const logs =
failIfContainerExited && getContainerStatus(failIfContainerExited) !== 'running'
? getContainerLogs(failIfContainerExited)
: '';
throw new Error(
`Timed out waiting for RPC ${rpcUrl}${containerContext}.${logs ? `\n${logs}` : ''}`,
);
}
}

Expand Down
12 changes: 10 additions & 2 deletions src/integrationTestHelpers/rpcCachingProxy.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { once } from 'node:events';
import { createServer, IncomingHttpHeaders } from 'node:http';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname } from 'node:path';
Expand Down Expand Up @@ -220,10 +221,17 @@ export async function startRpcCachingProxy(
}
});

server.listen(8449, '0.0.0.0');
server.listen(0, '0.0.0.0');
await once(server, 'listening');

const address = server.address();
if (!address || typeof address === 'string') {
server.close();
throw new Error('RPC caching proxy failed to resolve a listening TCP port.');
}

return {
proxyUrl: `http://host.docker.internal:8449`,
proxyUrl: `http://host.docker.internal:${address.port}`,
getSummaryLines: () => [
'RPC proxy cache summary',
` invalidated on startup: ${stats.cacheInvalidated ? 'yes' : 'no'}`,
Expand Down
19 changes: 19 additions & 0 deletions vitest.integration.anvil.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { configDefaults, defineConfig, mergeConfig } from 'vitest/config';
import commonConfig from './vitest.common';

export default mergeConfig(
commonConfig,
defineConfig({
test: {
provide: {
integrationTestMode: 'anvil',
},
// The Anvil stack boots a forked L1 and dockerized Nitro L2 in-process.
testTimeout: 45 * 60 * 1000,
setupFiles: ['./src/integrationTestHelpers/globalSetup.mjs'],
exclude: [...configDefaults.exclude],
include: ['./src/createRollup.integration.test.ts'],
fileParallelism: false,
},
}),
);
3 changes: 3 additions & 0 deletions vitest.integration.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export default mergeConfig(
commonConfig,
defineConfig({
test: {
provide: {
integrationTestMode: 'testnode',
},
// allow tests to run for 7 minutes as retryables can take a while
testTimeout: 7 * 60 * 1000,
exclude: [...configDefaults.exclude, './src/**/*.unit.test.ts'],
Expand Down
Loading