Skip to content

Commit

Permalink
first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
gaetbout committed Apr 16, 2024
0 parents commit 3515ae8
Show file tree
Hide file tree
Showing 22 changed files with 3,099 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ARGENT_CLASS_HASH=0x000000000000000000000000000000000000000000000000000000000000000
RPC_URL=http://127.0.0.1:5050
ADDRESS=0x000000000000000000000000000000000000000000000000000000000000000
PRIVATE_KEY=0x000000000000000000000000000000000000000000000000000000000000000
12 changes: 12 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"extends": ["plugin:@typescript-eslint/recommended"],
"env": {
"node": true
},
"ignorePatterns": ["dist", "cairo"]
}
28 changes: 28 additions & 0 deletions .github/workflows/integration-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Integration CI

on: push

jobs:
format:
runs-on: ubuntu-latest
steps:
- name: Step 1 - Check out main branch
uses: actions/checkout@v3

- name: Step 2 - Install project
run: yarn install --frozen-lockfile

- name: Step 3 - Check correct formatting
run: yarn prettier --check .

lint:
runs-on: ubuntu-latest
steps:
- name: Step 1 - Check out main branch
uses: actions/checkout@v3

- name: Step 2 - Install project
run: yarn install --frozen-lockfile

- name: Step 3 - Check correct linting
run: yarn eslint .
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.env
.env.*
!.env.example
.idea
.vscode
.DS_Store

dist
target
node_modules
node.json
package-lock.json

yarn-error.log
7 changes: 7 additions & 0 deletions .mocharc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"extensions": ["ts"],
"test": ["tests/**.ts"],
"node-option": ["loader=ts-node/esm"],
"slow": 5000,
"timeout": 300000
}
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
18
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
cairo
venv
target
deployments/artifacts
dist
.github
tests-integration/fixtures
starknet-devnet-rs
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"arrowParens": "always",
"useTabs": false,
"trailingComma": "all",
"singleQuote": false,
"semi": true,
"printWidth": 120,
"plugins": ["prettier-plugin-organize-imports"]
}
674 changes: 674 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Introduction

This collection of scripts is designed to execute stress tests on the Starknet network.
It utilizes the latest Argent account available, employing the new signature format.
The last successful test was conducted on Sepolia using the ClassHash 0x7e05c2de6c722119fa8fc7cd3571ad2bb286a2c0a84d5b6575b4e4ea4290d9d.

To begin, you'll need to deploy and fund a series of accounts. These accounts will subsequently be utilized for ERC20 transfers.

# Installation

Start by installing the required dependencies using your preferred package manager:

```bash
yarn
```

Then, complete the setup by populating the [env file](.env.example) and renaming it to .env:

```bash
mv .env.example .env
```

# Running

The repository includes the following scripts:

- [deploy-accounts](./scripts/deploy-accounts.ts): Deploys and saves the accounts in two files.
- [fund-accounts](./scripts/fund-accounts.ts): Verifies the balances of all accounts and transfers funds to those with balances below the threshold using a multicall
- [stress-test](./scripts/stress-test.ts): Executes the stress test
- [check-tx-status](./scripts/check-tx-status.ts): Check the transactions statuses. Caution: this may overload the RPC

To be able to perform the stress test, you'll need some accounts to be able to do those transfers.
Begin by reviewing [deploy-account script](./scripts/deploy-accounts.ts). Make sure all the constants suits your need. Execute the script using `yarn run deploy-accounts`. This will create two environment files used by the stress-test script.
**Please note, this setup is a one-time requirement.**

Once all accounts are deployed, and if necessary, you can check all accounts have enough funds to perform the transfers using `yarn run fund-accounts`. This will check the balance of every account and fund the one for which the balance is under the defined threshold.

Now you can perform the actual stress test. First check that the correct parameters are defined according to your needs:

- Desired Transactions Per Second (TPS)
- Duration of the script's execution
- Ratio of transaction versions V3 (STRK as fee token) compared to transaction V2 (ETH as fee token)
- Current maxFee and L1 gas allowance per transaction (provide additional margin to prevent transaction reverted).

Then you can run the script using `yarn start > log`.
Due to the extensive logging, it's advisable to redirect the output to a file for post-analysis.
Upon completion (or if interrupted with CTRL+C), the
Upon completion (or if interrupted with CTRL+C), the script will display statistics and create a new file (.env.txs) containing all transaction hashes. To check these transaction statuses, run `yarn check-tx-status`. Caution: this script may overload the RPC.

## How many accounts do I need to deploy?

Let's assume you'd wanna hit 50 TPS.
Given that a transaction takes approximately 1.15s (today, according to [voyager](https://voyager.online/analytics)) but sometimes it can take longer. Let's be safe and assume 10s average time. You'll need then to deploy `AVERAGE_TIME * TPS`:
10 \* 50 = 500 accounts
For a stress test at 50 TPS, with 80% of transactions using STRK as the fee token (TX_V3), deploy 400 TX_V3 accounts and 100 TX_V2 accounts. Adjust the ratio as necessary.

## How much I should fund each account?

This depends entirely on the current network costs. If a simple transfer costs X (transfer + fee), plan accordingly.
If you plan on hitting 50 TPS for 10 minutes, the total amount of transactions will be: `TPS * TIME_IN_SECONDS`:
50 \* (60 \* 10) = 30.000 transactions
If you have 500 accounts deployed to perform these transactions each accounts will need to do 60 transfers (30.000 / 500).
Assuming that a transfer costs 0.0000001ETH (random number).
You need to fund each account with AT LEAST 0.0000001ETH \* 60 = 0.000006ETH.
Apply a similar approach for STRK accounts.
174 changes: 174 additions & 0 deletions lib/accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {
Abi,
Account,
AllowArray,
CairoOption,
CairoOptionVariant,
Call,
CallData,
Contract,
DeployAccountContractPayload,
DeployContractResponse,
InvokeFunctionResponse,
RPC,
UniversalDetails,
hash,
num,
uint256,
} from "starknet";
import {
ArgentSigner,
KeyPair,
declareContract,
ethAddress,
loadContract,
provider,
randomStarknetKeyPair,
strkAddress,
} from ".";
export class ArgentAccount extends Account {
// Increase the gas limit by 30% to avoid failures due to gas estimation being too low with tx v3 and transactions the use escaping
override async deployAccount(
payload: DeployAccountContractPayload,
details?: UniversalDetails,
): Promise<DeployContractResponse> {
details ||= {};
if (!details.skipValidate) {
details.skipValidate = false;
}
return super.deployAccount(payload, details);
}

override async execute(
calls: AllowArray<Call>,
abis?: Abi[],
details: UniversalDetails = {},
): Promise<InvokeFunctionResponse> {
details ||= {};
if (!details.skipValidate) {
details.skipValidate = false;
}
if (details.resourceBounds) {
return super.execute(calls, abis, details);
}
const estimate = await this.estimateFee(calls, details);
return super.execute(calls, abis, {
...details,
resourceBounds: {
...estimate.resourceBounds,
l1_gas: {
...estimate.resourceBounds.l1_gas,
max_amount: num.toHexString(num.addPercent(estimate.resourceBounds.l1_gas.max_amount, 30)),
},
},
});
}
}

export interface ArgentWallet {
account: ArgentAccount;
accountContract: Contract;
owner: KeyPair;
}

export const deployer = (() => {
const address = process.env.ADDRESS;
const privateKey = process.env.PRIVATE_KEY;
if (address && privateKey) {
return new Account(provider, address, privateKey, undefined, RPC.ETransactionVersion.V2);
}
throw new Error("Missing deployer address or private key, please set ADDRESS and PRIVATE_KEY env variables.");
})();

console.log("Deployer:", deployer.address);

async function deployAccountInner(params: DeployAccountParams): Promise<
DeployAccountParams & {
account: Account;
classHash: string;
owner: KeyPair;
guardian?: KeyPair;
salt: string;
transactionHash: string;
}
> {
const finalParams = {
...params,
classHash: params.classHash ?? (await declareContract("ArgentAccount")),
salt: params.salt ?? num.toHex(randomStarknetKeyPair().privateKey),
owner: params.owner ?? randomStarknetKeyPair(),
useTxV3: params.useTxV3 ?? false,
selfDeploy: params.selfDeploy ?? false,
};
const guardian = finalParams.guardian
? finalParams.guardian.signerAsOption
: new CairoOption(CairoOptionVariant.None);
const constructorCalldata = CallData.compile({ owner: finalParams.owner.signer, guardian });

const { classHash, salt } = finalParams;
const contractAddress = hash.calculateContractAddressFromHash(salt, classHash, constructorCalldata, 0);
const fundingCall = finalParams.useTxV3
? await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e16, "STRK") // 0.01 STRK
: await fundAccountCall(contractAddress, finalParams.fundingAmount ?? 1e18, "ETH"); // 1 ETH
const calls = fundingCall ? [fundingCall] : [];

const transactionVersion = finalParams.useTxV3 ? RPC.ETransactionVersion.V3 : RPC.ETransactionVersion.V2;
const signer = new ArgentSigner(finalParams.owner, finalParams.guardian);
const account = new ArgentAccount(provider, contractAddress, signer, "1", transactionVersion);

let transactionHash;
if (finalParams.selfDeploy) {
const response = await deployer.execute(calls);
await provider.waitForTransaction(response.transaction_hash);
const { transaction_hash } = await account.deploySelf({ classHash, constructorCalldata, addressSalt: salt });
transactionHash = transaction_hash;
} else {
const udcCalls = deployer.buildUDCContractPayload({ classHash, salt, constructorCalldata, unique: false });
const { transaction_hash } = await deployer.execute([...calls, ...udcCalls]);
transactionHash = transaction_hash;
}

await provider.waitForTransaction(transactionHash);
return { ...finalParams, account, transactionHash };
}

export type DeployAccountParams = {
useTxV3?: boolean;
classHash?: string;
owner?: KeyPair;
guardian?: KeyPair;
salt?: string;
fundingAmount?: number | bigint;
selfDeploy?: boolean;
};

export async function deployAccountWithoutGuardian(
params: Omit<DeployAccountParams, "guardian"> = {},
): Promise<ArgentWallet & { transactionHash: string }> {
const { account, owner, transactionHash } = await deployAccountInner(params);
const accountContract = await loadContract(account.address);
accountContract.connect(account);
return { account, accountContract, owner, transactionHash };
}

export async function fundAccount(recipient: string, amount: number | bigint, token: "ETH" | "STRK") {
const call = await fundAccountCall(recipient, amount, token);
const response = await deployer.execute(call ? [call] : []);
await provider.waitForTransaction(response.transaction_hash);
}

export async function fundAccountCall(
recipient: string,
amount: number | bigint,
token: "ETH" | "STRK",
): Promise<Call | undefined> {
if (amount <= 0n) {
return;
}
const contractAddress = { ETH: ethAddress, STRK: strkAddress }[token];
if (!contractAddress) {
throw new Error(`Unsupported token ${token}`);
}
const calldata = CallData.compile([recipient, uint256.bnToUint256(amount)]);
return { contractAddress, calldata, entrypoint: "transfer" };
}
Loading

0 comments on commit 3515ae8

Please sign in to comment.