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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
83 changes: 76 additions & 7 deletions test/gas-analytics/HubPool.RootExecution.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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[];
Expand All @@ -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[]) {
Expand Down Expand Up @@ -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));
Expand All @@ -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]);
Expand Down Expand Up @@ -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.
Expand All @@ -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);
});
});
});
54 changes: 54 additions & 0 deletions test/gas-analytics/SpokePool.RelayerRefundRootExecution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ let tree: MerkleTree<RelayerRefundLeaf>;
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(
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have any information on limits on most L2s? If you don't know, that's totally fine, we should just add a TODO to figure out a good limit that's below all L2s that we're deploying to.

// 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);
});
});
});