Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add load testing with async and parameterized features #12

Merged
merged 13 commits into from
Aug 14, 2024
Merged
Binary file modified utils/bun.lockb
Binary file not shown.
15 changes: 15 additions & 0 deletions utils/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,28 @@ async function main() {
}
);

cli.command("load-test", "\n\t└>Load Test. The cmd makes use of a rich pk in order to perform the tests.\n\t It creates random Wallets\n\t Sends some ERC20 (amount/wallets) on L1\n\t Each wallet performs a deposit on L2\n\t The ERC20 is the zkStack's BaseToken")
.option("--l1url <l1url>", "ETH chain URL, defaults to localhost dev env")
.option("--l2url <l1url>", "zkStack chain URL, defaults to localhost dev env")
.option("--pk <pk>", "Rich PK, defaults to RETH's rich wallet")
.option("-a, --amount <a>", "ERC20 Amount to send, defaults to 100")
.option("-w, --wallets <w>", "Amount of wallets, defaults to 5")
.example("[dev-env] load-test")
.example("[real-env] load-test --l1url <l1-url> --l2url <zkstack_url> --pk <pk> --amount <amount> --wallets <number-of-wallets>")
.action(
async (options) => {
await cmd.test.loadTest(options.l1url, options.l2url, options.pk, options.wallets, options.amount);
}
);

cli.command("get-wallet [pk]", "\n\t└>Get Random Addr and PK or Get Addr if PK is passed as argument")
.example("get-wallet")
.action(
async (pk) => {
await cmd.utils.getRandomWallet(pk);
}
);

cli.help();
cli.parse();
}
Expand Down
7 changes: 7 additions & 0 deletions utils/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { getBalance } from "./cmdGetBalance";
import { sendBalance } from "./cmdSendBalance";
import { depositBalance } from "./cmdDeposit";
import { fixNonce } from "./cmdFixNonce";
<<<<<<< HEAD
import { loadTest } from "./loadTester/cmdLoadTest";
=======
import { getRandomWallet } from "./cmdGetRandomWallet";
>>>>>>> main

export const cmd = {
balance: {
Expand All @@ -13,6 +17,9 @@ export const cmd = {
maintenance: {
fixNonce
},
test: {
loadTest
},
utils: {
getRandomWallet
}
Expand Down
126 changes: 126 additions & 0 deletions utils/commands/loadTester/cmdLoadTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { Wallet, Provider } from "zksync-ethers";
import * as ethers from "ethers";

import { env } from "process";
import contractAbi from "./helpers/erc20_abi.json";
import { helpers } from "./helpers";

// HTTP RPC endpoints
const L1_RPC_ENDPOINT = "http://127.0.0.1:8545";
const L2_RPC_ENDPOINT = "http://127.0.0.1:3050";

const AMOUNT_TO_BRIDGE = "100";
const AMOUNT_OF_ETH = "0.05";
const AMOUNT_OF_WALLETS = 5;

const L1_RICH_PK =
env.L1_RICH_PK ||
"0x850683b40d4a740aa6e745f889a6fdc8327be76e122f5aba645a5b02d0248db8";

const L1_RICH = {
addr: ethers.utils.computeAddress(L1_RICH_PK),
pk: L1_RICH_PK,
};

export async function loadTest(l1url: string, l2url: string, pk: string, numberOfWallets: number, amount: string) {

// Initialize the rich wallet, ERC20 contract and providers
const l1Provider = new ethers.providers.JsonRpcProvider(l1url || L1_RPC_ENDPOINT);
const l2Provider = new Provider(l2url || L2_RPC_ENDPOINT);
const zkWallet = new Wallet(pk || L1_RICH.pk, l2Provider, l1Provider);

const ethWallet = new ethers.Wallet(pk || L1_RICH_PK, l1Provider);
const ERC20_L1 = new ethers.Contract(
await l2Provider.getBaseTokenContractAddress(),
contractAbi,
ethWallet
);
const ERC20_SYMBOL: string = await ERC20_L1.symbol();
const ERC20_DECIMALS_MUL = Math.pow(10, Number(await ERC20_L1.decimals()));

// Initialize the rich wallet.
const amountOfWallets = numberOfWallets || AMOUNT_OF_WALLETS;
let wallets: Wallet[] = new Array<Wallet>();
for (let index = 0; index < amountOfWallets; index++) {
const pk = Wallet.createRandom().privateKey;
const w = new Wallet(pk, l2Provider, l1Provider);
wallets.push(w);
}

const amountToBridge = Number(amount || AMOUNT_TO_BRIDGE);
const amountForEach = amountToBridge / wallets.length;

console.log("#####################################################\n");
wallets.forEach((w, i) => {
console.log(
`Wallet(${i.toString().padStart(2, "0")}) addr: ${w.address} || pk: ${w.privateKey}`
);
});
console.log("\n#####################################################\n");
console.log(`[L1] Endpoint: ${L1_RPC_ENDPOINT}`);
console.log(`[L2] Endpoint: ${L2_RPC_ENDPOINT}`);
console.log("\n#####################################################\n");

const erc20Balance: number = await helpers.l1.getERC20Balance(
ethWallet.address,
ERC20_L1,
ERC20_DECIMALS_MUL,
ERC20_SYMBOL
);
if (erc20Balance <= amountToBridge) {
const response = await ERC20_L1.mint(
ethWallet.address,
BigInt(amountToBridge * ERC20_DECIMALS_MUL)
);
const receipt = await response.wait();
console.log(
`${amountToBridge} Minted ${ERC20_SYMBOL}, txHash: ${receipt.transactionHash}`
);
await helpers.l1.getERC20Balance(
ethWallet.address,
ERC20_L1,
ERC20_DECIMALS_MUL,
ERC20_SYMBOL
);
}

let consumedL1Gas = await ethWallet.provider.getBalance(ethWallet.address);

console.log("=====================================================");

console.log(`[L1 -> L1]: Send ETH`);
const amountOfEth = ethers.utils.parseEther(AMOUNT_OF_ETH).div(wallets.length);
await helpers.l1.sendMultipleL1ETHTransfers(ethWallet, wallets, ethers.utils.formatEther(amountOfEth));

console.log("=====================================================");

console.log("[L1 -> L1]: Send ERC20");
await helpers.l1.sendMultipleL1ERC20Transfers(
ethWallet,
wallets,
amountForEach,
ERC20_L1
);

console.log("=====================================================");

consumedL1Gas = consumedL1Gas.add(amountOfEth).sub(
await ethWallet.provider.getBalance(ethWallet.address)
);

console.log(`Consumed L1 Gas: ${ethers.utils.formatEther(consumedL1Gas)}`);

console.log("=====================================================");

const amountForEachToDeposit = amountForEach / 2;
console.log("[L1->L2]: Deposit BaseToken");
await helpers.l2.sendMultipleL2BaseTokenDeposits(zkWallet, wallets, amountForEachToDeposit);

console.log("=====================================================");

const amountForEachToTransfer = amountForEachToDeposit / 2;
console.log("[L2->L2]: Transfer BaseToken");
await helpers.l2.sendMultipleL2Transfers(wallets, amountForEachToTransfer);

console.log("=====================================================");
}
135 changes: 135 additions & 0 deletions utils/commands/loadTester/helpers/L1-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { Wallet } from "zksync-ethers";
import * as ethers from "ethers";

export async function getERC20Balance(
address: string,
ERC20_L1: ethers.ethers.Contract,
ERC20_DECIMALS_MUL: number,
ERC20_SYMBOL: string
) {
return await ERC20_L1.balanceOf(address)
.then((balance: number) => {
console.log(
`L1 ERC20 Balance: ${balance / ERC20_DECIMALS_MUL} ${ERC20_SYMBOL}`
);
return balance / ERC20_DECIMALS_MUL;
})
.catch(() => {
console.error("Error fetching ERC20 balance from L1");
return 0;
});
};

export async function l1ERC20Transfer(
ethwallet: ethers.Wallet,
nonce: number,
amount: string | number,
address: string,
ERC20_L1: ethers.ethers.Contract
) {
const parsedAmount = typeof amount == "number" ? amount.toString() : amount;
const data = ERC20_L1.interface.encodeFunctionData("transfer", [
address,
ethers.utils.parseEther(parsedAmount),
]);
const limit = await ethwallet.provider.estimateGas({
to: ERC20_L1.address,
from: ethwallet.address,
nonce,
data: data,
});
const gasLimit = Math.ceil(limit.toNumber() * 1.2);

return ethwallet
.sendTransaction({
to: ERC20_L1.address,
from: ethwallet.address,
nonce,
data: data,
gasLimit,
})
.then(async (response) => {
const receipt = await response.wait();
const msg =
`#####################################################
Wallet: ${ethwallet.address}
Tx hash: ${receipt.transactionHash}
#####################################################`;
console.log(msg.split('\n').map(line => line.trim()).join('\n'));
return response;
})
.catch((error) => {
throw error;
});
};

export async function sendMultipleL1ERC20Transfers(
walletEthers: ethers.Wallet,
wallets: Wallet[],
amountForEach: string | number,
ERC20_L1: ethers.Contract
) {
const amount =
typeof amountForEach == "number" ? amountForEach.toString() : amountForEach;
const transactionPromises: Promise<ethers.providers.TransactionResponse>[] =
[];
let nonce = await walletEthers.provider.getTransactionCount(
walletEthers.address,
"latest"
);
for (const w of wallets) {
const transactionPromise = l1ERC20Transfer(
walletEthers,
nonce++,
amount,
w.address,
ERC20_L1
);
transactionPromises.push(transactionPromise);
}
await Promise.all(transactionPromises);
}

export async function sendMultipleL1ETHTransfers(
walletEthers: ethers.Wallet,
wallets: Wallet[],
amountForEach: string | number
) {
const amount =
typeof amountForEach == "number" ? amountForEach.toString() : amountForEach;
let nonce = await walletEthers.provider.getTransactionCount(
walletEthers.address,
"latest"
);
const transactionPromises: Promise<ethers.providers.TransactionResponse>[] =
[];

for (const w of wallets) {
const tx = {
to: w.address,
nonce: nonce++,
value: ethers.utils.parseEther(amount),
gasLimit: 21000,
gasPrice: await walletEthers.provider.getGasPrice(),
};

const transactionPromise = walletEthers
.sendTransaction(tx)
.then(async (response) => {
const receipt = await response.wait();
const msg =
`#####################################################
Wallet: ${w.address}
Tx hash: ${receipt.transactionHash}
#####################################################`;
console.log(msg.split('\n').map(line => line.trim()).join('\n'));
return response;
})
.catch((error) => {
throw error;
});

transactionPromises.push(transactionPromise);
}
await Promise.all(transactionPromises);
}
Loading