diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..cea57396 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Dependencies +node_modules/ + +# Environment +.env +.env.local +.env.*.local + +# Logs +*.log + +# Local overrides +docker-compose.override.yaml +docker-compose.override.yml + +# OS files +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ diff --git a/docker-compose.yaml b/docker-compose.yaml index 896d7e38..0dbe60f3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,7 +4,7 @@ services: - postgres - sequencer image: blockscout-testnode - restart: always + restart: unless-stopped container_name: 'blockscout' links: - postgres:database @@ -30,8 +30,13 @@ services: postgres: image: postgres:13.6 - restart: always + restart: unless-stopped container_name: 'postgres' + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 3s + retries: 5 environment: POSTGRES_PASSWORD: '' POSTGRES_USER: 'postgres' @@ -43,11 +48,24 @@ services: redis: image: redis:6.2.6 + restart: on-failure:3 ports: - "127.0.0.1:6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 3s + retries: 5 geth: image: ethereum/client-go:v1.16.4 + restart: on-failure:3 + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8545 --post-data='{\"jsonrpc\":\"2.0\",\"method\":\"eth_syncing\",\"params\":[],\"id\":1}' --header='Content-Type: application/json' || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s ports: - "127.0.0.1:8545:8545" - "127.0.0.1:8551:8551" @@ -157,7 +175,14 @@ services: sequencer: pid: host # allow debugging image: nitro-node-dev-testnode + restart: on-failure:3 entrypoint: /usr/local/bin/nitro + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://localhost:8547/health || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s ports: - "127.0.0.1:8547:8547" - "127.0.0.1:8548:8548" @@ -177,7 +202,8 @@ services: - --graphql.vhosts=* - --graphql.corsdomain=* depends_on: - - geth + geth: + condition: service_healthy sequencer_b: pid: host # allow debugging @@ -236,6 +262,7 @@ services: staker-unsafe: pid: host # allow debugging image: nitro-node-dev-testnode + restart: on-failure:3 entrypoint: /usr/local/bin/nitro ports: - "127.0.0.1:8047:8547" @@ -246,13 +273,17 @@ services: - "config:/config" command: --conf.file /config/unsafe_staker_config.json depends_on: - - sequencer - - redis - - validation_node + sequencer: + condition: service_healthy + redis: + condition: service_healthy + validation_node: + condition: service_started poster: pid: host # allow debugging image: nitro-node-dev-testnode + restart: on-failure:3 entrypoint: /usr/local/bin/nitro ports: - "127.0.0.1:8147:8547" @@ -263,8 +294,10 @@ services: - "config:/config" command: --conf.file /config/poster_config.json depends_on: - - geth - - redis + geth: + condition: service_healthy + redis: + condition: service_healthy poster_b: pid: host # allow debugging @@ -301,6 +334,7 @@ services: validator: pid: host # allow debugging image: nitro-node-dev-testnode + restart: on-failure:3 entrypoint: /usr/local/bin/nitro ports: - "127.0.0.1:8247:8547" @@ -311,18 +345,21 @@ services: - "config:/config" command: --conf.file /config/validator_config.json --http.port 8547 --http.api net,web3,arb,debug --ws.port 8548 depends_on: - - sequencer - - validation_node + sequencer: + condition: service_healthy + validation_node: + condition: service_started l3node: pid: host # allow debugging image: nitro-node-dev-testnode + restart: on-failure:3 entrypoint: /usr/local/bin/nitro ports: - "127.0.0.1:3347:3347" - "127.0.0.1:3348:3348" volumes: - - "validator-data:/home/user/.arbitrum/local/nitro" + - "l3node-data:/home/user/.arbitrum/local/nitro" - "l1keystore:/home/user/l1keystore" - "config:/config" command: --conf.file /config/l3node_config.json --http.port 3347 --http.api net,web3,arb,debug,eth --ws.port 3348 @@ -521,6 +558,7 @@ volumes: seqdata_d: unsafestaker-data: validator-data: + l3node-data: poster-data: poster-data-b: poster-data-c: diff --git a/scripts/config.ts b/scripts/config.ts index 114d62c5..16079840 100644 --- a/scripts/config.ts +++ b/scripts/config.ts @@ -7,12 +7,19 @@ import { S3Client, PutObjectCommand, CreateBucketCommand, HeadBucketCommand } fr const path = require("path"); +function warnIfPartialS3Credentials() { + if ((process.env.AWS_ACCESS_KEY_ID && !process.env.AWS_SECRET_ACCESS_KEY) || + (!process.env.AWS_ACCESS_KEY_ID && process.env.AWS_SECRET_ACCESS_KEY)) { + console.warn("Warning: Only one of AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY is set; both must be provided for custom S3 credentials (missing value will default to 'minioadmin')"); + } +} + const S3_CONFIG = { - endpoint: "http://minio:9000", - region: "us-east-1", + endpoint: process.env.S3_ENDPOINT || "http://minio:9000", + region: process.env.AWS_REGION || "us-east-1", credentials: { - accessKeyId: "minioadmin", - secretAccessKey: "minioadmin", + accessKeyId: process.env.AWS_ACCESS_KEY_ID || "minioadmin", + secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY || "minioadmin", }, forcePathStyle: true, }; @@ -189,13 +196,21 @@ function writeGethGenesisConfig(argv: any) { type ChainInfo = { [key: string]: any; -}; +}[]; -// Define a function to return ChainInfo function getChainInfo(): ChainInfo { const filePath = path.join(consts.configpath, "l2_chain_info.json"); - const fileContents = fs.readFileSync(filePath).toString(); - const chainInfo: ChainInfo = JSON.parse(fileContents); + let chainInfo: ChainInfo; + try { + const fileContents = fs.readFileSync(filePath).toString(); + chainInfo = JSON.parse(fileContents); + } catch (e: any) { + const action = (e instanceof SyntaxError) ? 'parse' : 'read'; + throw new Error(`Failed to ${action} chain info from ${filePath}: ${e.message}`); + } + if (!chainInfo || !Array.isArray(chainInfo) || chainInfo.length === 0) { + throw new Error(`Invalid chain info: expected non-empty array in ${filePath}`); + } return chainInfo; } @@ -406,7 +421,7 @@ function writeConfigs(argv: any) { sequencerConfig.node["delayed-sequencer"].enable = true if (argv.timeboost) { sequencerConfig.execution.sequencer.timeboost = { - "enable": false, // Create it false initially, turn it on with sed in test-node.bash after contract setup. + "enable": false, // Created false initially; enabled via jq in test-node.bash after auction contract deployment. "redis-url": argv.redisUrl }; } @@ -449,7 +464,7 @@ function writeConfigs(argv: any) { l3Config.node["batch-poster"]["redis-url"] = "" fs.writeFileSync(path.join(consts.configpath, "l3node_config.json"), JSON.stringify(l3Config)) - let validationNodeConfig = JSON.parse(JSON.stringify({ + const validationNodeConfig = { "persistent": { "chain": "local" }, @@ -467,7 +482,7 @@ function writeConfigs(argv: any) { "jwtsecret": valJwtSecret, "addr": "0.0.0.0", }, - })) + } fs.writeFileSync(path.join(consts.configpath, "validation_node_config.json"), JSON.stringify(validationNodeConfig)) } @@ -882,6 +897,7 @@ export const initTxFilteringMinioCommand = { fs.writeFileSync(path.join(consts.configpath, "initial_address_hashes.json"), JSON.stringify(initialAddressList, null, 2)); fs.writeFileSync(path.join(consts.configpath, "tx_filtering_salt.hex"), salt); + warnIfPartialS3Credentials(); const s3Client = new S3Client(S3_CONFIG); try { diff --git a/scripts/ethcommands.ts b/scripts/ethcommands.ts index 488555c6..4964a349 100644 --- a/scripts/ethcommands.ts +++ b/scripts/ethcommands.ts @@ -38,80 +38,99 @@ async function sendTransaction(argv: any, threadId: number) { async function bridgeFunds(argv: any, parentChainUrl: string, chainUrl: string, inboxAddr: string) { argv.provider = new ethers.providers.WebSocketProvider(parentChainUrl); + try { + argv.to = "address_" + inboxAddr; + argv.data = + "0x0f4d14e9000000000000000000000000000000000000000000000000000082f79cd90000"; - argv.to = "address_" + inboxAddr; - argv.data = - "0x0f4d14e9000000000000000000000000000000000000000000000000000082f79cd90000"; - - await runStress(argv, sendTransaction); - - argv.provider.destroy(); - if (argv.wait) { - const l2provider = new ethers.providers.WebSocketProvider(chainUrl); - const account = namedAccount(argv.from, argv.threadId).connect(l2provider) - const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); - while (true) { - const balance = await account.getBalance() - if (balance.gte(ethers.utils.parseEther(argv.ethamount))) { - return + await runStress(argv, sendTransaction); + + if (argv.wait) { + const l2provider = new ethers.providers.WebSocketProvider(chainUrl); + try { + const account = namedAccount(argv.from, argv.threadId).connect(l2provider) + const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + const maxWaitMs = 300000; // 5 minutes + const startTime = Date.now(); + while (true) { + if (Date.now() - startTime > maxWaitMs) { + throw new Error(`Timed out waiting for balance >= ${argv.ethamount} ETH after ${maxWaitMs / 1000}s`); + } + const balance = await account.getBalance() + if (balance.gte(ethers.utils.parseEther(argv.ethamount))) { + return + } + await sleep(100) + } + } finally { + l2provider.destroy(); } - await sleep(100) } + } finally { + argv.provider.destroy(); } } async function bridgeNativeToken(argv: any, parentChainUrl: string, chainUrl: string, inboxAddr: string, token: string) { argv.provider = new ethers.providers.WebSocketProvider(parentChainUrl); + const childProvider = new ethers.providers.WebSocketProvider(chainUrl); + try { + argv.to = "address_" + inboxAddr; - argv.to = "address_" + inboxAddr; + // snapshot balance before deposit + const bridger = namedAccount(argv.from, argv.threadId).connect(childProvider) + const bridgerBalanceBefore = await bridger.getBalance() - // snapshot balance before deposit - const childProvider = new ethers.providers.WebSocketProvider(chainUrl); - const bridger = namedAccount(argv.from, argv.threadId).connect(childProvider) - const bridgerBalanceBefore = await bridger.getBalance() - - // get token contract - const bridgerParentChain = namedAccount(argv.from, argv.threadId).connect(argv.provider) - const nativeTokenContract = new ethers.Contract(token, ERC20.abi, bridgerParentChain) - - // scale deposit amount - const decimals = await nativeTokenContract.decimals() - const depositAmount = BigNumber.from(argv.amount).mul(BigNumber.from('10').pow(decimals)) - - /// approve inbox to use fee token - await nativeTokenContract.approve(inboxAddr, depositAmount) - - /// deposit fee token - const iface = new ethers.utils.Interface(["function depositERC20(uint256 amount)"]) - argv.data = iface.encodeFunctionData("depositERC20", [depositAmount]); - - await runStress(argv, sendTransaction); - - argv.provider.destroy(); - if (argv.wait) { - const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); - - // calculate amount being minted on child chain - let expectedMintedAmount = depositAmount - if(decimals < 18) { - // inflate up to 18 decimals - expectedMintedAmount = depositAmount.mul(BigNumber.from('10').pow(18 - decimals)) - } else if(decimals > 18) { - // deflate down to 18 decimals, rounding up - const quotient = BigNumber.from('10').pow(decimals - 18) - expectedMintedAmount = depositAmount.div(quotient) - if(expectedMintedAmount.mul(quotient).lt(depositAmount)) { - expectedMintedAmount = expectedMintedAmount.add(1) + // get token contract + const bridgerParentChain = namedAccount(argv.from, argv.threadId).connect(argv.provider) + const nativeTokenContract = new ethers.Contract(token, ERC20.abi, bridgerParentChain) + + // scale deposit amount + const decimals = await nativeTokenContract.decimals() + const depositAmount = BigNumber.from(argv.amount).mul(BigNumber.from('10').pow(decimals)) + + /// approve inbox to use fee token + await nativeTokenContract.approve(inboxAddr, depositAmount) + + /// deposit fee token + const iface = new ethers.utils.Interface(["function depositERC20(uint256 amount)"]) + argv.data = iface.encodeFunctionData("depositERC20", [depositAmount]); + + await runStress(argv, sendTransaction); + + if (argv.wait) { + const sleep = (ms: number) => new Promise(r => setTimeout(r, ms)); + + // calculate amount being minted on child chain + let expectedMintedAmount = depositAmount + if(decimals < 18) { + // inflate up to 18 decimals + expectedMintedAmount = depositAmount.mul(BigNumber.from('10').pow(18 - decimals)) + } else if(decimals > 18) { + // deflate down to 18 decimals, rounding up + const quotient = BigNumber.from('10').pow(decimals - 18) + expectedMintedAmount = depositAmount.div(quotient) + if(expectedMintedAmount.mul(quotient).lt(depositAmount)) { + expectedMintedAmount = expectedMintedAmount.add(1) + } } - } - while (true) { - const bridgerBalanceAfter = await bridger.getBalance() - if (bridgerBalanceAfter.sub(bridgerBalanceBefore).eq(expectedMintedAmount)) { - return + const maxWaitMs = 300000; // 5 minutes + const startTime = Date.now(); + while (true) { + if (Date.now() - startTime > maxWaitMs) { + throw new Error(`Timed out waiting for native token bridge balance after ${maxWaitMs / 1000}s`); + } + const bridgerBalanceAfter = await bridger.getBalance() + if (bridgerBalanceAfter.sub(bridgerBalanceBefore).eq(expectedMintedAmount)) { + return + } + await sleep(100) } - await sleep(100) } + } finally { + argv.provider.destroy(); + childProvider.destroy(); } } @@ -190,12 +209,11 @@ async function deployWETHContract(deployerWallet: Wallet): Promise { async function createStylusDeployer(deployerWallet: Wallet): Promise { // this factory should be deployed by the rollupcreator when deploy helper is used const create2factory = '0x4e59b44847b379578588920ca78fbf26c0b4956c' - if (await deployerWallet.provider.getCode(create2factory) === '0x') { - // wait for 30 seconds, check again before throwing an error - await new Promise(resolve => setTimeout(resolve, 30000)); - if (await deployerWallet.provider.getCode(create2factory) === '0x') { - throw new Error('Create2 factory not yet deployed') - } + for (let attempt = 0; attempt < 6; attempt++) { + if (await deployerWallet.provider.getCode(create2factory) !== '0x') break; + if (attempt === 5) throw new Error('Create2 factory not deployed after 50s'); + console.log(`Waiting for Create2 factory deployment (attempt ${attempt + 1}/6)...`); + await new Promise(resolve => setTimeout(resolve, 10000)); } const salt = ethers.constants.HashZero @@ -340,28 +358,39 @@ export const transferL3ChainOwnershipCommand = { // get L3 upgrade executor address from token bridge creator const l2provider = new ethers.providers.WebSocketProvider(argv.l2url); - const tokenBridgeCreator = new ethers.Contract(argv.creator, L1AtomicTokenBridgeCreator.abi, l2provider); - const [,,,,,,,l3UpgradeExecutorAddress,] = await tokenBridgeCreator.inboxToL2Deployment(inboxAddr); + let l3UpgradeExecutorAddress: string; + try { + const tokenBridgeCreator = new ethers.Contract(argv.creator, L1AtomicTokenBridgeCreator.abi, l2provider); + const deployment = await tokenBridgeCreator.inboxToL2Deployment(inboxAddr); + l3UpgradeExecutorAddress = deployment[7]; + if (!l3UpgradeExecutorAddress || l3UpgradeExecutorAddress === ethers.constants.AddressZero) { + throw new Error(`Failed to get L3 UpgradeExecutor address from token bridge creator (inbox: ${inboxAddr})`); + } + } finally { + l2provider.destroy(); + } // set TX params argv.provider = new ethers.providers.WebSocketProvider(argv.l3url); - argv.to = "address_" + ARB_OWNER; - argv.from = "l3owner"; - argv.ethamount = "0"; - - // add L3 UpgradeExecutor to chain owners - const arbOwnerIface = new ethers.utils.Interface([ - "function addChainOwner(address newOwner) external", - "function removeChainOwner(address ownerToRemove) external" - ]) - argv.data = arbOwnerIface.encodeFunctionData("addChainOwner", [l3UpgradeExecutorAddress]); - await runStress(argv, sendTransaction); - - // remove L3 owner from chain owners - argv.data = arbOwnerIface.encodeFunctionData("removeChainOwner", [namedAccount("l3owner").address]); - await runStress(argv, sendTransaction); - - argv.provider.destroy(); + try { + argv.to = "address_" + ARB_OWNER; + argv.from = "l3owner"; + argv.ethamount = "0"; + + // add L3 UpgradeExecutor to chain owners + const arbOwnerIface = new ethers.utils.Interface([ + "function addChainOwner(address newOwner) external", + "function removeChainOwner(address ownerToRemove) external" + ]) + argv.data = arbOwnerIface.encodeFunctionData("addChainOwner", [l3UpgradeExecutorAddress]); + await runStress(argv, sendTransaction); + + // remove L3 owner from chain owners + argv.data = arbOwnerIface.encodeFunctionData("removeChainOwner", [namedAccount("l3owner").address]); + await runStress(argv, sendTransaction); + } finally { + argv.provider.destroy(); + } } }; @@ -395,60 +424,69 @@ export const createERC20Command = { // deploy token on l1 const l1provider = new ethers.providers.WebSocketProvider(argv.l1url); - const deployerWallet = namedAccount(argv.deployer).connect(l1provider); + try { + const deployerWallet = namedAccount(argv.deployer).connect(l1provider); - const tokenAddress = await deployERC20Contract(deployerWallet, argv.decimals); - const token = new ethers.Contract(tokenAddress, ERC20.abi, deployerWallet); - console.log("Contract deployed at L1 address:", token.address); - - if (!argv.bridgeable) return; - - // bridge to l2 - const l2provider = new ethers.providers.WebSocketProvider(argv.l2url); - const l1l2tokenbridge = JSON.parse( - fs - .readFileSync(path.join(consts.tokenbridgedatapath, "l1l2_network.json")) - .toString() - ); - - const l1GatewayRouter = new ethers.Contract(l1l2tokenbridge.l2Network.tokenBridge.parentGatewayRouter, L1GatewayRouter.abi, deployerWallet); - await (await token.functions.approve(l1l2tokenbridge.l2Network.tokenBridge.parentErc20Gateway, ethers.constants.MaxUint256)).wait(); - const supply = await token.totalSupply(); - // transfer 90% of supply to l2 - const transferAmount = supply.mul(9).div(10); - await (await l1GatewayRouter.functions.outboundTransfer( - token.address, deployerWallet.address, transferAmount, 100000000, 1000000000, "0x000000000000000000000000000000000000000000000000000fffffffffff0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", { - value: ethers.utils.parseEther("1"), + const tokenAddress = await deployERC20Contract(deployerWallet, argv.decimals); + const token = new ethers.Contract(tokenAddress, ERC20.abi, deployerWallet); + console.log("Contract deployed at L1 address:", token.address); + + if (!argv.bridgeable) { + return; } - )).wait(); - - const tokenL2Addr = (await l1GatewayRouter.functions.calculateL2TokenAddress(token.address))[0]; - // wait for l2 token to be deployed - for (let i = 0; i < 60; i++) { - if (await l2provider.getCode(tokenL2Addr) === "0x") { - await new Promise(f => setTimeout(f, 1000)); - } else { - break; + + // bridge to l2 + const l2provider = new ethers.providers.WebSocketProvider(argv.l2url); + try { + const l1l2tokenbridge = JSON.parse( + fs + .readFileSync(path.join(consts.tokenbridgedatapath, "l1l2_network.json")) + .toString() + ); + + const l1GatewayRouter = new ethers.Contract(l1l2tokenbridge.l2Network.tokenBridge.parentGatewayRouter, L1GatewayRouter.abi, deployerWallet); + await (await token.functions.approve(l1l2tokenbridge.l2Network.tokenBridge.parentErc20Gateway, ethers.constants.MaxUint256)).wait(); + const supply = await token.totalSupply(); + // transfer 90% of supply to l2 + const transferAmount = supply.mul(9).div(10); + await (await l1GatewayRouter.functions.outboundTransfer( + token.address, deployerWallet.address, transferAmount, 100000000, 1000000000, "0x000000000000000000000000000000000000000000000000000fffffffffff0000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000", { + value: ethers.utils.parseEther("1"), + } + )).wait(); + + const tokenL2Addr = (await l1GatewayRouter.functions.calculateL2TokenAddress(token.address))[0]; + // wait for l2 token to be deployed + for (let i = 0; i < 60; i++) { + if (await l2provider.getCode(tokenL2Addr) === "0x") { + await new Promise(f => setTimeout(f, 1000)); + } else { + break; + } + } + if (await l2provider.getCode(tokenL2Addr) === "0x") { + throw new Error("Failed to bridge token to L2"); + } + + console.log("Contract deployed at L2 address:", tokenL2Addr); + } finally { + l2provider.destroy(); } + } finally { + l1provider.destroy(); } - if (await l2provider.getCode(tokenL2Addr) === "0x") { - throw new Error("Failed to bridge token to L2"); - } - - console.log("Contract deployed at L2 address:", tokenL2Addr); - - l1provider.destroy(); - l2provider.destroy(); return; } // no l1-l2 token bridge, deploy token on l2 directly argv.provider = new ethers.providers.WebSocketProvider(argv.l2url); - const deployerWallet = namedAccount(argv.deployer).connect(argv.provider); - const tokenAddress = await deployERC20Contract(deployerWallet, argv.decimals); - console.log("Contract deployed at address:", tokenAddress); - - argv.provider.destroy(); + try { + const deployerWallet = namedAccount(argv.deployer).connect(argv.provider); + const tokenAddress = await deployERC20Contract(deployerWallet, argv.decimals); + console.log("Contract deployed at address:", tokenAddress); + } finally { + argv.provider.destroy(); + } }, }; @@ -465,14 +503,13 @@ export const createFeeTokenPricerCommand = { console.log("create-fee-token-pricer"); argv.provider = new ethers.providers.WebSocketProvider(argv.l2url); - const deployerWallet = new Wallet( - ethers.utils.sha256(ethers.utils.toUtf8Bytes(argv.deployer)), - argv.provider - ); - const feeTokenPricerAddress = await deployFeeTokenPricerContract(deployerWallet, BigNumber.from("15000000000000000000")); - console.log("Contract deployed at address:", feeTokenPricerAddress); - - argv.provider.destroy(); + try { + const deployerWallet = namedAccount(argv.deployer).connect(argv.provider); + const feeTokenPricerAddress = await deployFeeTokenPricerContract(deployerWallet, BigNumber.from("15000000000000000000")); + console.log("Contract deployed at address:", feeTokenPricerAddress); + } finally { + argv.provider.destroy(); + } }, }; @@ -494,41 +531,43 @@ export const deployExpressLaneAuctionContractCommand = { handler: async (argv: any) => { console.log("deploy ExpressLaneAuction contract"); argv.provider = new ethers.providers.WebSocketProvider(argv.l2url); - const l2OwnerWallet = namedAccount("l2owner").connect(argv.provider) - const contractFactory = new ContractFactory(ExpressLaneAuctionContract.abi, ExpressLaneAuctionContract.bytecode, l2OwnerWallet) - - const contract = await contractFactory.deploy(); - await contract.deployTransaction.wait(); - console.log("ExpressLaneAuction contract deployed at address:", contract.address); - - const auctioneerAddr = namedAddress(argv.auctioneer) - const initIface = new ethers.utils.Interface(["function initialize((address,address,address,(int64,uint64,uint64,uint64),uint256,address,address,address,address,address,address,address))"]) - const initData = initIface.encodeFunctionData("initialize", [[ - auctioneerAddr, //_auctioneer - argv.biddingToken, //_biddingToken - auctioneerAddr, //_beneficiary - [ - Math.round(Date.now() / 60000) * 60, // offsetTimestamp - most recent minute - 60, // roundDurationSeconds - 15, // auctionClosingSeconds - 15 // reserveSubmissionSeconds - ],// RoundTiminginfo - 1, // _minReservePrice - auctioneerAddr, //_auctioneerAdmin - auctioneerAddr, //_minReservePriceSetter, - auctioneerAddr, //_reservePriceSetter, - auctioneerAddr, //_reservePriceSetterAdmin, - auctioneerAddr, //_beneficiarySetter, - auctioneerAddr, //_roundTimingSetter, - auctioneerAddr //_masterAdmin - ]]); - - const proxyFactory = new ethers.ContractFactory(TransparentUpgradeableProxy.abi, TransparentUpgradeableProxy.bytecode, l2OwnerWallet) - const proxy = await proxyFactory.deploy(contract.address, namedAddress("l2owner"), initData) - await proxy.deployed() - console.log("Proxy(ExpressLaneAuction) contract deployed at address:", proxy.address); - - argv.provider.destroy(); + try { + const l2OwnerWallet = namedAccount("l2owner").connect(argv.provider) + const contractFactory = new ContractFactory(ExpressLaneAuctionContract.abi, ExpressLaneAuctionContract.bytecode, l2OwnerWallet) + + const contract = await contractFactory.deploy(); + await contract.deployTransaction.wait(); + console.log("ExpressLaneAuction contract deployed at address:", contract.address); + + const auctioneerAddr = namedAddress(argv.auctioneer) + const initIface = new ethers.utils.Interface(["function initialize((address,address,address,(int64,uint64,uint64,uint64),uint256,address,address,address,address,address,address,address))"]) + const initData = initIface.encodeFunctionData("initialize", [[ + auctioneerAddr, //_auctioneer + argv.biddingToken, //_biddingToken + auctioneerAddr, //_beneficiary + [ + Math.round(Date.now() / 60000) * 60, // offsetTimestamp - most recent minute + 60, // roundDurationSeconds + 15, // auctionClosingSeconds + 15 // reserveSubmissionSeconds + ],// RoundTiminginfo + 1, // _minReservePrice + auctioneerAddr, //_auctioneerAdmin + auctioneerAddr, //_minReservePriceSetter, + auctioneerAddr, //_reservePriceSetter, + auctioneerAddr, //_reservePriceSetterAdmin, + auctioneerAddr, //_beneficiarySetter, + auctioneerAddr, //_roundTimingSetter, + auctioneerAddr //_masterAdmin + ]]); + + const proxyFactory = new ethers.ContractFactory(TransparentUpgradeableProxy.abi, TransparentUpgradeableProxy.bytecode, l2OwnerWallet) + const proxy = await proxyFactory.deploy(contract.address, namedAddress("l2owner"), initData) + await proxy.deployed() + console.log("Proxy(ExpressLaneAuction) contract deployed at address:", proxy.address); + } finally { + argv.provider.destroy(); + } } }; @@ -547,8 +586,6 @@ async function setValidKeyset(argv: any, upgradeExecutorAddr: string, sequencerI argv.ethamount = "0" await sendTransaction(argv, 0); - - argv.provider.destroy(); } export const transferERC20Command = { @@ -584,12 +621,15 @@ export const transferERC20Command = { } else { argv.provider = new ethers.providers.WebSocketProvider(argv.l2url); } - const account = namedAccount(argv.from).connect(argv.provider); - const tokenContract = new ethers.Contract(argv.token, ERC20.abi, account); - const tokenDecimals = await tokenContract.decimals(); - const amountToTransfer = BigNumber.from(argv.amount).mul(BigNumber.from('10').pow(tokenDecimals)); - await(await tokenContract.transfer(namedAccount(argv.to).address, amountToTransfer)).wait(); - argv.provider.destroy(); + try { + const account = namedAccount(argv.from).connect(argv.provider); + const tokenContract = new ethers.Contract(argv.token, ERC20.abi, account); + const tokenDecimals = await tokenContract.decimals(); + const amountToTransfer = BigNumber.from(argv.amount).mul(BigNumber.from('10').pow(tokenDecimals)); + await(await tokenContract.transfer(namedAccount(argv.to).address, amountToTransfer)).wait(); + } finally { + argv.provider.destroy(); + } }, }; @@ -611,16 +651,20 @@ export const createWETHCommand = { console.log("create-weth"); const l1provider = new ethers.providers.WebSocketProvider(argv.l1url); - const deployerWallet = namedAccount(argv.deployer).connect(l1provider); + try { + const deployerWallet = namedAccount(argv.deployer).connect(l1provider); - const wethAddress = await deployWETHContract(deployerWallet); - const weth = new ethers.Contract(wethAddress, TestWETH9.abi, deployerWallet); - console.log("WETH deployed at L1 address:", weth.address); + const wethAddress = await deployWETHContract(deployerWallet); + const weth = new ethers.Contract(wethAddress, TestWETH9.abi, deployerWallet); + console.log("WETH deployed at L1 address:", weth.address); - if (argv.deposit > 0) { - const amount = ethers.utils.parseEther(argv.deposit.toString()); - const depositTx = await deployerWallet.sendTransaction({ to: wethAddress, value: amount, data:"0xd0e30db0" }); // deposit() - await depositTx.wait(); + if (argv.deposit > 0) { + const amount = ethers.utils.parseEther(argv.deposit.toString()); + const depositTx = await deployerWallet.sendTransaction({ to: wethAddress, value: amount, data:"0xd0e30db0" }); // deposit() + await depositTx.wait(); + } + } finally { + l1provider.destroy(); } }, }; @@ -630,22 +674,24 @@ export const createStylusDeployerCommand = { describe: "deploys the stylus deployer contract", builder: { deployer: { string: true, describe: "account (see general help)" }, - l3: { boolean: false, describe: "deploy on L3, otherwise deploy on L2" }, + l3: { boolean: true, default: false, describe: "deploy on L3, otherwise deploy on L2" }, }, handler: async (argv: any) => { console.log("create-stylus-deployer"); const provider = new ethers.providers.WebSocketProvider(argv.l3 ? argv.l3url : argv.l2url); - const deployerWallet = namedAccount(argv.deployer).connect(provider); - - const stylusDeployerAddress = await createStylusDeployer(deployerWallet); - if (argv.l3) { - console.log("Stylus deployer deployed at L3 address:", stylusDeployerAddress); - } else { - console.log("Stylus deployer deployed at L2 address:", stylusDeployerAddress); + try { + const deployerWallet = namedAccount(argv.deployer).connect(provider); + + const stylusDeployerAddress = await createStylusDeployer(deployerWallet); + if (argv.l3) { + console.log("Stylus deployer deployed at L3 address:", stylusDeployerAddress); + } else { + console.log("Stylus deployer deployed at L2 address:", stylusDeployerAddress); + } + } finally { + provider.destroy(); } - - provider.destroy(); } }; @@ -677,10 +723,11 @@ export const sendL1Command = { }, handler: async (argv: any) => { argv.provider = new ethers.providers.WebSocketProvider(argv.l1url); - - await runStress(argv, sendTransaction); - - argv.provider.destroy(); + try { + await runStress(argv, sendTransaction); + } finally { + argv.provider.destroy(); + } }, }; @@ -712,10 +759,11 @@ export const sendL2Command = { }, handler: async (argv: any) => { argv.provider = new ethers.providers.WebSocketProvider(argv.l2url); - - await runStress(argv, sendTransaction); - - argv.provider.destroy(); + try { + await runStress(argv, sendTransaction); + } finally { + argv.provider.destroy(); + } }, }; @@ -747,10 +795,11 @@ export const sendL3Command = { }, handler: async (argv: any) => { argv.provider = new ethers.providers.WebSocketProvider(argv.l3url); - - await runStress(argv, sendTransaction); - - argv.provider.destroy(); + try { + await runStress(argv, sendTransaction); + } finally { + argv.provider.destroy(); + } }, }; @@ -764,8 +813,8 @@ export const sendRPCCommand = { }, handler: async (argv: any) => { const rpcProvider = new ethers.providers.JsonRpcProvider(argv.url) - - await rpcProvider.send(argv.method, argv.params) + const result = await rpcProvider.send(argv.method, argv.params) + console.log(JSON.stringify(result, null, 2)) } } @@ -774,19 +823,23 @@ export const setValidKeysetCommand = { describe: "sets the anytrust keyset", handler: async (argv: any) => { argv.provider = new ethers.providers.WebSocketProvider(argv.l1url); - const deploydata = JSON.parse( - fs - .readFileSync(path.join(consts.configpath, "deployment.json")) + try { + const deploydata = JSON.parse( + fs + .readFileSync(path.join(consts.configpath, "deployment.json")) + .toString() + ); + const sequencerInboxAddr = ethers.utils.hexlify(deploydata["sequencer-inbox"]); + const upgradeExecutorAddr = ethers.utils.hexlify(deploydata["upgrade-executor"]); + + const keyset = fs + .readFileSync(path.join(consts.configpath, "l2_das_keyset.hex")) .toString() - ); - const sequencerInboxAddr = ethers.utils.hexlify(deploydata["sequencer-inbox"]); - const upgradeExecutorAddr = ethers.utils.hexlify(deploydata["upgrade-executor"]); - const keyset = fs - .readFileSync(path.join(consts.configpath, "l2_das_keyset.hex")) - .toString() - - await setValidKeyset(argv, upgradeExecutorAddr, sequencerInboxAddr, keyset) + await setValidKeyset(argv, upgradeExecutorAddr, sequencerInboxAddr, keyset) + } finally { + argv.provider.destroy(); + } } }; @@ -798,11 +851,15 @@ export const waitForSyncCommand = { }, handler: async (argv: any) => { const rpcProvider = new ethers.providers.JsonRpcProvider(argv.url) + const maxWaitMs = 300000; // 5 minutes + const startTime = Date.now(); let syncStatus; do { + if (Date.now() - startTime > maxWaitMs) { + throw new Error(`Timed out waiting for sync at ${argv.url} after ${maxWaitMs / 1000}s`); + } syncStatus = await rpcProvider.send("eth_syncing", []) if (syncStatus !== false) { - // Wait for a short interval before checking again await new Promise(resolve => setTimeout(resolve, 5000)) } } while (syncStatus !== false) @@ -814,17 +871,19 @@ export const grantFiltererRoleCommand = { describe: "grants TransactionFilterer role to the filterer account", handler: async (argv: any) => { argv.provider = new ethers.providers.WebSocketProvider(argv.l2url); - - const arbOwnerIface = new ethers.utils.Interface([ - "function addTransactionFilterer(address filterer) external" - ]); - - argv.data = arbOwnerIface.encodeFunctionData("addTransactionFilterer", [namedAddress("filterer")]); - argv.from = "l2owner"; - argv.to = "address_" + ARB_OWNER; - argv.ethamount = "0"; - - await runStress(argv, sendTransaction); - argv.provider.destroy(); + try { + const arbOwnerIface = new ethers.utils.Interface([ + "function addTransactionFilterer(address filterer) external" + ]); + + argv.data = arbOwnerIface.encodeFunctionData("addTransactionFilterer", [namedAddress("filterer")]); + argv.from = "l2owner"; + argv.to = "address_" + ARB_OWNER; + argv.ethamount = "0"; + + await runStress(argv, sendTransaction); + } finally { + argv.provider.destroy(); + } } }; diff --git a/scripts/index.ts b/scripts/index.ts index 3516e071..a81e8784 100644 --- a/scripts/index.ts +++ b/scripts/index.ts @@ -48,11 +48,11 @@ import { async function main() { await Yargs(hideBin(process.argv)) .options({ - redisUrl: { string: true, default: "redis://redis:6379" }, - l1url: { string: true, default: "ws://geth:8546" }, - l2url: { string: true, default: "ws://sequencer:8548" }, - l3url: { string: true, default: "ws://l3node:3348" }, - validationNodeUrl: { string: true, default: "ws://validation_node:8549" }, + redisUrl: { string: true, default: process.env.REDIS_URL || "redis://redis:6379" }, + l1url: { string: true, default: process.env.L1_URL || "ws://geth:8546" }, + l2url: { string: true, default: process.env.L2_URL || "ws://sequencer:8548" }, + l3url: { string: true, default: process.env.L3_URL || "ws://l3node:3348" }, + validationNodeUrl: { string: true, default: process.env.VALIDATION_NODE_URL || "ws://validation_node:8549" }, l2owner: { string: true, default: "0x3f1Eae7D46d88F08fc2F8ed27FCb2AB183EB2d0E" }, committeeMember: { string: true, default: "not_set" }, }) diff --git a/scripts/redis.ts b/scripts/redis.ts index 1282368a..bc30241a 100644 --- a/scripts/redis.ts +++ b/scripts/redis.ts @@ -16,7 +16,11 @@ async function getAndPrint( async function readRedis(redisUrl: string, key: string) { const redis = createClient({ url: redisUrl }); await redis.connect(); - await getAndPrint(redis, key); + try { + await getAndPrint(redis, key); + } finally { + await redis.quit(); + } } export const redisReadCommand = { @@ -54,10 +58,12 @@ async function writeRedisPriorities(redisUrl: string, priorities: number) { priostring = priostring + this_prio; } await redis.connect(); - - await redis.set("coordinator.priorities", priostring); - - await getAndPrint(redis, "coordinator.priorities"); + try { + await redis.set("coordinator.priorities", priostring); + await getAndPrint(redis, "coordinator.priorities"); + } finally { + await redis.quit(); + } } export const redisInitCommand = { diff --git a/scripts/stress.ts b/scripts/stress.ts index df5807cf..b9deb656 100644 --- a/scripts/stress.ts +++ b/scripts/stress.ts @@ -24,8 +24,4 @@ export async function runStress(argv: any, commandHandler: (argv: any, thread: n } } await Promise.all(promiseArray) - .catch(error => { - console.error(error) - process.exit(1) - }) } diff --git a/test-node.bash b/test-node.bash index 3c80c67a..7cfa3df4 100755 --- a/test-node.bash +++ b/test-node.bash @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -eu +set -euo pipefail : ${NITRO_NODE_VERSION:=offchainlabs/nitro-node:v3.9.6-91bf578} BLOCKSCOUT_VERSION=offchainlabs/blockscout:v1.1.0-0e716c8 @@ -11,7 +11,7 @@ BLOCKSCOUT_VERSION=offchainlabs/blockscout:v1.1.0-0e716c8 DEFAULT_NITRO_CONTRACTS_VERSION="v3.1.0" DEFAULT_TOKEN_BRIDGE_VERSION="v1.2.5" -# Set default versions if not overriden by provided env vars +# Set default versions if not overridden by provided env vars : ${NITRO_CONTRACTS_BRANCH:=$DEFAULT_NITRO_CONTRACTS_VERSION} : ${TOKEN_BRIDGE_BRANCH:=$DEFAULT_TOKEN_BRIDGE_VERSION} export NITRO_CONTRACTS_BRANCH @@ -20,20 +20,88 @@ export TOKEN_BRIDGE_BRANCH echo "Using NITRO_CONTRACTS_BRANCH: $NITRO_CONTRACTS_BRANCH" echo "Using TOKEN_BRIDGE_BRANCH: $TOKEN_BRIDGE_BRANCH" -mydir=`dirname $0` +mydir=$(dirname "$0") cd "$mydir" run_script() { docker compose run --rm scripts "$@" } +# Capture the last line of output from a command, failing if empty. +# Usage: var=$(capture_output run_script print-address --account foo) +capture_output() { + local full_output + full_output=$("$@") || { + echo "Error: command failed: $*" >&2 + if [ -n "$full_output" ]; then + echo "Stdout was:" >&2 + echo "$full_output" >&2 + fi + exit 1 + } + local output + output=$(echo "$full_output" | tail -n 1 | tr -d '\r\n') + if [ -z "$output" ]; then + echo "Error: empty output from command: $*" >&2 + echo "Full output was:" >&2 + echo "$full_output" >&2 + exit 1 + fi + echo "$output" +} + +# Capture the last whitespace-delimited field from the last line of command output. +# Usage: var=$(capture_last_field run_script create-erc20 --deployer foo) +capture_last_field() { + local output + output=$(capture_output "$@") + echo "$output" | awk '{ print $NF }' +} + +# Wait for chain to produce blocks, confirming readiness. +# Usage: wait_for_chain_progress +# e.g.: wait_for_chain_progress "send-l1" "L1" +wait_for_chain_progress() { + local cmd=$1 + local chain=$2 + local max_attempts=60 + local last_err="" + echo "== Waiting for $chain block production..." + for _ in $(seq 1 $max_attempts); do + # Capture stderr for diagnostics; discard stdout (2>&1 must precede >/dev/null) + if last_err=$(run_script "$cmd" --ethamount 0.0001 --to address_0x0000000000000000000000000000000000000000 --wait 2>&1 >/dev/null); then + return 0 + fi + sleep 1 + done + echo "Error: $chain did not produce blocks after $max_attempts attempts" >&2 + if [ -n "$last_err" ]; then + echo "Last error: $last_err" >&2 + fi + exit 1 +} + +# Track background PIDs for cleanup on exit +BACKGROUND_PIDS=() +cleanup_background() { + if [ ${#BACKGROUND_PIDS[@]} -gt 0 ]; then + for pid in "${BACKGROUND_PIDS[@]}"; do + kill "$pid" 2>/dev/null || true + done + for pid in "${BACKGROUND_PIDS[@]}"; do + wait "$pid" 2>/dev/null || true + done + fi +} +trap cleanup_background EXIT + if [[ $# -gt 0 ]] && [[ $1 == "script" ]]; then shift run_script "$@" exit $? fi -num_volumes=`docker volume ls --filter label=com.docker.compose.project=nitro-testnode -q | wc -l` +num_volumes=$(docker volume ls --filter label=com.docker.compose.project=nitro-testnode -q | wc -l) if [[ $num_volumes -eq 0 ]]; then force_init=true @@ -127,6 +195,7 @@ while [[ $# -gt 0 ]]; do ;; --dev-contracts) dev_contracts=true + shift ;; --ci) ci=true @@ -201,17 +270,13 @@ while [[ $# -gt 0 ]]; do shift ;; --nowait) - if ! $detach; then - echo "Error: --nowait requires --detach to be provided." - exit 1 - fi nowait=true shift ;; --batchposters) simple=false batchposters=$2 - if ! [[ $batchposters =~ [0-3] ]] ; then + if ! [[ "$batchposters" =~ ^[0-9]+$ ]] || (( batchposters < 0 || batchposters > 3 )); then echo "batchposters must be between 0 and 3 value:$batchposters." exit 1 fi @@ -228,26 +293,14 @@ while [[ $# -gt 0 ]]; do shift ;; --l3-fee-token) - if ! $l3node; then - echo "Error: --l3-fee-token requires --l3node to be provided." - exit 1 - fi l3_custom_fee_token=true shift ;; --l3-fee-token-pricer) - if ! $l3_custom_fee_token; then - echo "Error: --l3-fee-token-pricer requires --l3-fee-token to be provided." - exit 1 - fi l3_custom_fee_token_pricer=true shift ;; --l3-fee-token-decimals) - if ! $l3_custom_fee_token; then - echo "Error: --l3-fee-token-decimals requires --l3-fee-token to be provided." - exit 1 - fi l3_custom_fee_token_decimals=$2 if [[ $l3_custom_fee_token_decimals -lt 0 || $l3_custom_fee_token_decimals -gt 36 ]]; then echo "l3-fee-token-decimals must be in range [0,36], value: $l3_custom_fee_token_decimals." @@ -257,10 +310,6 @@ while [[ $# -gt 0 ]]; do shift ;; --l3-token-bridge) - if ! $l3node; then - echo "Error: --l3-token-bridge requires --l3node to be provided." - exit 1 - fi l3_token_bridge=true shift ;; @@ -283,7 +332,7 @@ while [[ $# -gt 0 ]]; do --redundantsequencers) simple=false redundantsequencers=$2 - if ! [[ $redundantsequencers =~ [0-3] ]] ; then + if ! [[ "$redundantsequencers" =~ ^[0-9]+$ ]] || (( redundantsequencers < 0 || redundantsequencers > 3 )); then echo "redundantsequencers must be between 0 and 3 value:$redundantsequencers." exit 1 fi @@ -350,6 +399,27 @@ while [[ $# -gt 0 ]]; do esac done +if $nowait && ! $detach; then + echo "Error: --nowait requires --detach to be provided." + exit 1 +fi +if $l3_custom_fee_token && ! $l3node; then + echo "Error: --l3-fee-token requires --l3node to be provided." + exit 1 +fi +if $l3_custom_fee_token_pricer && ! $l3_custom_fee_token; then + echo "Error: --l3-fee-token-pricer requires --l3-fee-token to be provided." + exit 1 +fi +if [[ "$l3_custom_fee_token_decimals" != "18" ]] && ! $l3_custom_fee_token; then + echo "Error: --l3-fee-token-decimals requires --l3-fee-token to be provided." + exit 1 +fi +if $l3_token_bridge && ! $l3node; then + echo "Error: --l3-token-bridge requires --l3node to be provided." + exit 1 +fi + NODES="sequencer" INITIAL_SEQ_NODES="sequencer" @@ -405,9 +475,9 @@ fi if $dev_nitro && $build_dev_nitro; then echo == Building Nitro if ! [ -n "${NITRO_SRC+set}" ]; then - NITRO_SRC=`dirname $PWD` + NITRO_SRC=$(dirname "$PWD") fi - if ! grep ^FROM "${NITRO_SRC}/Dockerfile" | grep nitro-node 2>&1 > /dev/null; then + if ! grep -q "^FROM.*nitro-node" "${NITRO_SRC}/Dockerfile" 2>/dev/null; then echo nitro source not found in "$NITRO_SRC" echo execute from a sub-directory of nitro or use NITRO_SRC environment variable exit 1 @@ -461,15 +531,21 @@ fi if $force_init; then echo == Removing old data.. - docker compose down - leftoverContainers=`docker container ls -a --filter label=com.docker.compose.project=nitro-testnode -q | xargs echo` - if [ `echo $leftoverContainers | wc -w` -gt 0 ]; then - docker rm $leftoverContainers + if ! docker compose down -v --remove-orphans --timeout 10; then + echo "Warning: 'docker compose down' failed; forcing cleanup of remaining resources" >&2 + fi + leftoverContainers=$(docker container ls -a --filter label=com.docker.compose.project=nitro-testnode -q) + if [ -n "$leftoverContainers" ]; then + echo "$leftoverContainers" | xargs docker rm -f || echo "Warning: failed to remove some containers" >&2 fi - docker volume prune -f --filter label=com.docker.compose.project=nitro-testnode - leftoverVolumes=`docker volume ls --filter label=com.docker.compose.project=nitro-testnode -q | xargs echo` - if [ `echo $leftoverVolumes | wc -w` -gt 0 ]; then - docker volume rm $leftoverVolumes + docker volume prune -f --filter label=com.docker.compose.project=nitro-testnode || echo "Warning: volume prune failed" >&2 + leftoverVolumes=$(docker volume ls --filter label=com.docker.compose.project=nitro-testnode -q) + if [ -n "$leftoverVolumes" ]; then + echo "$leftoverVolumes" | xargs docker volume rm -f || echo "Warning: failed to remove some volumes" >&2 + fi + leftoverNetworks=$(docker network ls --filter label=com.docker.compose.project=nitro-testnode -q) + if [ -n "$leftoverNetworks" ]; then + echo "$leftoverNetworks" | xargs docker network rm || echo "Warning: some networks could not be removed (may still be in use)" >&2 fi echo == Generating l1 keys @@ -520,8 +596,9 @@ if $force_init; then echo == create l1 traffic run_script send-l1 --ethamount 1000 --to user_l1user --wait run_script send-l1 --ethamount 0.0001 --from user_l1user --to user_l1user --wait --delay 1000 --times 1000000 > /dev/null & + BACKGROUND_PIDS+=($!) - l2ownerAddress=`run_script print-address --account l2owner | tail -n 1 | tr -d '\r\n'` + l2ownerAddress=$(capture_output run_script print-address --account l2owner) l2ChainConfigFlags="" if $l2anytrust; then @@ -534,9 +611,9 @@ if $force_init; then echo "== Writing l2 chain config" run_script --l2owner $l2ownerAddress write-l2-chain-config $l2ChainConfigFlags - sequenceraddress=`run_script print-address --account sequencer | tail -n 1 | tr -d '\r\n'` - l2ownerKey=`run_script print-private-key --account l2owner | tail -n 1 | tr -d '\r\n'` - wasmroot=`docker compose run --rm --entrypoint sh sequencer -c "cat /home/user/target/machines/latest/module-root.txt"` + sequenceraddress=$(capture_output run_script print-address --account sequencer) + l2ownerKey=$(capture_output run_script print-private-key --account l2owner) + wasmroot=$(capture_output docker compose run --rm --entrypoint sh sequencer -c "cat /home/user/target/machines/latest/module-root.txt") echo "== Deploying L2 chain" docker compose run --rm -e PARENT_CHAIN_RPC="http://geth:8545" -e DEPLOYER_PRIVKEY=$l2ownerKey -e PARENT_CHAIN_ID=$l1chainid -e CHILD_CHAIN_NAME="arb-dev-test" -e MAX_DATA_SIZE=117964 -e OWNER_ADDRESS=$l2ownerAddress -e WASM_MODULE_ROOT=$wasmroot -e SEQUENCER_ADDRESS=$sequenceraddress -e AUTHORIZE_VALIDATORS=10 -e CHILD_CHAIN_CONFIG_PATH="/config/l2_chain_config.json" -e CHAIN_DEPLOYMENT_INFO="/config/deployment.json" -e CHILD_CHAIN_INFO="/config/deployed_chain_info.json" rollupcreator create-rollup-testnode @@ -553,10 +630,19 @@ if $force_init; then docker compose run --rm --user root --entrypoint sh datool -c "mkdir /referenceda-provider/keys && chown -R 1000:1000 /referenceda-provider*" docker compose run --rm datool keygen --dir /referenceda-provider/keys --ecdsa - referenceDASignerAddress=`docker compose run --rm --entrypoint sh rollupcreator -c "cat /referenceda-provider/keys/ecdsa.pub | sed 's/^04/0x/' | tr -d '\n' | cast keccak | tail -c 41 | cast to-check-sum-address"` + referenceDASignerAddress=$(capture_output docker compose run --rm --entrypoint sh rollupcreator -c "cat /referenceda-provider/keys/ecdsa.pub | sed 's/^04/0x/' | tr -d '\n' | cast keccak | tail -c 41 | cast to-check-sum-address") echo "== Deploying Reference DA Proof Validator contract on L2" - l2referenceDAValidatorAddress=`docker compose run --rm --entrypoint sh rollupcreator -c "cd /contracts-local && forge create src/osp/ReferenceDAProofValidator.sol:ReferenceDAProofValidator --rpc-url http://geth:8545 --private-key $l2ownerKey --broadcast --constructor-args [$referenceDASignerAddress]" | awk '/Deployed to:/ {print $NF}'` + l2referenceDAOutput=$(docker compose run --rm --entrypoint sh rollupcreator -c "cd /contracts-local && forge create src/osp/ReferenceDAProofValidator.sol:ReferenceDAProofValidator --rpc-url http://geth:8545 --private-key $l2ownerKey --broadcast --constructor-args [$referenceDASignerAddress]") || { + echo "Error: forge create failed for ReferenceDAProofValidator" >&2 + exit 1 + } + l2referenceDAValidatorAddress=$(echo "$l2referenceDAOutput" | awk '/Deployed to:/ {print $NF}') + if [ -z "$l2referenceDAValidatorAddress" ]; then + echo "Error: failed to extract ReferenceDAProofValidator address from forge output:" >&2 + echo "$l2referenceDAOutput" >&2 + exit 1 + fi echo "== Generating Reference DA Config" run_script write-l2-referenceda-config --validator-address $l2referenceDAValidatorAddress @@ -580,8 +666,8 @@ if $l2anytrust; then run_script write-l2-das-committee-config run_script write-l2-das-mirror-config - das_bls_a=`docker compose run --rm --entrypoint sh datool -c "cat /das-committee-a/keys/das_bls.pub"` - das_bls_b=`docker compose run --rm --entrypoint sh datool -c "cat /das-committee-b/keys/das_bls.pub"` + das_bls_a=$(capture_output docker compose run --rm --entrypoint sh datool -c "cat /das-committee-a/keys/das_bls.pub") + das_bls_b=$(capture_output docker compose run --rm --entrypoint sh datool -c "cat /das-committee-b/keys/das_bls.pub") run_script write-l2-das-keyset-config --dasBlsA $das_bls_a --dasBlsB $das_bls_b docker compose run --rm --entrypoint sh datool -c "/usr/local/bin/datool dumpkeyset --conf.file /config/l2_das_keyset.json | grep 'Keyset: ' | awk '{ printf \"%s\", \$2 }' > /config/l2_das_keyset.hex" @@ -625,16 +711,21 @@ if $force_init; then echo == Funding l2 funnel and dev key docker compose up --wait $INITIAL_SEQ_NODES - sleep 45 # in case we need to create a smart contract wallet, allow for parent chain to recieve the contract creation tx and process it + # Wait for L1 blocks to be mined so the staker's smart contract wallet deployment tx is confirmed + wait_for_chain_progress send-l1 "L1" run_script bridge-funds --ethamount 100000 --wait run_script send-l2 --ethamount 100 --to l2owner --wait - rollupAddress=`docker compose run --rm --entrypoint sh poster -c "jq -r '.[0].rollup.rollup' /config/deployed_chain_info.json | tail -n 1 | tr -d '\r\n'"` + rollupAddress=$(capture_output docker compose run --rm --entrypoint sh poster -c "jq -r '.[0].rollup.rollup' /config/deployed_chain_info.json") + if [ "$rollupAddress" = "null" ]; then + echo "Error: failed to extract rollup address from deployed_chain_info.json" >&2 + exit 1 + fi if $l2timeboost; then run_script send-l2 --ethamount 100 --to auctioneer --wait - biddingTokenAddress=`run_script create-erc20 --deployer auctioneer | tail -n 1 | awk '{ print $NF }'` - auctionContractAddress=`run_script deploy-express-lane-auction --bidding-token $biddingTokenAddress | tail -n 1 | awk '{ print $NF }'` - auctioneerAddress=`run_script print-address --account auctioneer | tail -n1 | tr -d '\r\n'` + biddingTokenAddress=$(capture_last_field run_script create-erc20 --deployer auctioneer) + auctionContractAddress=$(capture_last_field run_script deploy-express-lane-auction --bidding-token "$biddingTokenAddress") + auctioneerAddress=$(capture_output run_script print-address --account auctioneer) echo == Starting up Timeboost auctioneer and bid validator. echo == Bidding token: $biddingTokenAddress, auction contract $auctionContractAddress run_script write-timeboost-configs --auction-contract $auctionContractAddress @@ -646,7 +737,7 @@ if $force_init; then run_script transfer-erc20 --token $biddingTokenAddress --amount 10000 --from auctioneer --to user_alice run_script transfer-erc20 --token $biddingTokenAddress --amount 10000 --from auctioneer --to user_bob - docker compose run --rm --entrypoint sh scripts -c "sed -i 's/\(\"execution\":{\"sequencer\":{\"enable\":true,\"timeboost\":{\"enable\":\)false/\1true,\"auction-contract-address\":\"$auctionContractAddress\",\"auctioneer-address\":\"$auctioneerAddress\"/' /config/sequencer_config.json" --wait + docker compose run --rm --entrypoint sh rollupcreator -c "jq --arg ac \"$auctionContractAddress\" --arg aa \"$auctioneerAddress\" '.execution.sequencer.timeboost.enable = true | .execution.sequencer.timeboost.\"auction-contract-address\" = \$ac | .execution.sequencer.timeboost.\"auctioneer-address\" = \$aa' /config/sequencer_config.json > /tmp/sequencer_config.json && mv /tmp/sequencer_config.json /config/sequencer_config.json" docker compose restart $INITIAL_SEQ_NODES fi @@ -663,7 +754,8 @@ if $force_init; then if $tokenbridge; then echo == Deploying L1-L2 token bridge - sleep 10 # no idea why this sleep is needed but without it the deploy fails randomly + # Ensure L2 is producing blocks before token bridge deployment + wait_for_chain_progress send-l2 "L2" docker compose run --rm -e ROLLUP_OWNER_KEY=$l2ownerKey -e ROLLUP_ADDRESS=$rollupAddress -e PARENT_KEY=$devprivkey -e PARENT_RPC=http://geth:8545 -e CHILD_KEY=$devprivkey -e CHILD_RPC=http://sequencer:8547 tokenbridge deploy:local:token-bridge docker compose run --rm --entrypoint sh tokenbridge -c "cat network.json && cp network.json l1l2_network.json && cp network.json localNetwork.json" echo @@ -684,6 +776,7 @@ if $force_init; then echo == create l2 traffic run_script send-l2 --ethamount 100 --to user_traffic_generator --wait run_script send-l2 --ethamount 0.0001 --from user_traffic_generator --to user_traffic_generator --wait --delay 500 --times 1000000 > /dev/null & + BACKGROUND_PIDS+=($!) fi if $l3node; then @@ -701,51 +794,64 @@ if $force_init; then run_script send-l2 --ethamount 100 --to user_fee_token_deployer --wait echo == Writing l3 chain config - l3owneraddress=`run_script print-address --account l3owner | tail -n 1 | tr -d '\r\n'` + l3owneraddress=$(capture_output run_script print-address --account l3owner) echo l3owneraddress $l3owneraddress run_script --l2owner $l3owneraddress write-l3-chain-config EXTRA_L3_DEPLOY_FLAG="" if $l3_custom_fee_token; then echo == Deploying custom fee token - nativeTokenAddress=`run_script create-erc20 --deployer user_fee_token_deployer --bridgeable $tokenbridge --decimals $l3_custom_fee_token_decimals | tail -n 1 | awk '{ print $NF }'` + nativeTokenAddress=$(capture_last_field run_script create-erc20 --deployer user_fee_token_deployer --bridgeable $tokenbridge --decimals $l3_custom_fee_token_decimals) run_script transfer-erc20 --token $nativeTokenAddress --amount 10000 --from user_fee_token_deployer --to l3owner run_script transfer-erc20 --token $nativeTokenAddress --amount 10000 --from user_fee_token_deployer --to user_token_bridge_deployer EXTRA_L3_DEPLOY_FLAG="-e FEE_TOKEN_ADDRESS=$nativeTokenAddress" if $l3_custom_fee_token_pricer; then echo == Deploying custom fee token pricer - feeTokenPricerAddress=`run_script create-fee-token-pricer --deployer user_fee_token_deployer | tail -n 1 | awk '{ print $NF }'` + feeTokenPricerAddress=$(capture_last_field run_script create-fee-token-pricer --deployer user_fee_token_deployer) EXTRA_L3_DEPLOY_FLAG="$EXTRA_L3_DEPLOY_FLAG -e FEE_TOKEN_PRICER_ADDRESS=$feeTokenPricerAddress" fi fi echo == Deploying L3 - l3ownerkey=`run_script print-private-key --account l3owner | tail -n 1 | tr -d '\r\n'` - l3sequenceraddress=`run_script print-address --account l3sequencer | tail -n 1 | tr -d '\r\n'` + l3ownerkey=$(capture_output run_script print-private-key --account l3owner) + l3sequenceraddress=$(capture_output run_script print-address --account l3sequencer) docker compose run --rm -e DEPLOYER_PRIVKEY=$l3ownerkey -e PARENT_CHAIN_RPC="http://sequencer:8547" -e PARENT_CHAIN_ID=412346 -e CHILD_CHAIN_NAME="orbit-dev-test" -e MAX_DATA_SIZE=104857 -e OWNER_ADDRESS=$l3owneraddress -e WASM_MODULE_ROOT=$wasmroot -e SEQUENCER_ADDRESS=$l3sequenceraddress -e AUTHORIZE_VALIDATORS=10 -e CHILD_CHAIN_CONFIG_PATH="/config/l3_chain_config.json" -e CHAIN_DEPLOYMENT_INFO="/config/l3deployment.json" -e CHILD_CHAIN_INFO="/config/deployed_l3_chain_info.json" $EXTRA_L3_DEPLOY_FLAG rollupcreator create-rollup-testnode docker compose run --rm --entrypoint sh rollupcreator -c "jq [.[]] /config/deployed_l3_chain_info.json > /config/l3_chain_info.json" echo == Funding l3 funnel and dev key docker compose up --wait l3node sequencer - sleep 45 # in case we need to create a smart contract wallet, allow for parent chain to recieve the contract creation tx and process it + # Wait for L2 blocks to be mined so the L3 staker's smart contract wallet deployment tx is confirmed + wait_for_chain_progress send-l2 "L2" if $l3_token_bridge; then echo == Deploying L2-L3 token bridge - deployer_key=`printf "%s" "user_token_bridge_deployer" | openssl dgst -sha256 | sed 's/^.*= //'` - rollupAddress=`docker compose run --rm --entrypoint sh poster -c "jq -r '.[0].rollup.rollup' /config/deployed_l3_chain_info.json | tail -n 1 | tr -d '\r\n'"` + deployer_key=$(printf "%s" "user_token_bridge_deployer" | openssl dgst -sha256 | sed 's/^.*= //') + rollupAddress=$(capture_output docker compose run --rm --entrypoint sh poster -c "jq -r '.[0].rollup.rollup' /config/deployed_l3_chain_info.json") + if [ "$rollupAddress" = "null" ]; then + echo "Error: failed to extract rollup address from deployed_l3_chain_info.json" >&2 + exit 1 + fi l2Weth="" if $tokenbridge; then # we deployed an L1 L2 token bridge # we need to pull out the L2 WETH address and pass it as an override to the L2 L3 token bridge deployment - l2Weth=`docker compose run --rm --entrypoint sh tokenbridge -c "cat l1l2_network.json" | jq -r '.l2Network.tokenBridge.childWeth'` + l2Weth=$(docker compose run --rm --entrypoint sh tokenbridge -c "cat l1l2_network.json" | jq -r '.l2Network.tokenBridge.childWeth') + if [ "$l2Weth" = "null" ] || [ -z "$l2Weth" ]; then + echo "Error: failed to extract childWeth from l1l2_network.json" >&2 + exit 1 + fi fi docker compose run --rm -e PARENT_WETH_OVERRIDE=$l2Weth -e ROLLUP_OWNER_KEY=$l3ownerkey -e ROLLUP_ADDRESS=$rollupAddress -e PARENT_RPC=http://sequencer:8547 -e PARENT_KEY=$deployer_key -e CHILD_RPC=http://l3node:3347 -e CHILD_KEY=$deployer_key tokenbridge deploy:local:token-bridge docker compose run --rm --entrypoint sh tokenbridge -c "cat network.json && cp network.json l2l3_network.json" - # set L3 UpgradeExecutor, deployed by token bridge creator in previous step, to be the L3 chain owner. L3owner (EOA) and alias of L2 UpgradeExectuor have the executor role on the L3 UpgradeExecutor + # set L3 UpgradeExecutor, deployed by token bridge creator in previous step, to be the L3 chain owner. L3owner (EOA) and alias of L2 UpgradeExecutor have the executor role on the L3 UpgradeExecutor echo == Set L3 UpgradeExecutor to be chain owner - tokenBridgeCreator=`docker compose run --rm --entrypoint sh tokenbridge -c "cat l2l3_network.json" | jq -r '.l1TokenBridgeCreator'` + tokenBridgeCreator=$(docker compose run --rm --entrypoint sh tokenbridge -c "cat l2l3_network.json" | jq -r '.l1TokenBridgeCreator') + if [ "$tokenBridgeCreator" = "null" ] || [ -z "$tokenBridgeCreator" ]; then + echo "Error: failed to extract l1TokenBridgeCreator from l2l3_network.json" >&2 + exit 1 + fi run_script transfer-l3-chain-ownership --creator $tokenBridgeCreator echo fi @@ -769,6 +875,7 @@ if $force_init; then echo == create l3 traffic run_script send-l3 --ethamount 10 --to user_traffic_generator --wait run_script send-l3 --ethamount 0.0001 --from user_traffic_generator --to user_traffic_generator --wait --delay 5000 --times 1000000 > /dev/null & + BACKGROUND_PIDS+=($!) fi fi fi