From f4c809f7329a2ea8d448c066f36093f5df3debdb Mon Sep 17 00:00:00 2001 From: Jason-Wanxt Date: Wed, 25 Mar 2026 05:10:11 +0800 Subject: [PATCH 1/3] add force inclusion example --- README.md | 1 + .../.claude/settings.local.json | 14 + packages/force-inclusion/.env-sample | 18 + packages/force-inclusion/README.md | 92 ++++ packages/force-inclusion/fullnode-config.json | 54 ++ packages/force-inclusion/package.json | 16 + .../scripts/force-inclusion-test.js | 463 ++++++++++++++++++ 7 files changed, 658 insertions(+) create mode 100644 packages/force-inclusion/.claude/settings.local.json create mode 100644 packages/force-inclusion/.env-sample create mode 100644 packages/force-inclusion/README.md create mode 100644 packages/force-inclusion/fullnode-config.json create mode 100644 packages/force-inclusion/package.json create mode 100644 packages/force-inclusion/scripts/force-inclusion-test.js diff --git a/README.md b/README.md index 6c09098..0e968fa 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ Using public testnet RPCs can be slow because many tutorials wait for transactio - 🌉 [Bridging a custom token through the generic-custom gateway](./packages/custom-token-bridging/) - 🌉 [Bridging a custom token through a custom gateway](./packages/custom-gateway-bridging/) - ✈️ [Send a signed transaction from the parent chain](./packages/delayedInbox-l2msg/) +- 🛡️ [Force inclusion end-to-end test](./packages/force-inclusion/) - 🎁 [Redeem pending retryable ticket](./packages/redeem-pending-retryable/) - 🧮 [Gas estimation](./packages/gas-estimation/) - 🌀 [Deposit Ether or Tokens from L1 to L3](./packages/l1-l3-teleport/) diff --git a/packages/force-inclusion/.claude/settings.local.json b/packages/force-inclusion/.claude/settings.local.json new file mode 100644 index 0000000..d2618c7 --- /dev/null +++ b/packages/force-inclusion/.claude/settings.local.json @@ -0,0 +1,14 @@ +{ + "permissions": { + "allow": [ + "Bash(node:*)", + "Bash(find /Users/jasonwan1/Work/code/arbitrum-tutorials/packages/force-inclusion/node_modules/@arbitrum/chain-sdk -name SequencerInbox*)", + "Bash(docker ps:*)", + "Bash(git:*)", + "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\(''''scripts'''',{}\\), indent=2\\)\\)\")", + "Bash(yarn build:*)", + "Bash(docker image:*)", + "Bash(docker run:*)" + ] + } +} diff --git a/packages/force-inclusion/.env-sample b/packages/force-inclusion/.env-sample new file mode 100644 index 0000000..b77b1f6 --- /dev/null +++ b/packages/force-inclusion/.env-sample @@ -0,0 +1,18 @@ +# This is a sample .env file for use in local development. +# Duplicate this file as .env here + +# Private key of the deployer (must have ETH on the parent chain) +DEPLOYER_PRIVATE_KEY="0x your key here" + +# The parent chain's RPC +# (default: Arbitrum Sepolia) +PARENT_CHAIN_RPC="https://sepolia-rollup.arbitrum.io/rpc" + +# (Optional) Parent chain ID +# Defaults to Arbitrum Sepolia (421614) if not set +# PARENT_CHAIN_ID=421614 + +# (Optional) Batch poster and validator keys +# Auto-generated if not set (only needed for --with-node) +# BATCH_POSTER_PRIVATE_KEY="0x..." +# VALIDATOR_PRIVATE_KEY="0x..." diff --git a/packages/force-inclusion/README.md b/packages/force-inclusion/README.md new file mode 100644 index 0000000..44c4a7b --- /dev/null +++ b/packages/force-inclusion/README.md @@ -0,0 +1,92 @@ +# Tutorial: Force Inclusion End-to-End Test + +`force-inclusion` demonstrates Arbitrum's **censorship resistance** mechanism end-to-end. It deploys a fresh Orbit rollup with a short force inclusion delay (90 seconds), deposits ETH via the delayed inbox, waits for the delay window to pass, and then force includes the deposit — all without a running sequencer. + +## What is force inclusion? + +Arbitrum's [Sequencer](https://docs.arbitrum.io/how-arbitrum-works/sequencer) normally orders transactions. But what if the sequencer goes offline or starts censoring? Arbitrum guarantees that any message sent to the **delayed inbox** on the parent chain can be **force included** into the chain's inbox after a time delay, bypassing the sequencer entirely. + +This tutorial runs the full cycle in a single command: + +1. **Deploy** a new Orbit rollup with `maxTimeVariation.delaySeconds = 90` (instead of the default 24 hours) +2. **Deposit** ETH via `Inbox.depositEth()` on the parent chain (goes into the delayed inbox) +3. **Force include** the deposit by calling `SequencerInbox.forceInclusion()` after the delay window passes +4. *(Optional)* **Start a fullnode** (no sequencer) to verify the deposit appears on the child chain + +## Prerequisites + +- Node.js 18+ +- A deployer account with ETH on the parent chain (Arbitrum Sepolia or a custom parent chain) +- Docker (only needed for the `--with-node` option) + +## Set environment variables + +Copy the sample env file and fill in your values: + +```bash +cp .env-sample .env +``` + +Required variables: + +| Variable | Description | +|---|---| +| `DEPLOYER_PRIVATE_KEY` | Private key of the deployer (must have ETH on the parent chain) | +| `PARENT_CHAIN_RPC` | RPC URL of the parent chain | + +Optional variables: + +| Variable | Description | +|---|---| +| `PARENT_CHAIN_ID` | Parent chain ID (defaults to Arbitrum Sepolia 421614) | +| `BATCH_POSTER_PRIVATE_KEY` | Batch poster key (auto-generated if not set) | +| `VALIDATOR_PRIVATE_KEY` | Validator key (auto-generated if not set) | + +## Run + +Run steps 1–3 (deploy, deposit, force include): + +```bash +yarn test +``` + +Run all 4 steps including fullnode verification (requires Docker): + +```bash +yarn test:withNode +``` + +## How it works + +### Rollup deployment + +The script uses `@arbitrum/chain-sdk` (Orbit SDK) to deploy a new rollup. The key configuration is `sequencerInboxMaxTimeVariation`, which controls how long a delayed message must wait before it can be force included: + +```js +sequencerInboxMaxTimeVariation: { + delayBlocks: 6n, + futureBlocks: 12n, + delaySeconds: 90n, + futureSeconds: 3600n, +} +``` + +### Delayed inbox deposit + +ETH is deposited using `@arbitrum/sdk`'s `EthBridger.deposit()`. Under the hood, this calls `Inbox.depositEth()` on the parent chain, which routes through `bridge.enqueueDelayedMessage()`. Without a running sequencer, this message stays in the delayed inbox. + +### Force inclusion + +After the delay window passes (90 seconds + 6 blocks), `InboxTools.forceInclude()` calls `SequencerInbox.forceInclusion()` on the parent chain. This emits the same `SequencerBatchDelivered` event as a normal sequencer batch, making the deposit part of the canonical chain. + +### Fullnode verification (--with-node) + +When `--with-node` is passed, the script starts a Nitro fullnode via Docker with the sequencer disabled (`node.sequencer = false`, `execution.forwarding-target = "null"`). The fullnode reads from the parent chain and processes the force-included batch, allowing you to verify that the deposited ETH appears on the child chain. + +## Related tutorials + +- [Send a signed transaction from the parent chain](../delayedInbox-l2msg/) — demonstrates sending transactions via the delayed inbox with a running sequencer + +

+ +

diff --git a/packages/force-inclusion/fullnode-config.json b/packages/force-inclusion/fullnode-config.json new file mode 100644 index 0000000..527ce66 --- /dev/null +++ b/packages/force-inclusion/fullnode-config.json @@ -0,0 +1,54 @@ +{ + "chain": { + "info-json": "[{\"chain-id\":835090776,\"parent-chain-id\":421614,\"parent-chain-is-arbitrum\":true,\"chain-name\":\"ForceInclusionTestChain\",\"chain-config\":{\"chainId\":835090776,\"homesteadBlock\":0,\"daoForkBlock\":null,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"berlinBlock\":0,\"londonBlock\":0,\"clique\":{\"period\":0,\"epoch\":0},\"arbitrum\":{\"EnableArbOS\":true,\"AllowDebugPrecompiles\":false,\"DataAvailabilityCommittee\":false,\"InitialArbOSVersion\":51,\"InitialChainOwner\":\"0x860C58951A77ac2F2Cb452cC9F36B1d1FdF7c521\",\"GenesisBlockNum\":0,\"MaxCodeSize\":24576,\"MaxInitCodeSize\":49152}},\"rollup\":{\"bridge\":\"0x301C08f30060e365ddA1bA462E47Cd8F164f9f3F\",\"inbox\":\"0xDaebf491243df4BfB42f8eD5faA665F25C6dB3E0\",\"sequencer-inbox\":\"0x82b4c8c9005Bc43F90d18853E1a3C6C42322CFC7\",\"rollup\":\"0xEdB4d8ab6d51B0C623D049732Cc3a3Dbe22e0541\",\"validator-wallet-creator\":\"0x2c37dCBCE3fbe32c9Ba62892F1E41DbB023BB62b\",\"stake-token\":\"0x0000000000000000000000000000000000000000\",\"deployed-at\":253218540}}]", + "name": "ForceInclusionTestChain" + }, + "parent-chain": { + "connection": { + "url": "https://arb-sepolia.g.alchemy.com/v2/A9Awdfc74bkIT_CzQkOcyM036BSQxyk4" + } + }, + "http": { + "addr": "0.0.0.0", + "port": 8449, + "vhosts": [ + "*" + ], + "corsdomain": [ + "*" + ], + "api": [ + "eth", + "net", + "web3", + "arb", + "debug" + ] + }, + "node": { + "sequencer": false, + "delayed-sequencer": { + "enable": false + }, + "batch-poster": { + "enable": false + }, + "staker": { + "enable": false + }, + "dangerous": { + "no-sequencer-coordinator": true, + "disable-blob-reader": false + }, + "ensure-rollup-deployment": false + }, + "execution": { + "forwarding-target": "null", + "sequencer": { + "enable": false + }, + "caching": { + "archive": true + } + } +} \ No newline at end of file diff --git a/packages/force-inclusion/package.json b/packages/force-inclusion/package.json new file mode 100644 index 0000000..57cd9af --- /dev/null +++ b/packages/force-inclusion/package.json @@ -0,0 +1,16 @@ +{ + "name": "force-inclusion", + "version": "1.0.0", + "description": "End-to-end force inclusion test: deploy a rollup with a short delay, deposit ETH via delayed inbox, and force include — all without a sequencer.", + "scripts": { + "test": "node scripts/force-inclusion-test.js", + "test:withNode": "node scripts/force-inclusion-test.js --with-node" + }, + "author": "Offchain Labs, Inc.", + "license": "Apache-2.0", + "dependencies": { + "@arbitrum/sdk": "^v4.0.1", + "@arbitrum/chain-sdk": "^0.25.0", + "viem": "^1.20.0" + } +} diff --git a/packages/force-inclusion/scripts/force-inclusion-test.js b/packages/force-inclusion/scripts/force-inclusion-test.js new file mode 100644 index 0000000..c95134d --- /dev/null +++ b/packages/force-inclusion/scripts/force-inclusion-test.js @@ -0,0 +1,463 @@ +/** + * Force Inclusion End-to-End Test + * + * This script runs the full force inclusion cycle in one command: + * 1. Deploy a new Orbit rollup with a short force inclusion delay (90s) + * 2. Deposit ETH via the delayed inbox on the parent chain + * 3. Wait for the delay window, then force include the deposit + * 4. (Optional, --with-node) Start a fullnode to verify the L2 state + * + * Usage: + * node scripts/force-inclusion-test.js # Steps 1-3 + * node scripts/force-inclusion-test.js --with-node # Steps 1-4 (requires docker) + * + * Required environment variables: + * DEPLOYER_PRIVATE_KEY - Private key of the deployer (must have ETH on parent chain) + * PARENT_CHAIN_RPC - RPC URL of the parent chain + * + * Optional: + * PARENT_CHAIN_ID - Parent chain ID (default: Arbitrum Sepolia) + */ + +const { createPublicClient, http, defineChain } = require('viem'); +const { privateKeyToAccount, generatePrivateKey } = require('viem/accounts'); +const { arbitrumSepolia } = require('viem/chains'); +const { + createRollupPrepareDeploymentParamsConfig, + prepareChainConfig, + createRollupPrepareTransactionRequest, + createRollupPrepareTransactionReceipt, + prepareNodeConfig, +} = require('@arbitrum/chain-sdk'); +const { generateChainId, sanitizePrivateKey } = require('@arbitrum/chain-sdk/utils'); +const { + SequencerInbox__factory, +} = require('@arbitrum/sdk/dist/lib/abi/factories/SequencerInbox__factory'); +const { utils, providers, Wallet } = require('ethers'); +const { EthBridger, InboxTools, registerCustomArbitrumNetwork } = require('@arbitrum/sdk'); +const { execSync } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +require('dotenv').config(); + +// ============================================================ +// Config +// ============================================================ + +const WITH_NODE = process.argv.includes('--with-node'); + +const FORCE_INCLUSION_DELAY_SECONDS = 72n; +const FORCE_INCLUSION_DELAY_BLOCKS = 6n; +const FUTURE_SECONDS = 3600n; +const FUTURE_BLOCKS = 12n; + +const DEPOSIT_AMOUNT = utils.parseEther('0.01'); +const POLL_INTERVAL_SECONDS = 10; +const FULLNODE_RPC = 'http://localhost:8449'; +const SYNC_TIMEOUT_MS = 180_000; + +// ============================================================ +// Helpers +// ============================================================ + +function requireEnv(name) { + if (!process.env[name]) { + throw new Error(`Please provide the "${name}" environment variable`); + } + return process.env[name]; +} + +function getParentChain(parentChainRpc) { + if (process.env.PARENT_CHAIN_ID) { + const id = Number(process.env.PARENT_CHAIN_ID); + if (id === arbitrumSepolia.id) return { chain: arbitrumSepolia, isCustom: false }; + return { + chain: defineChain({ + id, + name: 'Custom Parent Chain', + nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, + rpcUrls: { default: { http: [parentChainRpc] } }, + }), + isCustom: true, + }; + } + return { chain: arbitrumSepolia, isCustom: false }; +} + +// ============================================================ +// Step 1: Deploy Rollup +// ============================================================ + +async function deployRollup() { + console.log('\n========================================'); + console.log(' Step 1: Deploy Orbit Rollup'); + console.log('========================================\n'); + + const deployerPrivateKey = sanitizePrivateKey(requireEnv('DEPLOYER_PRIVATE_KEY')); + const deployer = privateKeyToAccount(deployerPrivateKey); + const parentChainRpc = requireEnv('PARENT_CHAIN_RPC'); + + console.log(`Deployer: ${deployer.address}`); + + // Generate batch poster and validator keys (required for deployment but not used) + const batchPosterPrivateKey = process.env.BATCH_POSTER_PRIVATE_KEY + ? sanitizePrivateKey(process.env.BATCH_POSTER_PRIVATE_KEY) + : generatePrivateKey(); + const batchPoster = privateKeyToAccount(batchPosterPrivateKey).address; + + const validatorPrivateKey = process.env.VALIDATOR_PRIVATE_KEY + ? sanitizePrivateKey(process.env.VALIDATOR_PRIVATE_KEY) + : generatePrivateKey(); + const validator = privateKeyToAccount(validatorPrivateKey).address; + + const { chain: parentChain, isCustom: parentChainIsCustom } = getParentChain(parentChainRpc); + const parentChainPublicClient = createPublicClient({ + chain: parentChain, + transport: http(parentChainRpc), + }); + + const chainId = generateChainId(); + console.log(`New rollup chain ID: ${chainId}`); + console.log( + `Force inclusion delay: ${FORCE_INCLUSION_DELAY_SECONDS}s / ${FORCE_INCLUSION_DELAY_BLOCKS} blocks`, + ); + + const chainConfig = prepareChainConfig({ + chainId, + arbitrum: { + InitialChainOwner: deployer.address, + DataAvailabilityCommittee: false, + }, + }); + + const deploymentParamsConfig = { + chainId: BigInt(chainId), + owner: deployer.address, + chainConfig, + sequencerInboxMaxTimeVariation: { + delayBlocks: FORCE_INCLUSION_DELAY_BLOCKS, + futureBlocks: FUTURE_BLOCKS, + delaySeconds: FORCE_INCLUSION_DELAY_SECONDS, + futureSeconds: FUTURE_SECONDS, + }, + }; + + if (parentChainIsCustom) { + deploymentParamsConfig.confirmPeriodBlocks = 150n; + } + + const createRollupConfig = createRollupPrepareDeploymentParamsConfig( + parentChainPublicClient, + deploymentParamsConfig, + ); + + console.log('\nPreparing deployment transaction...'); + const request = await createRollupPrepareTransactionRequest({ + params: { + config: createRollupConfig, + batchPosters: [batchPoster], + validators: [validator], + }, + account: deployer.address, + publicClient: parentChainPublicClient, + }); + + console.log('Sending deployment transaction...'); + const txHash = await parentChainPublicClient.sendRawTransaction({ + serializedTransaction: await deployer.signTransaction(request), + }); + console.log(`Transaction hash: ${txHash}`); + + console.log('Waiting for confirmation...'); + const txReceipt = createRollupPrepareTransactionReceipt( + await parentChainPublicClient.waitForTransactionReceipt({ hash: txHash }), + ); + + const coreContracts = txReceipt.getCoreContracts(); + console.log('\nRollup deployed!'); + console.log(` Rollup: ${coreContracts.rollup}`); + console.log(` Inbox: ${coreContracts.inbox}`); + console.log(` SequencerInbox: ${coreContracts.sequencerInbox}`); + console.log(` Bridge: ${coreContracts.bridge}`); + + // Generate fullnode config (for optional --with-node step) + const nodeConfig = prepareNodeConfig({ + chainName: 'ForceInclusionTestChain', + chainConfig, + coreContracts, + batchPosterPrivateKey, + validatorPrivateKey, + stakeToken: '0x0000000000000000000000000000000000000000', + parentChainId: parentChain.id, + parentChainRpcUrl: parentChainRpc, + }); + nodeConfig.node.sequencer = false; + nodeConfig.node['delayed-sequencer'] = { enable: false }; + nodeConfig.node['batch-poster'] = { enable: false }; + nodeConfig.node.staker = { enable: false }; + nodeConfig.execution.sequencer = { enable: false }; + nodeConfig.execution['forwarding-target'] = 'null'; + nodeConfig['ensure-rollup-deployment'] = false; + + const nodeConfigPath = path.join(__dirname, '..', 'fullnode-config.json'); + fs.writeFileSync(nodeConfigPath, JSON.stringify(nodeConfig, null, 2)); + + return { + chainId, + parentChainId: parentChain.id, + coreContracts, + chainConfig, + nodeConfigPath, + }; +} + +// ============================================================ +// Step 2: Deposit ETH via Delayed Inbox +// ============================================================ + +async function depositEth(chainId, coreContracts, parentChainId) { + console.log('\n========================================'); + console.log(' Step 2: Deposit ETH via Delayed Inbox'); + console.log('========================================\n'); + + const parentChainProvider = new providers.JsonRpcProvider(requireEnv('PARENT_CHAIN_RPC')); + const parentChainWallet = new Wallet(requireEnv('DEPLOYER_PRIVATE_KEY'), parentChainProvider); + + // Register the custom network with Arbitrum SDK (v4 API) + const childChainNetwork = registerCustomArbitrumNetwork({ + chainId, + parentChainId, + confirmPeriodBlocks: 150, + ethBridge: { + bridge: coreContracts.bridge, + inbox: coreContracts.inbox, + outbox: coreContracts.outbox, + rollup: coreContracts.rollup, + sequencerInbox: coreContracts.sequencerInbox, + }, + isCustom: true, + isTestnet: true, + name: 'ForceInclusionTestChain', + }); + const ethBridger = new EthBridger(childChainNetwork); + + console.log(`Depositing ${utils.formatEther(DEPOSIT_AMOUNT)} ETH to the delayed inbox...`); + console.log(`Depositor: ${parentChainWallet.address}`); + + const depositTx = await ethBridger.deposit({ + amount: DEPOSIT_AMOUNT, + parentSigner: parentChainWallet, + childProvider: parentChainProvider, + }); + + const depositReceipt = await depositTx.wait(); + console.log(`\nDeposit confirmed! TX: ${depositReceipt.transactionHash}`); + console.log('The deposit is now in the delayed inbox. No sequencer will pick it up.'); + + return childChainNetwork; +} + +// ============================================================ +// Step 3: Force Include +// ============================================================ + +async function forceInclude(childChainNetwork) { + console.log('\n========================================'); + console.log(' Step 3: Force Include'); + console.log('========================================\n'); + + const parentChainProvider = new providers.JsonRpcProvider(requireEnv('PARENT_CHAIN_RPC')); + const parentChainWallet = new Wallet(requireEnv('DEPLOYER_PRIVATE_KEY'), parentChainProvider); + + const inboxTools = new InboxTools(parentChainWallet, childChainNetwork); + + console.log(`Waiting for force inclusion delay (~${FORCE_INCLUSION_DELAY_SECONDS}s) to pass...`); + console.log('Polling for force-includable events...\n'); + + let forceIncludableEvent = null; + while (!forceIncludableEvent) { + try { + // eslint-disable-next-line no-await-in-loop + forceIncludableEvent = await inboxTools.getForceIncludableEvent(); + } catch (e) { + console.log( + ` Query failed (likely L1 block not yet mapped). Retrying in ${POLL_INTERVAL_SECONDS}s...`, + ); + } + if (!forceIncludableEvent) { + console.log(` No eligible messages yet. Retrying in ${POLL_INTERVAL_SECONDS}s...`); + // eslint-disable-next-line no-await-in-loop + await new Promise((r) => setTimeout(r, POLL_INTERVAL_SECONDS * 1000)); + } + } + + console.log('Found force-includable event!'); + console.log(` Message index: ${forceIncludableEvent.event.messageIndex.toString()}`); + + console.log('\nCalling forceInclude()...'); + const tx = await inboxTools.forceInclude(forceIncludableEvent); + const receipt = await tx.wait(); + + console.log(`\nForce inclusion successful!`); + console.log(` TX: ${receipt.transactionHash}`); + + // Verify totalDelayedMessagesRead + + const sequencerInbox = SequencerInbox__factory.connect( + childChainNetwork.ethBridge.sequencerInbox, + parentChainProvider, + ); + const totalRead = await sequencerInbox.totalDelayedMessagesRead(); + console.log(` totalDelayedMessagesRead: ${totalRead.toString()}`); +} + +// ============================================================ +// Step 4 (Optional): Verify on Fullnode +// ============================================================ + +async function verifyOnFullnode(nodeConfigPath) { + console.log('\n========================================'); + console.log(' Step 4: Verify on Fullnode (--with-node)'); + console.log('========================================\n'); + + if (!fs.existsSync(nodeConfigPath)) { + console.error('fullnode-config.json not found.'); + return; + } + + console.log('Starting fullnode via docker (sequencer disabled)...'); + let dockerContainerId; + try { + dockerContainerId = execSync( + `docker run -d ` + + `-v ${nodeConfigPath}:/config/nodeConfig.json ` + + `-p 8449:8449 ` + + `offchainlabs/nitro-node:v3.9.5-66e42c4 ` + + `--conf.file /config/nodeConfig.json`, + { encoding: 'utf8' }, + ).trim(); + console.log(`Container started: ${dockerContainerId.substring(0, 12)}`); + + // Wait briefly then verify the container is still running + await new Promise((r) => setTimeout(r, 3000)); + const running = execSync( + `docker ps -q --filter id=${dockerContainerId}`, + { encoding: 'utf8' }, + ).trim(); + if (!running) { + console.error('Container exited immediately. Logs:'); + try { + console.error(execSync(`docker logs ${dockerContainerId}`, { encoding: 'utf8' })); + } catch (_) { /* ignore */ } + try { execSync(`docker rm ${dockerContainerId}`, { encoding: 'utf8' }); } catch (_) { /* ignore */ } + return; + } + + console.log('Waiting for node to initialize...\n'); + } catch (error) { + console.error('Failed to start docker container. Is docker running?'); + console.error(error.message); + return; + } + + try { + console.log(`Connecting to fullnode RPC: ${FULLNODE_RPC}`); + let childChainProvider; + let connected = false; + const connectStart = Date.now(); + while (Date.now() - connectStart < SYNC_TIMEOUT_MS) { + try { + childChainProvider = new providers.JsonRpcProvider(FULLNODE_RPC); + // eslint-disable-next-line no-await-in-loop + const network = await childChainProvider.getNetwork(); + console.log(`Connected! Chain ID: ${network.chainId}\n`); + connected = true; + break; + } catch (e) { + console.log(' Node not ready yet, retrying in 5s...'); + // eslint-disable-next-line no-await-in-loop + await new Promise((r) => setTimeout(r, 5_000)); + } + } + if (!connected) { + console.error('\nTimeout: could not connect to fullnode RPC.'); + return; + } + const wallet = new Wallet(requireEnv('DEPLOYER_PRIVATE_KEY'), childChainProvider); + + console.log('Waiting for fullnode to sync...'); + const startTime = Date.now(); + let latestBlock; + while (Date.now() - startTime < SYNC_TIMEOUT_MS) { + latestBlock = await childChainProvider.getBlock('latest'); + if (latestBlock.number > 0) break; + console.log(` Block: ${latestBlock.number}, waiting...`); + await new Promise((r) => setTimeout(r, 5_000)); + } + + if (!latestBlock || latestBlock.number === 0) { + console.error('\nTimeout: fullnode did not produce blocks.'); + return; + } + + console.log(`Fullnode synced! Latest block: ${latestBlock.number}`); + + const balance = await wallet.getBalance(); + console.log(`\nWallet: ${wallet.address}`); + console.log(`Balance on child chain: ${utils.formatEther(balance)} ETH`); + + if (balance.gt(0)) { + console.log('\nETH deposit verified on child chain! Force inclusion works end-to-end.'); + } else { + console.log('\nBalance is 0. Fullnode may need more time to process.'); + } + } finally { + console.log(`\nStopping container ${dockerContainerId.substring(0, 12)}...`); + try { + execSync(`docker stop ${dockerContainerId}`, { encoding: 'utf8' }); + } catch (e) { + /* container may have already stopped */ + } + try { + execSync(`docker rm ${dockerContainerId}`, { encoding: 'utf8' }); + } catch (e) { + /* container may have already been removed */ + } + } +} + +// ============================================================ +// Main +// ============================================================ + +async function main() { + console.log('=== Force Inclusion End-to-End Test ==='); + console.log( + `Mode: ${ + WITH_NODE ? 'full (with fullnode verification)' : 'deploy + deposit + forceInclude' + }\n`, + ); + + // Step 1: Deploy + const { chainId, parentChainId, coreContracts, nodeConfigPath } = await deployRollup(); + + // Step 2: Deposit + const childChainNetwork = await depositEth(chainId, coreContracts, parentChainId); + + // Step 3: Force Include + await forceInclude(childChainNetwork); + + // Step 4: (Optional) Verify on fullnode + if (WITH_NODE) { + await verifyOnFullnode(nodeConfigPath); + } + + console.log('\n=== Done ==='); +} + +main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + }); From cabf744f6a92d2e4d91249281f6a242df268077b Mon Sep 17 00:00:00 2001 From: Jason-Wanxt Date: Wed, 25 Mar 2026 05:12:08 +0800 Subject: [PATCH 2/3] remove unused fiels --- .../force-inclusion/.claude/settings.local.json | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 packages/force-inclusion/.claude/settings.local.json diff --git a/packages/force-inclusion/.claude/settings.local.json b/packages/force-inclusion/.claude/settings.local.json deleted file mode 100644 index d2618c7..0000000 --- a/packages/force-inclusion/.claude/settings.local.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(node:*)", - "Bash(find /Users/jasonwan1/Work/code/arbitrum-tutorials/packages/force-inclusion/node_modules/@arbitrum/chain-sdk -name SequencerInbox*)", - "Bash(docker ps:*)", - "Bash(git:*)", - "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\(''''scripts'''',{}\\), indent=2\\)\\)\")", - "Bash(yarn build:*)", - "Bash(docker image:*)", - "Bash(docker run:*)" - ] - } -} From 8bf56e9aa006c78a6bfba87eff2f12acb4b0283f Mon Sep 17 00:00:00 2001 From: Jason-Wanxt Date: Wed, 25 Mar 2026 05:13:27 +0800 Subject: [PATCH 3/3] remove unused files --- packages/force-inclusion/fullnode-config.json | 54 ------------------- 1 file changed, 54 deletions(-) delete mode 100644 packages/force-inclusion/fullnode-config.json diff --git a/packages/force-inclusion/fullnode-config.json b/packages/force-inclusion/fullnode-config.json deleted file mode 100644 index 527ce66..0000000 --- a/packages/force-inclusion/fullnode-config.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "chain": { - "info-json": "[{\"chain-id\":835090776,\"parent-chain-id\":421614,\"parent-chain-is-arbitrum\":true,\"chain-name\":\"ForceInclusionTestChain\",\"chain-config\":{\"chainId\":835090776,\"homesteadBlock\":0,\"daoForkBlock\":null,\"daoForkSupport\":true,\"eip150Block\":0,\"eip150Hash\":\"0x0000000000000000000000000000000000000000000000000000000000000000\",\"eip155Block\":0,\"eip158Block\":0,\"byzantiumBlock\":0,\"constantinopleBlock\":0,\"petersburgBlock\":0,\"istanbulBlock\":0,\"muirGlacierBlock\":0,\"berlinBlock\":0,\"londonBlock\":0,\"clique\":{\"period\":0,\"epoch\":0},\"arbitrum\":{\"EnableArbOS\":true,\"AllowDebugPrecompiles\":false,\"DataAvailabilityCommittee\":false,\"InitialArbOSVersion\":51,\"InitialChainOwner\":\"0x860C58951A77ac2F2Cb452cC9F36B1d1FdF7c521\",\"GenesisBlockNum\":0,\"MaxCodeSize\":24576,\"MaxInitCodeSize\":49152}},\"rollup\":{\"bridge\":\"0x301C08f30060e365ddA1bA462E47Cd8F164f9f3F\",\"inbox\":\"0xDaebf491243df4BfB42f8eD5faA665F25C6dB3E0\",\"sequencer-inbox\":\"0x82b4c8c9005Bc43F90d18853E1a3C6C42322CFC7\",\"rollup\":\"0xEdB4d8ab6d51B0C623D049732Cc3a3Dbe22e0541\",\"validator-wallet-creator\":\"0x2c37dCBCE3fbe32c9Ba62892F1E41DbB023BB62b\",\"stake-token\":\"0x0000000000000000000000000000000000000000\",\"deployed-at\":253218540}}]", - "name": "ForceInclusionTestChain" - }, - "parent-chain": { - "connection": { - "url": "https://arb-sepolia.g.alchemy.com/v2/A9Awdfc74bkIT_CzQkOcyM036BSQxyk4" - } - }, - "http": { - "addr": "0.0.0.0", - "port": 8449, - "vhosts": [ - "*" - ], - "corsdomain": [ - "*" - ], - "api": [ - "eth", - "net", - "web3", - "arb", - "debug" - ] - }, - "node": { - "sequencer": false, - "delayed-sequencer": { - "enable": false - }, - "batch-poster": { - "enable": false - }, - "staker": { - "enable": false - }, - "dangerous": { - "no-sequencer-coordinator": true, - "disable-blob-reader": false - }, - "ensure-rollup-deployment": false - }, - "execution": { - "forwarding-target": "null", - "sequencer": { - "enable": false - }, - "caching": { - "archive": true - } - } -} \ No newline at end of file