diff --git a/package.json b/package.json index 3bddda9a5..8b50f31b9 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "test": "hardhat test", "test:report-gas": "REPORT_GAS=true hardhat test", "test:gas-analytics": "GAS_TEST_ENABLED=true hardhat test ./test/gas-analytics/*", + "test:all": "GAS_TEST_ENABLED=true REPORT_GAS=true yarn hardhat test", "prepublish": "yarn build" }, "dependencies": { diff --git a/test/gas-analytics/HubPool.RootExecution.ts b/test/gas-analytics/HubPool.RootExecution.ts index 4835b72e3..d1d3c24a6 100644 --- a/test/gas-analytics/HubPool.RootExecution.ts +++ b/test/gas-analytics/HubPool.RootExecution.ts @@ -1,4 +1,4 @@ -import { toBNWei, toBN, SignerWithAddress, seedWallet, Contract, ethers, hre } from "../utils"; +import { toBNWei, toBN, SignerWithAddress, seedWallet, Contract, ethers, hre, expect } from "../utils"; import { getContractFactory, BigNumber, randomAddress, createRandomBytes32 } from "../utils"; import { deployErc20 } from "./utils"; import * as consts from "../constants"; @@ -11,6 +11,7 @@ require("dotenv").config(); let hubPool: Contract, timer: Contract, weth: Contract; let owner: SignerWithAddress, dataWorker: SignerWithAddress, liquidityProvider: SignerWithAddress; +let hubPoolChainId: number; // Associates an array of L1 tokens to sends refunds for to each chain ID. let l1Tokens: Contract[]; @@ -25,6 +26,13 @@ const SEND_AMOUNT = toBNWei("10"); const STARTING_LP_AMOUNT = SEND_AMOUNT.mul(100); // This should be >= `SEND_AMOUNT` otherwise some relays will revert because // the pool balance won't be sufficient to cover the relay. const LP_FEE = SEND_AMOUNT.div(toBN(10)); +// Regarding the block limit, the max limit is 30 million gas, the expected block gas limit is 15 million, so +// we'll target 12 million gas as a conservative upper-bound. This test script will fail if executing a leaf with +// `STRESS_TEST_L1_TOKEN_COUNT` number of tokens to send pool rebalances for is not within the +// [TARGET_GAS_LOWER_BOUND, TARGET_GAS_UPPER_BOUND] gas usage range. +const TARGET_GAS_UPPER_BOUND = 12_000_000; +const TARGET_GAS_LOWER_BOUND = 10_000_000; +const STRESS_TEST_L1_TOKEN_COUNT = 100; // Construct tree with REFUND_CHAIN_COUNT leaves, each containing REFUND_TOKEN_COUNT sends async function constructSimpleTree(_destinationChainIds: number[], _l1Tokens: Contract[]) { @@ -66,7 +74,7 @@ describe("Gas Analytics: HubPool Root Bundle Execution", function () { [owner, dataWorker, liquidityProvider] = await ethers.getSigners(); ({ hubPool, timer, weth } = await hubPoolFixture()); - const hubPoolChainId = Number(await hre.getChainId()); + hubPoolChainId = Number(await hre.getChainId()); // Seed data worker with bond tokens. await seedWallet(dataWorker, [], weth, consts.bondAmount.mul(10)); @@ -78,10 +86,6 @@ describe("Gas Analytics: HubPool Root Bundle Execution", function () { const _l1Token = await deployErc20(owner, `Test Token #${i}`, `T-${i}`); l1Tokens.push(_l1Token); - // Mint data worker amount of tokens needed to bond a new root - await seedWallet(dataWorker, [_l1Token], undefined, consts.bondAmount.mul(100)); - await _l1Token.connect(dataWorker).approve(hubPool.address, consts.maxUint256); - // Mint LP amount of tokens needed to cover relay await seedWallet(liquidityProvider, [_l1Token], undefined, STARTING_LP_AMOUNT); await enableTokensForLP(owner, hubPool, weth, [_l1Token]); @@ -190,7 +194,6 @@ describe("Gas Analytics: HubPool Root Bundle Execution", function () { const gasUsed = receipts.map((_receipt) => _receipt.gasUsed).reduce((x, y) => x.add(y)); console.log(`(average) executeRootBundle-gasUsed: ${gasUsed.div(REFUND_CHAIN_COUNT)}`); }); - it("Executing all leaves using multicall", async function () { await hubPool.connect(dataWorker).proposeRootBundle( destinationChainIds, // bundleEvaluationBlockNumbers used by bots to construct bundles. Length must equal the number of leafs. @@ -212,5 +215,71 @@ describe("Gas Analytics: HubPool Root Bundle Execution", function () { const receipt = await (await hubPool.connect(dataWorker).multicall(multicallData)).wait(); console.log(`(average) executeRootBundle-gasUsed: ${receipt.gasUsed.div(REFUND_CHAIN_COUNT)}`); }); + it(`Stress Test: 1 leaf contains ${STRESS_TEST_L1_TOKEN_COUNT} L1 tokens with netSendAmounts > 0`, async function () { + // This test should inform the limit # of L1 tokens that we would allow a PoolRebalanceLeaf to contain to avoid + // publishing a leaf that is unexecutable due to the block gas limit. Note that this estimate is a bit contrived + // and likely an underestimate because we are relaying tokens via the MockAdapter, not an Adapter used for + // production. + + // Regarding the block limit, the max limit is 30 million gas, the expected block gas limit is 15 million, so + // we'll target 12 million gas as a conservative upper-bound. + const l1TokenAddresses = []; + for (let i = 0; i < STRESS_TEST_L1_TOKEN_COUNT; i++) { + const _l1Token = await deployErc20(owner, `Test Token #${i}`, `T-${i}`); + l1TokenAddresses.push(_l1Token.address); + + // Mint LP amount of tokens needed to cover relay + await seedWallet(liquidityProvider, [_l1Token], undefined, STARTING_LP_AMOUNT); + await enableTokensForLP(owner, hubPool, weth, [_l1Token]); + await _l1Token.connect(liquidityProvider).approve(hubPool.address, consts.maxUint256); + await hubPool.connect(liquidityProvider).addLiquidity(_l1Token.address, STARTING_LP_AMOUNT); + + // Whitelist token route from HubPool to dest. chain ID. Destination token doesn't matter for this test. + await hubPool.setPoolRebalanceRoute(destinationChainIds[0], _l1Token.address, randomAddress()); + } + + // Add leaf to tree that contains enough L1 tokens that we can determine the limit after which the executeRoot + // will fail due to out of gas. + const bigLeaves = buildPoolRebalanceLeafs( + [destinationChainIds[0]], + [l1TokenAddresses], + [Array(STRESS_TEST_L1_TOKEN_COUNT).fill(toBNWei("0"))], + [Array(STRESS_TEST_L1_TOKEN_COUNT).fill(SEND_AMOUNT)], + [Array(STRESS_TEST_L1_TOKEN_COUNT).fill(SEND_AMOUNT)], + [0] + ); + const bigLeafTree = await buildPoolRebalanceLeafTree(bigLeaves); + + await hubPool + .connect(dataWorker) + .proposeRootBundle( + [consts.mockBundleEvaluationBlockNumbers[0]], + 1, + bigLeafTree.getHexRoot(), + consts.mockRelayerRefundRoot, + consts.mockSlowRelayRoot + ); + + // Advance time so the request can be executed and execute the request. + await timer.setCurrentTime(Number(await timer.getCurrentTime()) + consts.refundProposalLiveness + 1); + + // Estimate the transaction gas and set it (plus some buffer) explicitly as the transaction's gas limit. This is + // done because ethers.js' default gas limit setting doesn't seem to always work and sometimes overestimates + // it and throws something like: + // "InvalidInputError: Transaction gas limit is X and exceeds block gas limit of 30000000" + const gasEstimate = await hubPool + .connect(dataWorker) + .estimateGas.executeRootBundle(...Object.values(bigLeaves[0]), bigLeafTree.getHexProof(bigLeaves[0])); + const txn = await hubPool + .connect(dataWorker) + .executeRootBundle(...Object.values(bigLeaves[0]), bigLeafTree.getHexProof(bigLeaves[0]), { + gasLimit: gasEstimate.mul(toBN("1.2")), + }); + + const receipt = await txn.wait(); + console.log(`executeRootBundle-gasUsed: ${receipt.gasUsed}`); + expect(Number(receipt.gasUsed)).to.be.lessThanOrEqual(TARGET_GAS_UPPER_BOUND); + expect(Number(receipt.gasUsed)).to.be.greaterThanOrEqual(TARGET_GAS_LOWER_BOUND); + }); }); }); diff --git a/test/gas-analytics/SpokePool.RelayerRefundRootExecution.ts b/test/gas-analytics/SpokePool.RelayerRefundRootExecution.ts index 848843641..ba34843e1 100644 --- a/test/gas-analytics/SpokePool.RelayerRefundRootExecution.ts +++ b/test/gas-analytics/SpokePool.RelayerRefundRootExecution.ts @@ -32,6 +32,15 @@ let tree: MerkleTree; const REFUND_LEAF_COUNT = 10; const REFUNDS_PER_LEAF = 10; const REFUND_AMOUNT = toBNWei("10"); +// Regarding the block limit, the max limit is 30 million gas, the expected block gas limit is 15 million, so +// we'll target 12 million gas as a conservative upper-bound. This test script will fail if executing a leaf with +// `STRESS_TEST_REFUND_COUNT` number of refunds is not within the [TARGET_GAS_LOWER_BOUND, TARGET_GAS_UPPER_BOUND] +// gas usage range. +const TARGET_GAS_UPPER_BOUND = 12_000_000; +const TARGET_GAS_LOWER_BOUND = 5_000_000; +// Note: I can't get this to work with a gas >> 5mil without the transaction timing out. This is why I've set +// the lower bound to 6mil instead of a tighter 10mil. +const STRESS_TEST_REFUND_COUNT = 800; // Construct tree with REFUND_LEAF_COUNT leaves, each containing REFUNDS_PER_LEAF refunds. async function constructSimpleTree( @@ -244,5 +253,50 @@ describe("Gas Analytics: SpokePool Relayer Refund Root Execution", function () { const receipt = await txn.wait(); console.log(`executeRelayerRefundRoot-gasUsed: ${receipt.gasUsed}`); }); + it(`Stress Test: 1 leaf contains ${STRESS_TEST_REFUND_COUNT} refunds with amount > 0`, async function () { + // This test should inform the limit # refunds that we would allow a RelayerRefundLeaf to contain to avoid + // publishing a leaf that is unexecutable due to the block gas limit. + + // Note: Since the SpokePool is deployed on L2s we care specifically about L2 block gas limits. + // - Optimism: 15mil cap, soon to be raised to 30mil when they upgrade to London. + // - Arbitrum: uses different units when reasoning about gas (but with the nitro upgrade those will then be + // closer to Ethereum). You can do about the same amount of computation per second on the chain; each + // transaction can use up to 2.5m arbgas in computation. + // - Polygon: same as L1 + + // Regarding the block limit, the max limit is 30 million gas, the expected block gas limit is 15 million, so + // we'll target 12 million gas as a conservative upper-bound. + await seedContract(spokePool, owner, [], weth, toBN(STRESS_TEST_REFUND_COUNT).mul(REFUND_AMOUNT).mul(toBN(10))); + + // Create tree with 1 large leaf. + const bigLeaves = buildRelayerRefundLeafs( + [destinationChainIds[0]], + [toBNWei("1")], // Set amount to return > 0 to better simulate long execution path of _executeRelayerRefundLeaf + [weth.address], + [Array(STRESS_TEST_REFUND_COUNT).fill(recipient.address)], + [Array(STRESS_TEST_REFUND_COUNT).fill(REFUND_AMOUNT)] + ); + const bigLeafTree = await buildRelayerRefundTree(bigLeaves); + + await spokePool.connect(dataWorker).relayRootBundle(bigLeafTree.getHexRoot(), consts.mockSlowRelayRoot); + + // Estimate the transaction gas and set it (plus some buffer) explicitly as the transaction's gas limit. This is + // done because ethers.js' default gas limit setting doesn't seem to always work and sometimes overestimates + // it and throws something like: + // "InvalidInputError: Transaction gas limit is X and exceeds block gas limit of 30000000" + const gasEstimate = await spokePool + .connect(dataWorker) + .estimateGas.executeRelayerRefundRoot(1, bigLeaves[0], bigLeafTree.getHexProof(bigLeaves[0])); + const txn = await spokePool + .connect(dataWorker) + .executeRelayerRefundRoot(1, bigLeaves[0], bigLeafTree.getHexProof(bigLeaves[0]), { + gasLimit: gasEstimate.mul(toBN("1.2")), + }); + + const receipt = await txn.wait(); + console.log(`executeRelayerRefundRoot-gasUsed: ${receipt.gasUsed}`); + expect(Number(receipt.gasUsed)).to.be.lessThanOrEqual(TARGET_GAS_UPPER_BOUND); + expect(Number(receipt.gasUsed)).to.be.greaterThanOrEqual(TARGET_GAS_LOWER_BOUND); + }); }); });