diff --git a/.gitignore b/.gitignore index ce12a8c86..ddf667e4d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,6 @@ pkg /web/deps /web/priv/static/assets/ node_modules -package-lock.json -package.json # Generated solution binaries *.bin diff --git a/test/stress/.env.devnet b/test/stress/.env.devnet new file mode 100644 index 000000000..f2383c484 --- /dev/null +++ b/test/stress/.env.devnet @@ -0,0 +1,5 @@ +RPC_URL='https://localhost:8545' +ZK_ARCADE_URL="https://localhost:4005" +BATCHER_URL="wss://localhost:8080" +BATCHER_PAYMENT_SERVICE_ADDR="0x7bc06c482DEAd17c0e297aFbC32f6e63d3846650" +CHAIN_NAME='anvil' diff --git a/test/stress/.env.sepolia b/test/stress/.env.sepolia new file mode 100644 index 000000000..1ba1e4a89 --- /dev/null +++ b/test/stress/.env.sepolia @@ -0,0 +1,5 @@ +RPC_URL='https://ethereum-sepolia-rpc.publicnode.com' +ZK_ARCADE_URL="https://test.zkarcade.com" +BATCHER_URL="wss://sepolia.batcher.alignedlayer.com" +BATCHER_PAYMENT_SERVICE_ADDR="0x403dE630751e148bD71BFFcE762E5667C0825399" +CHAIN_NAME='sepolia' diff --git a/test/stress/README.md b/test/stress/README.md new file mode 100644 index 000000000..ac5602392 --- /dev/null +++ b/test/stress/README.md @@ -0,0 +1,101 @@ +# ZK Arcade - Stress Testing Suite + +This directory contains a set of tools used to perform stress testing on the ZK Arcade backend, which also tests Aligned. The test consists of generating parity proofs for each address and then sending them to the backend via HTTP POST requests. + +## Description + +The stress test simulates a multi-user load by sending proofs to the backend concurrently. This allows us to validate the system’s scalability and how it reacts under heavy load scenarios. + +## Test Architecture + +### Main files + +#### 1. `src/stress_test.js` + +Contains the main logic for each user, which consists of: + + 1. Obtaining the CSRF token and establishing the session + 2. Signing the service agreement by sending a POST request to the backend + 3. Making a deposit in Aligned if the testing chain is not the Sepolia network + 4. Validating the agreement status (this check also keeps the address stored in the backend session) + 5. Generating the Circom proof (a computationally heavy task) + 6. Sending the proof to the system + +#### 2. `src/circom_proof_generator.js` + +Generates the verification data to be sent to the batcher, using Circom and the `snark.js` library to generate the inner proof from the solution. + +#### 3. `src/aligned.js` + +Handles deposits to the Aligned batcher payment service contract. + +#### 4. `src/utils/sign_agreement.js` + +Contains the logic to sign the service agreement and send it to the ZK Arcade backend. + +#### 5. Other utility files + +- **`src/utils/cookie_utils.js`**: Manages cookies between HTTP calls to preserve each session’s data. +- **`src/constants.js`**: Contains configuration specific to the selected network. +- **`src/utils/estimate_max_fee.js`**: Contains logic to estimate the maximum fee sent to the batcher in the proof submission message. +- **`src/utils/batcher.js`**: Retrieves the user’s nonce from the batcher via an RPC call. + +## Data Configuration + +### Testing addresses + +The system supports two file formats for providing funded test accounts: + +- The first is a CSV file (`data/sepolia_rich_accounts.csv`) with the private key and address in the header. +- The second is a JSON file (`data/rich_accounts.json`), which consists of an array of objects containing `address` and `privateKey` fields. + +### Game solution file + +The test requires a solution to generate the Circom proof that certifies users have completed the game. +The solution is read from the `data/solution.json` file, which contains the level boards and user positions for each level in JSON format. + +You can copy this data from the browser’s local storage variable `parity-game-data`, which contains the solution played by the user for that specific game configuration. + +## Environment Configuration + +### Configuration variables (`constants.js`) + +As mentioned before, this file contains the network-specific variables for the chain being tested. For example, the Sepolia network configuration looks like this: + +```javascript +RPC_URL = 'https://ethereum-sepolia-rpc.publicnode.com' +ZK_ARCADE_URL = "https://test.zkarcade.com" +BATCHER_URL = "wss://sepolia.batcher.alignedlayer.com" +BATCHER_PAYMENT_SERVICE_ADDR = "0x403dE630751e148bD71BFFcE762E5667C0825399" +USED_CHAIN = sepolia +``` + +TODO: Move these values into a .env file specific to each network (at least Sepolia and Devnet). + +### Dependencies + +The test requires the following Node.js dependencies: + +- `viem` +- `snarkjs` +- `axios/undici` +- `csv-parse` +- `sha3` + +## Execution + +1. Install the project dependencies: + + ```bash + npm install + ``` + +2. Add your funded account for the tested network in either `data/sepolia_rich_accounts.csv` or `data/rich_accounts.json`. + - If needed, you can generate test accounts for Devnet using the generate_rich_wallets.sh script: `./generate_rich_wallets.sh ` +3. Make sure that `data/solution.json` contains a valid solution. Note that it does not need to be updated between runs. + +4. Run the Test + + ```bash + node src/stress_test.js + ``` diff --git a/test/stress/data/rich_accounts.json b/test/stress/data/rich_accounts.json new file mode 100644 index 000000000..b78e01215 --- /dev/null +++ b/test/stress/data/rich_accounts.json @@ -0,0 +1,402 @@ +[ + { + "address": "0xE7bA00184Bc1f5f202268786Eb6eC836ca253965", + "privateKey": "0xe02c7c5d82857c6b24b53dddd4991c097579c64188ba3d6932a067632a55c88a" + }, + { + "address": "0x0bBeD11A554ebd1eC438429DfBa88380041aD63c", + "privateKey": "0x98f7595396aa076b8c85b716d85629cd0964976308108662ed188d773e5abcec" + }, + { + "address": "0x7429c25A986F5d97C15267BB944118d1f31b02BB", + "privateKey": "0x530f1cdfbad9e0cf993c9c591bf9fc4c9198733ee4757715463c0cb3f5149421" + }, + { + "address": "0x8498e539671b60A98aBfE89eD50D3231FCD13983", + "privateKey": "0x98d69195d688b2856066b0c9201a14dc697e45025fc7b9ad1d48f23edd71273f" + }, + { + "address": "0x6A29A470417ae09111D8c07851D790C76E581A05", + "privateKey": "0x3216b11fbfaf149814db6e2ad450e78f6067107383f3bef1100c138283329ef4" + }, + { + "address": "0x8168ed59cab614f1d4b33EF540d7004E45Dc07dd", + "privateKey": "0x030a3bf596bf838b6c88e9d589fc22cc55c24e617e2cd3c941de4e8072d531be" + }, + { + "address": "0xDCD9a55Fc47310e8cd2A7255e5db726906b43D37", + "privateKey": "0xa8dd43821ac4ec55bb7bf74fe3d41c0852e508b8765acb56deca07d419a8a6a0" + }, + { + "address": "0x45aCD70D522C9539C679E248FD0731922D4A3BF7", + "privateKey": "0x844b4ba5a294d3d19a022886114ee8b92cbaadc1cdc0807aa9a6b6a0dc31ab23" + }, + { + "address": "0xA161FfB7527f571cF7aE7f51DbC7A3C5A81A9870", + "privateKey": "0x098eac4bcc5a0881d36555c4bb554d0af9e277288a9ba012ea7d668d991c13f9" + }, + { + "address": "0x33Ab8dD6FC9BAEbb850A84E648c0F1c0af10287E", + "privateKey": "0xc43002ff3d1ed9b8282638f255c1706db3b5b6dea83bcc49206c2d61ef59b2a2" + }, + { + "address": "0x9d06843Ae3540765c4500E6da00a5b05faaD96C0", + "privateKey": "0x09be97eda5c79b68dd43c76cb31b55614e12cdce8015d9ddc70c819b73eb869b" + }, + { + "address": "0x5BB497Be3B7CbCEE5bD8D679dCBc359f5f4dF5b1", + "privateKey": "0xa5756a153923c5991569cbcff1ea4662f7bf0c2eede6a37a6094679a2bd4e241" + }, + { + "address": "0x7c237F3EcDe6a85D997534173C103fa776B52e34", + "privateKey": "0x0a42cc5acb7ff8b39f3bd1fff10bd90f4f0909aff08395b53d06548b3bf0ec57" + }, + { + "address": "0xBD85E4f25badE390E407d30F6AF93DB4cC669cb9", + "privateKey": "0x92b4e18fb83b14a9102cccab5a151389c5c7f64244915c0743bb32a381b6641f" + }, + { + "address": "0xF0d467087F7A0Bb464F1eC978620143899328e61", + "privateKey": "0x73ad0c9b91aa245fc11e5e26067786d4eff2212a3d949557af50f45a4a1828c8" + }, + { + "address": "0xd549380366DaFbBdb317DBF4DD4d57D8B2A2A3dC", + "privateKey": "0x0ee6fd0806e739a448f17e70ec596b74707f70801e933bbf7b1d833d257c3f76" + }, + { + "address": "0x0F0eBfFab29cabc14DC2dA79c1c427f978a17df1", + "privateKey": "0x090e574e8eb9d975d0bdf163133ac1ebe24c3d8af01aa8c50a73c9310985b1e0" + }, + { + "address": "0x74C76872bB4432C143Df3EDbc9fBe49f34Cd39D4", + "privateKey": "0x63daef1690659470ce1f4e89c49f2e6d2bc6e1532de7b0b742af2d59cdc95ecf" + }, + { + "address": "0xF2B152D45E6029FA36cD128CFc291a002dc2e645", + "privateKey": "0x211b593a240d457002ddb342c5f7ada87253665f1a04e2afd2b1ebb582221292" + }, + { + "address": "0x164599dE31cf2D63D3c582Aa9641605eC7c7aa9c", + "privateKey": "0xadb5ae9da7113e2000d2676d9e42faae73994655ad79e56438a9127322256966" + }, + { + "address": "0xF16146E9d6E2CA506d9C2A8cE93e3bc05AD26465", + "privateKey": "0xfbfe193289ed9a385e120c6ccfd844a1ac14dd8bbfd390996baeb790335b0bb8" + }, + { + "address": "0x9c5f7582f169316fd0645cDe32634d19B5Fd7C55", + "privateKey": "0x60915c726a98884974f68986de6b872450f6284d25d362812fb80eb0a86904c2" + }, + { + "address": "0x4e86bD238025De00aD4e9Accb196370B701605c3", + "privateKey": "0x854912ab70134d2412fe8df5d7bc7f5d548e30bbf51e72cd1f03f7b4f9157fb2" + }, + { + "address": "0x068Ee2abCB74BC4EAc6d272FaE991365688fd12B", + "privateKey": "0x9786578b6ea9b1f29dcc060bcb31bdc53366b77bfd5d136d3a69fcb40e0586c1" + }, + { + "address": "0x0e00A88ad0E8Ea28c8FD45Cbac497589A6f5B317", + "privateKey": "0xd849c2fb8feaf5e2bc78b57fbba93624c77f82ccbab33a01825ec799a731ba2b" + }, + { + "address": "0x3227a17297577ac5f2E514D493AE145ca951e7b8", + "privateKey": "0xea7cfee60cadfff9c80ca0de71ad9a9ecc829faf0641c7b3f06bf67f0d76d5b5" + }, + { + "address": "0xD42DE54e5528f5AEeEf111a79Bf4065e4749fdca", + "privateKey": "0x0d50a9cae07893d5a76f007d83b3e8935e6d09a7c3d77e2075a00b38717bdfca" + }, + { + "address": "0xA06E445A780fa13000b21DeB5910f181F2c0Cf44", + "privateKey": "0x795fa17f6dd7731e88539221e93e9b61e5bf9e588f8b06d28d561a3c1420c8c4" + }, + { + "address": "0xcaB7C4ec470d5BE111B28b7d816AB29154e1aA2b", + "privateKey": "0x4004061d045d1504d3097c68b2bdef14be584398e4f96e1014bc55620eb9a6b8" + }, + { + "address": "0xf7aD7E3472FE01277d2A1B2769701Fb42b7a3739", + "privateKey": "0xd6d620641b14827260d7a642ce42dfcef1d1d646d903d84cd78c9da99d6b3aa8" + }, + { + "address": "0xD6f550680a1b706971314c86F7A27A20Aed1Da87", + "privateKey": "0x077563c548b302e0e59349cc65c4929f2aab7abff372eb89675cd19bd53c7b57" + }, + { + "address": "0x9caFC29ca4346bE2b1Eda8cc5324Dd69B2F5E0E3", + "privateKey": "0x3ed41a84de5891b75bb81f554776436ca4cdefecca35fa0eee349e4bd23f05c8" + }, + { + "address": "0x3496fa164994aE8B1C96391e0cCcEAE5b9d9b20D", + "privateKey": "0x3256eef3fe8ac1254c70f4b30dce2b353b77320471de6d6e1f751aec574ade61" + }, + { + "address": "0xf719e8E44E84CE4De599f1896749C0e836AFf9a5", + "privateKey": "0x2ee4a5d6f935d7cdb218828423755bdf4f1b9c072552566d40dd46edb0dabf77" + }, + { + "address": "0x0c8FffDa7e093fD7a71179FE509c70f759D9c8a4", + "privateKey": "0xaf9182c1ab0e7feee343a8d64d7cb76ed97f9006cdecceb2a562b41eee61b653" + }, + { + "address": "0x58ecd43CE33Aa784c6D7ea3f7f2d7bdaB33E2218", + "privateKey": "0x2df0e994b7475c4af228bbd6d993bd4aa681ea3b445763fe21fc16b634943109" + }, + { + "address": "0xF8AbffAADb620420aCbdCF59887595cB655e383C", + "privateKey": "0xb93f66c1abd93c5617aad66d5ceebce30b1ace822aa98cd6fdb414d89974d261" + }, + { + "address": "0x62a32A252118195A057B81fE1D365ca17a5a668C", + "privateKey": "0x60b3d81d132014ebe071ce7726cd28677d5c6e00eb9b8617a1532c114fc0aff7" + }, + { + "address": "0x99Ec70cde89cE1250BcA1Ddb55BaF2995F76a1b2", + "privateKey": "0xd47b61a2ba74fc4a3640310d2d5b5d2a77da0a48c75f468d980ef57e9ca18a3c" + }, + { + "address": "0xac7335fD3bb52CB807B7E199ca79d26c6A6908Bb", + "privateKey": "0xb5a362c116e46fff93eb04d2518fac3d48bdfc6f0e2f8a9fbd0b522c2046a483" + }, + { + "address": "0x4848F3aFa7339DBcf06e05FBD2132BC96244C703", + "privateKey": "0x797022dbab60ca1ecc0ce245f5ba91457b9f4973c76cf816fe56f59b0c7c3ea6" + }, + { + "address": "0x16a7f96c3d62Bf93aCc04E473F264Ec4C52CA563", + "privateKey": "0xdb31b84ecf4ba75b9b8c246caac08d0671a2fb4667fd479c51ee85cf79d35bf8" + }, + { + "address": "0x1925E56d9b32787EA89572913e5bEBE6d86BE760", + "privateKey": "0xbbbdfba3d095267f8ba50e32fcd2e9502efd864dde84d6a9daa71e1f8ebba489" + }, + { + "address": "0x29aC90419798fB8a86624B79739c737624779C88", + "privateKey": "0xa88570b918bc3186856314dd6ae37fc5c858939104600a1bd4d00492b102cd64" + }, + { + "address": "0x3ad26ce5c6C30f4AE587671A3F4Dd0E33c364551", + "privateKey": "0x02cba8df8ba39cc3eef9bfcb5a418ca85d9e2e7a25f238a38ce82978971c350a" + }, + { + "address": "0x0c5Bc8d237bF65ea85DAec7cFc7b913c9d2B14CA", + "privateKey": "0x8399097df90add44ec344c1f25d698a20b12968bf3596ba378733bc73fc9ee79" + }, + { + "address": "0x1322ebE500E6EA1400E98c8bE7542B97Cb4b0638", + "privateKey": "0x9e85aca629035d597b2998a16bd5cac18db71ba3f1583cf3abca88f4504abbbd" + }, + { + "address": "0x09BD02ecC464cf07644f290889143c0622f61B03", + "privateKey": "0x4b1163c4c4eba85be3d1df813cd0af04445b28c7c8612d020f6a7d3bc753d57b" + }, + { + "address": "0x39Bd84f83a8ef6CF95C2768171e33b8a72C8B820", + "privateKey": "0x094662620a15cb9d31d9d51f5d17587c905e638c207439bde4944870fc51ea83" + }, + { + "address": "0x2368E5aDdE16dFb41a119770f8bC356Bdf618673", + "privateKey": "0x0509b251c2c329a9bcd09e4bc9af9dc8f7892ad5b32a6d85e04b1363c2dd9cf8" + }, + { + "address": "0x5621B8F3EB6E298c9b5A6B5eF2FF03B2251066AF", + "privateKey": "0xd0066f152c43c68a4b400e3d18cfee9a2b316a19684b03430f3a53e57436456d" + }, + { + "address": "0xF465F2ceDd2fEBEBdF19Fe87aD6f51ebDDccE69d", + "privateKey": "0xc27e4e572998af57654836f717433a19aba7fbdb27d690218f627228855f38ce" + }, + { + "address": "0xF222db04Bc3B21c90f1e30ccF66C703caABA9B7C", + "privateKey": "0xe0252a38224724db6ea661465addddc7e2231927bd950b5e43ab19ca0a80250f" + }, + { + "address": "0x32b2fc3eB57457B96A70C72DBF23c0A15728e112", + "privateKey": "0xb91d7a7c3f399fe6c9446155db2d3becff769a199a6b6d9d16a3c9bff528ed4c" + }, + { + "address": "0xCeBB851bb52A2BdE5B95Fbc06c832851D7e742c0", + "privateKey": "0x205dc1363881dead42d0def256920b9e0e3bc7424c3ae1f8843a721b92664949" + }, + { + "address": "0x3E77cC5895C64683d18002BEB7d01bE316384738", + "privateKey": "0x0392a777e9529dd085a1c0978fae4487d283ffed676f340449b97c15b9a6055b" + }, + { + "address": "0x8F7293fb990e9248f0F78510Fe92C3DBF733AdfF", + "privateKey": "0x53b9ed4c2ef57adf87f55f740a4b09e6940d85ad60a941e529a156e9b6076215" + }, + { + "address": "0xcEBE77517d92784fF3462d5F1B2aA53Dd8244f23", + "privateKey": "0x27ae11cf666850745bb06bea85588590b9fbd02a0846b53fb91387aa9876236c" + }, + { + "address": "0xf75394c2Fa4b7db9b32F4613C201390e7E1C89a7", + "privateKey": "0xce8ef5ddf99fb89004e2da5d2db062ad6f93ff6fe5ded433f0a12e80327092d4" + }, + { + "address": "0xfe6b3232eEf5e54aE81C2218455c96050f226CA1", + "privateKey": "0x9a0233c664b51d1cb7da94399eb64cc42b6031f88334c669d411f64d5769a0a6" + }, + { + "address": "0x25693EA6531F970C75E70ff72A31fFD508C649ff", + "privateKey": "0xe87a7b0174fa986438c24955a8a463421a729964cacebb063b7d8c55cf509c12" + }, + { + "address": "0xfB1710650E9e601c3C1aFE21cBAdF97D08209eE9", + "privateKey": "0x447bd0999d1d100fa72a9ad5d9cdb46c03aee58b7d898e4b8ff46944664f8723" + }, + { + "address": "0x83E0A55D5F4dBB9F359040aB82AB9cC74C64271a", + "privateKey": "0x42bf155eb98a5402c3168fc1716e1f0930d43680867ead2b00108b70fb06d00a" + }, + { + "address": "0x059B30e1720F8B6535F51E59Fd2951c4e1F8D357", + "privateKey": "0x44c339aa22f0afb4d54425672590075412dd8aa5348a25eff0965f2e11f700aa" + }, + { + "address": "0x989296bE11B2A6374782B13B93B74f8D110e410a", + "privateKey": "0x2fbedb65faad954cf39f0282455519ed9aa555cf2249d873ce8ca9e6c7bf9fcc" + }, + { + "address": "0xD9DEb61c5b885F0E1eFedaA60C2eE542e8b11a34", + "privateKey": "0x87b36eb1ab30879b60bd868d3bb3ab51f0b1f9ad9821a5bbeb2b88aad65349b7" + }, + { + "address": "0x808FDdD821a27bAd026842073eD708D0e3Fe9D24", + "privateKey": "0xfd0ce0184374c4bc4aed215613052e6fb5fc4e07506ee79e608ad18e19139320" + }, + { + "address": "0xBB78C90fCD2054ff2A6B1F2Cf09D8edb58482Fcb", + "privateKey": "0xb5f5dcdcee631909f67b00f188d7e9a1cd75b02e464b635364b9b4f7e3e2838a" + }, + { + "address": "0x21B1CcA460D249c012BD395b2da3c8dd5a186Cf9", + "privateKey": "0x00e51c621b689b993fa9932668eb27c1668d75642ae9ee64c8e69a9979f27e2d" + }, + { + "address": "0xDFbf88fed6B96814d2d0e4d2Eef93Bf85F7F3Fc3", + "privateKey": "0xc8864b51af7eccc70a2f102a96ba588c147d588cb0112044e7a2b164d68876eb" + }, + { + "address": "0x89DF9B4f530f61e8c3CFB8b8eA5C523eCac946dF", + "privateKey": "0xdbfe3104109ab7cb95f3464f51a15602f990f3d4f435dd72656818b16c1a1900" + }, + { + "address": "0x44Ad53c44941C433a40224d0D3243eD4E3cd373C", + "privateKey": "0xfe658c9557a1d535a48f4f011eec3f222c9223ef37f56ec9096a63b59f8b701a" + }, + { + "address": "0x1F47c6E7BaF5E9d47EF55C0C25217EAF505F1e7f", + "privateKey": "0x664a1686cbfda543904b82c6aa55407ed2d1f4a9dc1fd2a1675a4834794d40e1" + }, + { + "address": "0x39547659045671AE06856d6C68064a8F1B992CAb", + "privateKey": "0x64c38cade7879a6cbf330ea6f7e1728dfcac9c4e4b2206d4c4684a46dc9d71ee" + }, + { + "address": "0x62D96171CCdC6c0804034675AF29E8a674d1110A", + "privateKey": "0x900940287c41b9838948bf96c904f192a710ce674e35ad1d79741365bb5ad7e3" + }, + { + "address": "0x157774a8F6286dBdFc477ec30BB4219Ca9Ba2aE0", + "privateKey": "0x5a00ebef3c88a18690e80d09e72b690f669dc6c125271fd2a773b9272b1688e0" + }, + { + "address": "0x4ddA3eff0dD055a49BCc1B4e8D1e3F8958f9732F", + "privateKey": "0x4b9ed01359f5ac908fbef64d8b549a3962294745df88e9aa34c126708275b7a7" + }, + { + "address": "0xEbF0d9926C138a65DA25B7705B2E039728Dc0851", + "privateKey": "0xf226c27920133f308b420c4be1b0e61393756f61717e9829368c04881a7d61df" + }, + { + "address": "0x81268180d7De6D86353ee9994ddfDc4Ff1A6FBAA", + "privateKey": "0x2dce3acf4ede786df7bd21ae8ef84997a0c9728eeee2f2aec761a5ad9f259965" + }, + { + "address": "0xBD26e3E18c0aCeAd4BE31f31b5768dd255a0F8DF", + "privateKey": "0x89948778741c477e6eea241c338cb29df78ad8f0850275d2b1eab3ec1fe11b41" + }, + { + "address": "0x799eC57cf73C29009ea1c742e9777e9D754E0F4B", + "privateKey": "0x901fbba00170931c8b0918b3ae99cf6c3cecda69bf55cc573fda310ecc1ec17a" + }, + { + "address": "0xB81c9c9Bce4340337ED2DC6335910a41cF354012", + "privateKey": "0x3f6c9f41ab4306803eba8d4ad6982f248693706b121f43f89f192b61caa8fef6" + }, + { + "address": "0xC35F6aE36335E812b084D394f3F2883F7C5BfCA9", + "privateKey": "0x658196bb5e95bcb1a93d7ce5ca0835c3abb576a26d870d2f06411ca477e4e55c" + }, + { + "address": "0xf60eDB9436705bfb3d01531E18cF14Ce1152C719", + "privateKey": "0x48732098a4e0294d866a7f46e78707036613b286c40d4990d2e9aada324db11c" + }, + { + "address": "0xffcbbf028f58d8f816c5b02D2016990E95f374cD", + "privateKey": "0x06eaf478fbd39b381318a9ad0cf159c4267cf9a6b57fd077984691a0a832a31d" + }, + { + "address": "0xCBde81688482c0B61c07E24eA15069Cf6b18Bc72", + "privateKey": "0x74e30868357a42a8fda159b3282c68539caf2208b735195386e6afa31fc7c202" + }, + { + "address": "0xb8D1a3fea5904fbba7A6Bdcd245D287Fd087e963", + "privateKey": "0x55496d6782fe637cd9433e9e1e14c2e35c474907bc9e21fc70c518e387512e1f" + }, + { + "address": "0xB7F05B8Db6A46B32aF54e8420a945E7028Ad9106", + "privateKey": "0xf57da7d90633539def5f4055cad824140cac111b597d2631a89a71389829cde1" + }, + { + "address": "0x0711982d1514034cf24833c11C7b30cA7376Ef01", + "privateKey": "0xfd6f32dda05bc681418085cda78007be7cc091247dfe0eccd00a90b72d7a46df" + }, + { + "address": "0x0BB2577b71cb111Dfa8CD413B0A51C76bf25c427", + "privateKey": "0x64196692935875d5fbb6449a69ad9e1b1e1fbdfce7bd1471ea01c6075d2a683c" + }, + { + "address": "0xF54eE487D5C83f54736034AF987E47e108dB7eE5", + "privateKey": "0x548f064761ca1777b52598fd9cb450ac1b67422d5b7b3f30911ef050b0627c35" + }, + { + "address": "0x4391bEA4a32e7F60Bf6bD87A82e07E5A6428782B", + "privateKey": "0xc2b2626082fe196c646f7eda7bb417d7e529bd9a32b6e565d764812492b5b103" + }, + { + "address": "0x948B4CdEBD5f5e22532f4E1758a9cE1D7856422D", + "privateKey": "0x2ed2ad0036a58acba0ed2beed2f08793ec28232d2185790a24e7b5df7aad31d7" + }, + { + "address": "0x9cA8aAc3C4e9841fa48e51C36B2C155d31c5B2fb", + "privateKey": "0x8bf4808fdd6868f9d783305678871f095a5763a8053675c556226c07a2a472c8" + }, + { + "address": "0xd078df39B6a910BcDeD2D0cCa10557b9F56a6C0b", + "privateKey": "0x10b689ea9710ba6ab2d6ebe19c754d8dcc568d680cb8cd7e372c84d4a9c6d485" + }, + { + "address": "0x83eC3787D4fe4530e4930d9B4160b39499635f12", + "privateKey": "0x579c766d566bd711a59611a403d05a4870de76c5dd6ad4c6ae0f08ecb9407530" + }, + { + "address": "0x81104FB3CD1A0175D4BB3A0D44b990aCd4E65E21", + "privateKey": "0xc084be2f8f32160e68dbf405bb3f080a871d5b47117dfa41d584266dd7b793bc" + }, + { + "address": "0x737FA646419431E5E64aEf6db28fcD074e1BF530", + "privateKey": "0x1ab8e67ed2d3c1463d5080d95e0b5ab9b0b11ba2cf3313104a5ddfba21e8c97d" + }, + { + "address": "0x7436F6F02f0b6378D41dbc95fD7E968aAebeE94a", + "privateKey": "0x0de26850a8caf14eb0d85e4831053be3e14e690a0bc9953e37be352f3001b704" + }, + { + "address": "0x995022d9006621bA15A8f1d662B72C73f52fAD51", + "privateKey": "0xa15ed91d443d10ec3904cbaac3d6c3c10eb86f4c73e1f5db47649ea35a5fbe7f" + } +] diff --git a/test/stress/data/sepolia_rich_accounts.csv b/test/stress/data/sepolia_rich_accounts.csv new file mode 100644 index 000000000..dc47cb49b --- /dev/null +++ b/test/stress/data/sepolia_rich_accounts.csv @@ -0,0 +1 @@ +privateKey,address diff --git a/test/stress/data/solution.json b/test/stress/data/solution.json new file mode 100644 index 000000000..fa3a3ba6f --- /dev/null +++ b/test/stress/data/solution.json @@ -0,0 +1,280 @@ +{ + "levelsBoards": [ + [ + [ + 29, + 29, + 31, + 28, + 28, + 31, + 30, + 27, + 29 + ], + [ + 29, + 29, + 31, + 29, + 28, + 31, + 30, + 27, + 29 + ], + [ + 29, + 29, + 31, + 29, + 29, + 31, + 30, + 27, + 29 + ], + [ + 29, + 29, + 31, + 29, + 29, + 31, + 30, + 28, + 29 + ], + [ + 29, + 29, + 31, + 29, + 29, + 31, + 30, + 28, + 30 + ], + [ + 29, + 29, + 31, + 29, + 29, + 31, + 30, + 29, + 30 + ], + [ + 29, + 29, + 31, + 29, + 29, + 31, + 30, + 29, + 31 + ], + [ + 29, + 29, + 31, + 29, + 29, + 31, + 30, + 30, + 31 + ], + [ + 29, + 29, + 31, + 29, + 30, + 31, + 30, + 30, + 31 + ], + [ + 29, + 30, + 31, + 29, + 30, + 31, + 30, + 30, + 31 + ], + [ + 30, + 30, + 31, + 29, + 30, + 31, + 30, + 30, + 31 + ], + [ + 30, + 30, + 31, + 30, + 30, + 31, + 30, + 30, + 31 + ], + [ + 30, + 30, + 31, + 30, + 30, + 31, + 31, + 30, + 31 + ], + [ + 30, + 30, + 31, + 30, + 30, + 31, + 31, + 31, + 31 + ], + [ + 30, + 30, + 31, + 30, + 31, + 31, + 31, + 31, + 31 + ], + [ + 30, + 31, + 31, + 30, + 31, + 31, + 31, + 31, + 31 + ], + [ + 31, + 31, + 31, + 30, + 31, + 31, + 31, + 31, + 31 + ], + [ + 31, + 31, + 31, + 31, + 31, + 31, + 31, + 31, + 31 + ] + ] + ], + "userPositions": [ + [ + [ + 1, + 1 + ], + [ + 0, + 1 + ], + [ + 1, + 1 + ], + [ + 1, + 2 + ], + [ + 2, + 2 + ], + [ + 1, + 2 + ], + [ + 2, + 2 + ], + [ + 1, + 2 + ], + [ + 1, + 1 + ], + [ + 1, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 1 + ], + [ + 0, + 2 + ], + [ + 1, + 2 + ], + [ + 1, + 1 + ], + [ + 1, + 0 + ], + [ + 0, + 0 + ], + [ + 0, + 1 + ] + ] + ] +} diff --git a/test/stress/generate_rich_wallets.sh b/test/stress/generate_rich_wallets.sh new file mode 100755 index 000000000..76775b58a --- /dev/null +++ b/test/stress/generate_rich_wallets.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +RPC_URL="http://localhost:8545" +SENDER_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +AMOUNT="10ether" +NUM_ACCOUNTS=$1 +OUTPUT_FILE="data/rich_accounts.json" + +echo "[" > "$OUTPUT_FILE" + +echo "Generating $NUM_ACCOUNTS accounts and sending $AMOUNT to each one..." + +for i in $(seq 1 $NUM_ACCOUNTS); do + echo "Processing account $i/$NUM_ACCOUNTS..." + + WALLET_OUTPUT=$(cast wallet new) + + ADDRESS=$(echo "$WALLET_OUTPUT" | grep "Address:" | awk '{print $2}') + PRIVATE_KEY=$(echo "$WALLET_OUTPUT" | grep "Private key:" | awk '{print $3}') + + echo " Sending $AMOUNT a $ADDRESS..." + cast send "$ADDRESS" \ + --value "$AMOUNT" \ + --private-key "$SENDER_PRIVATE_KEY" \ + --rpc-url "$RPC_URL" > /dev/null 2>&1 + + if [ $? -eq 0 ]; then + echo " ✓ Successful transfer" + else + echo " ✗ Transfer error" + fi + + if [ $i -eq $NUM_ACCOUNTS ]; then + echo " {" >> "$OUTPUT_FILE" + echo " \"address\": \"$ADDRESS\"," >> "$OUTPUT_FILE" + echo " \"privateKey\": \"$PRIVATE_KEY\"" >> "$OUTPUT_FILE" + echo " }" >> "$OUTPUT_FILE" + else + echo " {" >> "$OUTPUT_FILE" + echo " \"address\": \"$ADDRESS\"," >> "$OUTPUT_FILE" + echo " \"privateKey\": \"$PRIVATE_KEY\"" >> "$OUTPUT_FILE" + echo " }," >> "$OUTPUT_FILE" + fi + + sleep 0.01 +done + +echo "]" >> "$OUTPUT_FILE" + +echo "" +echo "✓ Process completed!" +echo "Total $NUM_ACCOUNTS accounts have been saved to $OUTPUT_FILE" diff --git a/test/stress/package-lock.json b/test/stress/package-lock.json new file mode 100644 index 000000000..fc703788a --- /dev/null +++ b/test/stress/package-lock.json @@ -0,0 +1,1087 @@ +{ + "name": "stress", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "axios": "^1.12.2", + "cbor2": "^2.0.1", + "csv-parse": "^6.1.0", + "dotenv": "^17.2.3", + "fs": "^0.0.1-security", + "minimist": "^1.2.8", + "sha3": "^2.1.4", + "snarkjs": "^0.7.5", + "undici": "^7.16.0", + "viem": "^2.37.9" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.1.tgz", + "integrity": "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==", + "license": "MIT" + }, + "node_modules/@cto.af/wtf8": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@cto.af/wtf8/-/wtf8-0.0.2.tgz", + "integrity": "sha512-ATm4UQiKrdm5GnU6BvIwUDN+LDEtt23zuzKFpnfDT59ULAd0aMYm/nSFzbSO02garLcXumRC13PzNfa7BsfvSg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@iden3/bigarray": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@iden3/bigarray/-/bigarray-0.0.2.tgz", + "integrity": "sha512-Xzdyxqm1bOFF6pdIsiHLLl3HkSLjbhqJHVyqaTxXt3RqXBEnmsUmEW47H7VOi/ak7TdkRpNkxjyK5Zbkm+y52g==", + "license": "GPL-3.0" + }, + "node_modules/@iden3/binfileutils": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@iden3/binfileutils/-/binfileutils-0.0.12.tgz", + "integrity": "sha512-naAmzuDufRIcoNfQ1d99d7hGHufLA3wZSibtr4dMe6ZeiOPV1KwOZWTJ1YVz4HbaWlpDuzVU72dS4ATQS4PXBQ==", + "license": "GPL-3.0", + "dependencies": { + "fastfile": "0.0.20", + "ffjavascript": "^0.3.0" + } + }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/abitype": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz", + "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3.22.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", + "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bfj": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/bfj/-/bfj-7.1.0.tgz", + "integrity": "sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==", + "license": "MIT", + "dependencies": { + "bluebird": "^3.7.2", + "check-types": "^11.2.3", + "hoopy": "^0.1.4", + "jsonpath": "^1.1.1", + "tryer": "^1.0.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/blake2b-wasm": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz", + "integrity": "sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==", + "license": "MIT", + "dependencies": { + "b4a": "^1.0.1", + "nanoassert": "^2.0.0" + } + }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/cbor2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cbor2/-/cbor2-2.0.1.tgz", + "integrity": "sha512-9bE8+tueGxONyxpttNKkAKKcGVtAPeoSJ64AjVTTjEuBOuRaeeP76EN9BbmQqkz1ZeTP0QPvksNBKwvEutIUzQ==", + "license": "MIT", + "dependencies": { + "@cto.af/wtf8": "0.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/check-types": { + "version": "11.2.3", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz", + "integrity": "sha512-+67P1GkJRaxQD6PKK0Et9DhwQB+vGg3PM5+aavopCpZT1lj9jeqfvpgTLAWErNj8qApkkmXlu/Ug74kmhagkXg==", + "license": "MIT" + }, + "node_modules/circom_runtime": { + "version": "0.1.28", + "resolved": "https://registry.npmjs.org/circom_runtime/-/circom_runtime-0.1.28.tgz", + "integrity": "sha512-ACagpQ7zBRLKDl5xRZ4KpmYIcZDUjOiNRuxvXLqhnnlLSVY1Dbvh73TI853nqoR0oEbihtWmMSjgc5f+pXf/jQ==", + "license": "Apache-2.0", + "dependencies": { + "ffjavascript": "0.3.1" + }, + "bin": { + "calcwit": "calcwit.js" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/csv-parse": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-6.1.0.tgz", + "integrity": "sha512-CEE+jwpgLn+MmtCpVcPtiCZpVtB6Z2OKPTr34pycYYoL7sxdOkXDdQ4lRiw6ioC0q6BLqhc6cKweCVvral8yhw==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fastfile": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/fastfile/-/fastfile-0.0.20.tgz", + "integrity": "sha512-r5ZDbgImvVWCP0lA/cGNgQcZqR+aYdFx3u+CtJqUE510pBUVGMn4ulL/iRTI4tACTYsNJ736uzFxEBXesPAktA==", + "license": "GPL-3.0" + }, + "node_modules/ffjavascript": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.3.1.tgz", + "integrity": "sha512-4PbK1WYodQtuF47D4pRI5KUg3Q392vuP5WjE1THSnceHdXwU3ijaoS0OqxTzLknCtz4Z2TtABzkBdBdMn3B/Aw==", + "license": "GPL-3.0", + "dependencies": { + "wasmbuilder": "0.0.16", + "wasmcurves": "0.2.2", + "web-worker": "1.2.0" + } + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==", + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoopy": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", + "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/isows": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz", + "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", + "license": "MIT" + }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "license": "MIT", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/logplease": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/logplease/-/logplease-1.2.15.tgz", + "integrity": "sha512-jLlHnlsPSJjpwUfcNyUxXCl33AYg2cHhIf9QhGL2T4iPT0XPB+xP1LRKFPgIg1M/sg9kAJvy94w9CzBNrfnstA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/nanoassert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-2.0.0.tgz", + "integrity": "sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==", + "license": "ISC" + }, + "node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ox": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.6.tgz", + "integrity": "sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/r1csfile": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/r1csfile/-/r1csfile-0.0.48.tgz", + "integrity": "sha512-kHRkKUJNaor31l05f2+RFzvcH5XSa7OfEfd/l4hzjte6NL6fjRkSMfZ4BjySW9wmfdwPOtq3mXurzPvPGEf5Tw==", + "license": "GPL-3.0", + "dependencies": { + "@iden3/bigarray": "0.0.2", + "@iden3/binfileutils": "0.0.12", + "fastfile": "0.0.20", + "ffjavascript": "0.3.0" + } + }, + "node_modules/r1csfile/node_modules/ffjavascript": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ffjavascript/-/ffjavascript-0.3.0.tgz", + "integrity": "sha512-l7sR5kmU3gRwDy8g0Z2tYBXy5ttmafRPFOqY7S6af5cq51JqJWt5eQ/lSR/rs2wQNbDYaYlQr5O+OSUf/oMLoQ==", + "license": "GPL-3.0", + "dependencies": { + "wasmbuilder": "0.0.16", + "wasmcurves": "0.2.2", + "web-worker": "1.2.0" + } + }, + "node_modules/sha3": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/sha3/-/sha3-2.1.4.tgz", + "integrity": "sha512-S8cNxbyb0UGUM2VhRD4Poe5N58gJnJsLJ5vC7FYWGUmGhcsj4++WaIOBFVDxlG0W3To6xBuiRh+i0Qp2oNCOtg==", + "license": "MIT", + "dependencies": { + "buffer": "6.0.3" + } + }, + "node_modules/snarkjs": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/snarkjs/-/snarkjs-0.7.5.tgz", + "integrity": "sha512-h+3c4rXZKLhLuHk4LHydZCk/h5GcNvk5GjVKRRkHmfb6Ntf8gHOA9zea3g656iclRuhqQ3iKDWFgiD9ypLrKiA==", + "license": "GPL-3.0", + "dependencies": { + "@iden3/binfileutils": "0.0.12", + "bfj": "^7.0.2", + "blake2b-wasm": "^2.4.0", + "circom_runtime": "0.1.28", + "ejs": "^3.1.6", + "fastfile": "0.0.20", + "ffjavascript": "0.3.1", + "js-sha3": "^0.8.0", + "logplease": "^1.2.15", + "r1csfile": "0.0.48" + }, + "bin": { + "snarkjs": "build/cli.cjs" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "license": "MIT", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/tryer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", + "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", + "license": "MIT" + }, + "node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", + "integrity": "sha512-QEg3HPMll0o3t2ourKwOeUAZ159Kn9mx5pnzHRQO8+Wixmh88YdZRiIwat0iNzNNXn0yoEtXJqFpyW7eM8BV7g==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/viem": { + "version": "2.37.9", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.37.9.tgz", + "integrity": "sha512-XXUOE5yJcjr9/M9kRoQcPMUfetwHprO9aTho6vNELjBKJIBx7rYq1fjvBw+xEnhsRjhh5lsORi6B0h8fYFB7NA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@noble/curves": "1.9.1", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0", + "abitype": "1.1.0", + "isows": "1.0.7", + "ox": "0.9.6", + "ws": "8.18.3" + }, + "peerDependencies": { + "typescript": ">=5.0.4" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/wasmbuilder": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/wasmbuilder/-/wasmbuilder-0.0.16.tgz", + "integrity": "sha512-Qx3lEFqaVvp1cEYW7Bfi+ebRJrOiwz2Ieu7ZG2l7YyeSJIok/reEQCQCuicj/Y32ITIJuGIM9xZQppGx5LrQdA==", + "license": "GPL-3.0" + }, + "node_modules/wasmcurves": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/wasmcurves/-/wasmcurves-0.2.2.tgz", + "integrity": "sha512-JRY908NkmKjFl4ytnTu5ED6AwPD+8VJ9oc94kdq7h5bIwbj0L4TDJ69mG+2aLs2SoCmGfqIesMWTEJjtYsoQXQ==", + "license": "GPL-3.0", + "dependencies": { + "wasmbuilder": "0.0.16" + } + }, + "node_modules/web-worker": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.2.0.tgz", + "integrity": "sha512-PgF341avzqyx60neE9DD+XS26MMNMoUQRz9NOZwW32nPQrF6p77f1htcnjBSEV8BGMKZ16choqUG4hyI0Hx7mA==", + "license": "Apache-2.0" + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/test/stress/package.json b/test/stress/package.json new file mode 100644 index 000000000..ffabc5f63 --- /dev/null +++ b/test/stress/package.json @@ -0,0 +1,15 @@ +{ + "type": "module", + "dependencies": { + "axios": "^1.12.2", + "cbor2": "^2.0.1", + "csv-parse": "^6.1.0", + "dotenv": "^17.2.3", + "fs": "^0.0.1-security", + "minimist": "^1.2.8", + "sha3": "^2.1.4", + "snarkjs": "^0.7.5", + "undici": "^7.16.0", + "viem": "^2.37.9" + } +} diff --git a/test/stress/src/aligned.js b/test/stress/src/aligned.js new file mode 100644 index 000000000..215ab8c41 --- /dev/null +++ b/test/stress/src/aligned.js @@ -0,0 +1,36 @@ +import { createWalletClient, createPublicClient, http, parseAbi, parseEther } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' + +import { RPC_URL, BATCHER_PAYMENT_SERVICE_ADDR, USED_CHAIN } from './constants.js' + +export async function depositIntoAligned(privateKey, valueEth = '0.1') { + const account = privateKeyToAccount(privateKey) + const publicClient = createPublicClient({ chain: USED_CHAIN, transport: http(RPC_URL) }) + const walletClient = createWalletClient({ account, chain: USED_CHAIN, transport: http(RPC_URL) }) + + const hash = await walletClient.sendTransaction({ + to: BATCHER_PAYMENT_SERVICE_ADDR, + value: parseEther(valueEth), + }) + + const receipt = await publicClient.waitForTransactionReceipt({ hash }) + + return { hash, receipt } +} + +export async function readAlignedBalance(userAddress) { + const publicClient = createPublicClient({ chain: USED_CHAIN, transport: http(RPC_URL) }) + + const abi = parseAbi([ + 'function user_balances(address account) view returns (uint256)' + ]) + + const balance = await publicClient.readContract({ + address: BATCHER_PAYMENT_SERVICE_ADDR, + abi, + functionName: 'user_balances', + args: [userAddress], + }) + + return balance +} diff --git a/test/stress/src/batcher.js b/test/stress/src/batcher.js new file mode 100644 index 000000000..cf2086023 --- /dev/null +++ b/test/stress/src/batcher.js @@ -0,0 +1,50 @@ + +import WebSocket from "ws"; +import { encode as cborEncode, decode as cborDecode } from 'cbor2'; +import { hexToBigInt } from "viem"; + +export async function getBatcherNonce(batcher_url, address, idx) { + return new Promise((resolve, reject) => { + if (!address) { + return reject(new Error("No address provided")); + } + + const ws = new WebSocket(batcher_url); + ws.binaryType = "arraybuffer"; + + ws.onopen = () => { + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Established connection to the batcher to get nonce`); + } + }; + + ws.onmessage = (event) => { + try { + const cbor_data = event.data; + const data = cborDecode(new Uint8Array(cbor_data)); + + if (data?.ProtocolVersion) { + const message = { GetNonceForAddress: address }; + const encoded = cborEncode(message).buffer; + ws.send(encoded); + } else if (data?.Nonce) { + ws.close(); + resolve(hexToBigInt(data.Nonce)); + } else if (data?.EthRpcError || data?.InvalidRequest) { + ws.close(); + reject(new Error(JSON.stringify(data))); + } + } catch (e) { + ws.close(); + reject(e); + } + }; + + ws.onerror = () => { + ws.close(); + reject(new Error("WebSocket connection error")); + }; + + ws.onclose = () => {}; + }); +} diff --git a/test/stress/src/circom_proof_generator.js b/test/stress/src/circom_proof_generator.js new file mode 100644 index 000000000..a0e96d6ad --- /dev/null +++ b/test/stress/src/circom_proof_generator.js @@ -0,0 +1,289 @@ +import { getAddress, pad, toBytes, toHex, createWalletClient, http } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import * as snarkjs from "snarkjs"; +import { readFile } from "fs/promises"; +import { TextEncoder } from "util"; +import path from "path"; +import { Keccak } from "sha3"; + +import { estimateMaxFeeForBatchOfProofs } from "./utils/estimate_max_fee.js"; +import { getBatcherNonce } from "./batcher.js"; +import { BATCHER_PAYMENT_SERVICE_ADDR, RPC_URL, USED_CHAIN, BATCHER_URL } from "./constants.js"; + +const PARITY_MAX_MOVEMENTS = 55; +const MaxLevels = 3; + +const toBytesFromJSON = (obj) => + new TextEncoder().encode(JSON.stringify(obj)); + +const fetchTextAsBytes = async (filePath) => { + const absPath = path.resolve(filePath); + const text = await readFile(absPath, "utf-8"); + return new TextEncoder().encode(text); +}; + +function clonePos(p) { + return [p[0], p[1]]; +} + +function fillLevelElements( + positions, + boards +) { + const pos = positions.map(clonePos); + const brd = boards.map(row => row.slice()); + + const lastPos = pos[pos.length - 1] ?? [0, 0]; + const lastBrd = brd[brd.length - 1] ?? Array(9).fill(0); + + while (pos.length < PARITY_MAX_MOVEMENTS) pos.push(clonePos(lastPos)); + while (brd.length < PARITY_MAX_MOVEMENTS) brd.push([...lastBrd]); + + return { positions: pos, boards: brd }; +} + +function makeEmptyLevel() { + const zeroPos = [0, 0]; + const zeroBoard = Array(9).fill(0); + return { + positions: Array.from({ length: PARITY_MAX_MOVEMENTS }, () => + clonePos(zeroPos) + ), + boards: Array.from({ length: PARITY_MAX_MOVEMENTS }, () => [ + ...zeroBoard, + ]), + }; +} + +export async function generateCircomParityProof(user_address, userPositions, levelsBoards, privateKey, idx) { + let nonce = BigInt(0); + try { + nonce = await getBatcherNonce(BATCHER_URL, user_address, idx); + if (idx % 100 === 0) { + console.log(`[${user_address} - ${idx}] Nonce:`, nonce.toString()); + } + } catch (err) { + console.error(`[${user_address} - ${idx}] Error:`, err); + } + + const allUserPositions = []; + const allLevelsBoards = []; + + const usedLevels = Math.min(levelsBoards.length, MaxLevels); + + for (let i = 0; i < usedLevels; i++) { + const levelUserPositions = userPositions[i] ?? []; + const levelBoards = levelsBoards[i] ?? []; + + const { positions, boards } = fillLevelElements( + levelUserPositions, + levelBoards + ); + allUserPositions.push(positions); + allLevelsBoards.push(boards); + } + + while (allLevelsBoards.length < MaxLevels) { + const empty = makeEmptyLevel(); + allUserPositions.push(empty.positions); + allLevelsBoards.push(empty.boards); + } + + const input = { + levelsBoards: allLevelsBoards, + userPositions: allUserPositions, + userAddress: user_address, + }; + + const wasmPath = "../../web/priv/static/artifacts/parity.wasm"; + const zkeyPath = "../../web/priv/static/artifacts/parity_final.zkey"; + const vkeyPath = "../../web/priv/static/artifacts/verification_key.json"; + + const { proof, publicSignals } = await snarkjs.groth16.fullProve( + input, + wasmPath, + zkeyPath + ); + + const proofBytes = toBytesFromJSON(proof); + const publicInputsBytes = []; + publicSignals.forEach(pub => { + let number = pad(toHex(BigInt(pub)), { + dir: "left", + size: 32, + }); + let bytes = toBytes(number); + bytes.forEach(byte => publicInputsBytes.push(byte)); + }); + + const vKeyBytes = await fetchTextAsBytes(vkeyPath); + + // Create verification data + const verificationData = { + provingSystem: "CircomGroth16Bn256", + proof: Array.from(proofBytes), + publicInput: publicInputsBytes, + vmProgramCode: undefined, + verificationKey: Array.from(vKeyBytes), + proofGeneratorAddress: user_address, + }; + + const maxFee = await estimateMaxFeeForBatchOfProofs(16); + if (!maxFee) { + alert("Could not estimate max fee"); + return; + } + + const chainId = USED_CHAIN.id; + const payment_service_addr = getAddress(BATCHER_PAYMENT_SERVICE_ADDR); + + const noncedVerificationdata = { + maxFee: toHex(maxFee, { size: 32 }), + nonce: toHex(nonce, { size: 32 }), + chain_id: toHex(chainId, { size: 32 }), + payment_service_addr, + verificationData, + }; + + const { r, s, v } = await signVerificationData( + privateKey, + noncedVerificationdata, + payment_service_addr + ); + + const submitProofMessage = { + verificationData: noncedVerificationdata, + signature: { + r, + s, + v: Number(v), + }, + }; + + return JSON.stringify(submitProofMessage); +} + +async function signVerificationData( + privateKey, + noncedVerificationData, + payment_service_addr, + opts = {} +) { + const account = privateKeyToAccount(privateKey); + const client = createWalletClient({ account, transport: http(RPC_URL) }); + + const toHex = (x) => { + if (typeof x === "string") return x.startsWith("0x") ? x : `0x${Buffer.from(x, "utf8").toString("hex")}`; + return `0x${Buffer.from(x).toString("hex")}`; + }; + + const message = { + verification_data_hash: toHex( + computeVerificationDataCommitment(noncedVerificationData.verificationData).commitmentDigest + ), + nonce: noncedVerificationData.nonce, + max_fee: noncedVerificationData.maxFee, + }; + + const domain = eip712Domain(Number(noncedVerificationData.chain_id), payment_service_addr); + + const signature = await client.signTypedData({ + account, + domain, + types: eip712Types, + primaryType: "NoncedVerificationData", + message, + }); + + const r = `0x${signature.slice(2, 66)}`; + const s = `0x${signature.slice(66, 130)}`; + let v = parseInt(signature.slice(130, 132), 16); + + if (opts.normalizeV) v = v === 0 || v === 1 ? v + 27 : v; + if (!Number.isFinite(v)) throw new Error("Failure obtaining the sign, v is undefined"); + + return { r, s, v, signature }; +} + +export const provingSystemNameToByte = { + GnarkPlonkBls12_381: 0, + GnarkPlonkBn254: 1, + GnarkGroth16Bn254: 2, + SP1: 3, + Risc0: 4, + CircomGroth16Bn256: 5, +}; + +function hexStringToBytes(hex) { + if (hex.startsWith("0x")) hex = hex.slice(2); + if (hex.length % 2 !== 0) throw new Error("Invalid hex string"); + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); + } + return bytes; +} + +export const computeVerificationDataCommitment = (verificationData) => { + const hasher = new Keccak(256); + hasher.update(Buffer.from(verificationData.proof)); + const proofCommitment = hasher.digest(); + hasher.reset(); + + let pubInputCommitment = Buffer.from(new Uint8Array(32).fill(0)); + const publicInput = verificationData.publicInput; + if (publicInput) { + pubInputCommitment = hasher.update(Buffer.from(publicInput)).digest(); + } + hasher.reset(); + + let provingSystemAuxDataCommitment = Buffer.from( + new Uint8Array(32).fill(0) + ); + const provingSystemByte = Buffer.from([ + provingSystemNameToByte[verificationData.provingSystem], + ]); + if (verificationData.verificationKey) { + hasher.update(Buffer.from(verificationData.verificationKey)); + hasher.update(provingSystemByte); + provingSystemAuxDataCommitment = hasher.digest(); + } else if (verificationData.vmProgramCode) { + hasher.update(Buffer.from(verificationData.vmProgramCode)); + hasher.update(provingSystemByte); + provingSystemAuxDataCommitment = hasher.digest(); + } + + const proofGeneratorAddress = Buffer.from( + hexStringToBytes(verificationData.proofGeneratorAddress) + ); + + hasher.reset(); + hasher.update(proofCommitment); + hasher.update(pubInputCommitment); + hasher.update(provingSystemAuxDataCommitment); + hasher.update(proofGeneratorAddress); + const commitmentDigest = hasher.digest(); + hasher.reset(); + + return { + commitmentDigest, + proofCommitment, + pubInputCommitment, + provingSystemAuxDataCommitment, + }; +}; + +export const eip712Domain = (chainId, batcherPaymentServiceAddress) => ({ + name: "Aligned", + version: "1", + chainId, + verifyingContract: batcherPaymentServiceAddress, +}); + +export const eip712Types = { + NoncedVerificationData: [ + { name: "verification_data_hash", type: "bytes32" }, + { name: "nonce", type: "uint256" }, + { name: "max_fee", type: "uint256" }, + ], +}; diff --git a/test/stress/src/constants.js b/test/stress/src/constants.js new file mode 100644 index 000000000..3964259e3 --- /dev/null +++ b/test/stress/src/constants.js @@ -0,0 +1,17 @@ +import { anvil, sepolia, mainnet } from 'viem/chains' +import { config } from 'dotenv' + +// Load environment variables from .env file +config({ path: '../.env' }) + +// Reads from .env file, but defaults to devnet if not set +export const RPC_URL = process.env.RPC_URL || 'https://ethereum-sepolia-rpc.publicnode.com'; +export const ZK_ARCADE_URL = process.env.ZK_ARCADE_URL || "https://test.zkarcade.com"; +export const BATCHER_URL = process.env.BATCHER_URL || "wss://sepolia.batcher.alignedlayer.com"; + +export const BATCHER_PAYMENT_SERVICE_ADDR = process.env.BATCHER_PAYMENT_SERVICE_ADDR || "0x403dE630751e148bD71BFFcE762E5667C0825399"; + +export const CHAIN_NAME = process.env.CHAIN_NAME || 'sepolia'; + +// Used chain object from viem. Can be sepolia, anvil or mainnet. Defaults to anvil +export const USED_CHAIN = CHAIN_NAME === 'sepolia' ? sepolia : (CHAIN_NAME === 'mainnet' ? mainnet : anvil); diff --git a/test/stress/src/process_worker_pool.js b/test/stress/src/process_worker_pool.js new file mode 100644 index 000000000..7a267f8d6 --- /dev/null +++ b/test/stress/src/process_worker_pool.js @@ -0,0 +1,148 @@ +import { fork } from 'child_process'; +import { cpus } from 'os'; +import path from 'path'; + +export class ProcessWorkerPool { + + constructor(workerScript, poolSize = cpus().length) { + this.workerScript = workerScript; + this.poolSize = poolSize; + this.workers = []; + this.taskQueue = []; + this.activeWorkers = new Set(); + + this.initialize(); + } + + initialize() { + console.log(`[ProcessWorkerPool] Initializing ${this.poolSize} worker processes...`); + for (let i = 0; i < this.poolSize; i++) { + const childProcess = fork(path.resolve(this.workerScript), [], { + silent: false, // Allow process to inherit stdio for debugging + execArgv: [] // Don't inherit Node.js flags + }); + + this.workers.push({ + id: i, + process: childProcess, + busy: false + }); + } + console.log(`[ProcessWorkerPool] All worker processes initialized`); + } + + async exec(data) { + return new Promise((resolve, reject) => { + const task = { data, resolve, reject }; + + const availableWorker = this.workers.find(w => !w.busy); + + if (availableWorker) { + this.runTask(availableWorker, task); + } else { + this.taskQueue.push(task); + } + }); + } + + runTask(workerInfo, task) { + workerInfo.busy = true; + this.activeWorkers.add(workerInfo.id); + + const { process: childProcess } = workerInfo; + + const messageHandler = (result) => { + cleanup(); + if (result.success) { + task.resolve(result); + } else { + task.reject(new Error(result.error)); + } + + workerInfo.busy = false; + this.activeWorkers.delete(workerInfo.id); + + if (this.taskQueue.length > 0) { + const nextTask = this.taskQueue.shift(); + this.runTask(workerInfo, nextTask); + } + }; + + const errorHandler = (error) => { + cleanup(); + task.reject(error); + workerInfo.busy = false; + this.activeWorkers.delete(workerInfo.id); + + if (this.taskQueue.length > 0) { + const nextTask = this.taskQueue.shift(); + this.runTask(workerInfo, nextTask); + } + }; + + const exitHandler = (code, signal) => { + cleanup(); + task.reject(new Error(`Worker process exited with code ${code} and signal ${signal}`)); + workerInfo.busy = false; + this.activeWorkers.delete(workerInfo.id); + + const newChildProcess = fork(path.resolve(this.workerScript), [], { + silent: false, + execArgv: [] + }); + workerInfo.process = newChildProcess; + + if (this.taskQueue.length > 0) { + const nextTask = this.taskQueue.shift(); + this.runTask(workerInfo, nextTask); + } + }; + + const cleanup = () => { + childProcess.off('message', messageHandler); + childProcess.off('error', errorHandler); + childProcess.off('exit', exitHandler); + }; + + childProcess.once('message', messageHandler); + childProcess.once('error', errorHandler); + childProcess.once('exit', exitHandler); + + childProcess.send(task.data); + } + + async terminate() { + console.log('[ProcessWorkerPool] Terminating all worker processes...'); + const promises = this.workers.map(w => { + return new Promise((resolve) => { + if (w.process.killed) { + resolve(); + return; + } + + w.process.once('exit', () => resolve()); + w.process.kill('SIGTERM'); + + + setTimeout(() => { + if (!w.process.killed) { + w.process.kill('SIGKILL'); + } + resolve(); + }, 5000); + }); + }); + + await Promise.all(promises); + console.log('[ProcessWorkerPool] All worker processes terminated'); + } + + getStats() { + return { + poolSize: this.poolSize, + activeWorkers: this.activeWorkers.size, + queuedTasks: this.taskQueue.length, + availableWorkers: this.workers.filter(w => !w.busy).length + }; + } +} diff --git a/test/stress/src/proof_worker_process.js b/test/stress/src/proof_worker_process.js new file mode 100644 index 000000000..a02adf9a4 --- /dev/null +++ b/test/stress/src/proof_worker_process.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node +import { generateCircomParityProof } from './circom_proof_generator.js'; + +// Listen for messages from parent process +process.on('message', async (data) => { + const { address, privateKey, userPositions, levelsBoards, idx } = data; + + try { + const result = await generateCircomParityProof( + address, + userPositions, + levelsBoards, + privateKey, + idx + ); + + process.send({ + success: true, + result, + address, + idx + }); + } catch (error) { + process.send({ + success: false, + error: error.message, + address, + idx + }); + } +}); + +// Handle process termination gracefully +process.on('SIGTERM', () => { + process.exit(0); +}); + +process.on('SIGINT', () => { + process.exit(0); +}); diff --git a/test/stress/src/stress_test.js b/test/stress/src/stress_test.js new file mode 100644 index 000000000..d2664d408 --- /dev/null +++ b/test/stress/src/stress_test.js @@ -0,0 +1,459 @@ +import { fetch } from "undici"; +import { privateKeyToAccount } from 'viem/accounts'; +import { sepolia } from 'viem/chains'; + +import solution from '../data/solution.json' with { type: 'json' }; +import rawRichAccounts from '../data/rich_accounts.json' with { type: 'json' }; + +import fs from 'fs/promises'; +import { parse } from 'csv-parse'; + +import signMessageFromPrivateKey from './utils/sign_agreement.js'; +import { CookieJar, getSetCookies } from './utils/cookie_utils.js'; +import { depositIntoAligned } from './aligned.js'; + +import { cpus } from 'os'; +import { ProcessWorkerPool } from './process_worker_pool.js'; + +const WORKER_POOL_SIZE = Math.max(1, cpus().length - 1); + +let proofWorkerPool; + +import { ZK_ARCADE_URL, USED_CHAIN } from './constants.js'; + +const DEPOSIT_WAIT_MS = 10_000; + +const HTTP_TIMEOUT_MS = 120_000; + +const MAX_RETRIES = 3; +const RETRY_BASE_DELAY_MS = 3_000; + +// Helper function to implement exponential backoff retry logic +async function withRetry(asyncFn, maxRetries = MAX_RETRIES, baseDelay = RETRY_BASE_DELAY_MS) { + let lastError; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await asyncFn(); + } catch (error) { + lastError = error; + + // Check if it's a timeout error worth retrying + const isRetryableError = error.message.includes('timeout') || + error.message.includes('TIMEOUT') || + error.message.includes('UND_ERR_CONNECT_TIMEOUT') || + error.message.includes('fetch failed') || + error.message.includes('AbortError') || + error.message.includes('WebSocket connection error'); + + if (attempt === maxRetries || !isRetryableError) { + throw lastError; + } + + // Exponential backoff with jitter for high concurrency + const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 4000; + console.log(`[Retry ${attempt + 1}/${maxRetries}] Waiting ${Math.round(delay)}ms before retry...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + throw lastError; +} + +function normalizeAccounts(raw) { + if (!Array.isArray(raw)) { + throw new Error('rich_accounts.json must be an array'); + } + + if (raw.length > 0 && typeof raw[0] === 'string') { + return raw.map((pk) => { + const addr = privateKeyToAccount(pk).address; + return { address: addr, privateKey: pk }; + }); + } + + return raw.map((item, idx) => { + if (!item || typeof item !== 'object') { + throw new Error(`Invalid element in rich_accounts.json at index ${idx}`); + } + if (!item.privateKey) { + throw new Error(`Missing privateKey at index ${idx}`); + } + const derived = privateKeyToAccount(item.privateKey).address; + const provided = item.address ?? derived; + + if (item.address && provided.toLowerCase() !== derived.toLowerCase()) { + console.warn( + `[WARN] Provided address does not match the derived address from the PK at idx ${idx}. + provided=${provided} derived=${derived}. Using the derived address.` + ); + } + return { address: derived, privateKey: item.privateKey }; + }); +} + +async function generateProofVerificationData(address, privateKey, idx) { + const levelsBoards = solution.levelsBoards || []; + const userPositions = solution.userPositions || []; + + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Generating proof using process worker...`); + const stats = proofWorkerPool.getStats(); + console.log(` Worker pool stats: ${stats.activeWorkers}/${stats.poolSize} active, ${stats.queuedTasks} queued`); + } + + const result = await proofWorkerPool.exec({ + address, + privateKey, + userPositions, + levelsBoards, + idx + }); + + return { + submit_proof_message: result.result, + game: "Parity", + game_idx: 182, // Note: to be able to submit the same proof multiple times for testing, this value should change on each run + }; +} + +async function createNewSession(jar) { + try { + // Tries to get the CSRF token from the main page headers + const homeRes = await fetch(`${ZK_ARCADE_URL}/`, { + method: "GET", + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }); + if (homeRes.ok) { + jar.absorb(getSetCookies(homeRes)); + const htmlContent = await homeRes.text(); + + const csrfMatch = htmlContent.match(/csrf-token["']\s*content=["']([^"']+)["']/i); + if (csrfMatch && csrfMatch[1]) { + return { csrf_token: csrfMatch[1] }; + } + } + } catch (error) { + console.warn("Could not obtain CSRF token from main page:", error.message); + } + + // If all fails, throw an error instead of using an invalid fallback + throw new Error("Could not obtain a valid CSRF token. The backend requires CSRF and there is no way to obtain it."); +} + +async function doSignPost(jar, csrf_token, payload) { + const res = await fetch(`${ZK_ARCADE_URL}/wallet/sign`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-csrf-token": csrf_token, + "cookie": jar.toHeader() + }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }); + jar.absorb(getSetCookies(res)); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`POST /wallet/sign ${res.status} ${res.statusText} ${text.slice(0,200)}`); + } + return res.json().catch(() => null); +} + +async function doSubmitPost(jar, csrf_token, payload) { + const res = await fetch(`${ZK_ARCADE_URL}/proof/`, { + method: "POST", + headers: { + "content-type": "application/json", + "x-csrf-token": csrf_token, + "cookie": jar.toHeader(), + }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }); + jar.absorb(getSetCookies(res)); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`POST /proof ${res.status} ${res.statusText} ${text.slice(0,200)}`); + } + return res.json().catch(() => null); +} + +async function getAgreementStatus(jar, address) { + const res = await fetch(`${ZK_ARCADE_URL}/api/wallet/${address}/agreement-status`, { + method: 'GET', + headers: { + "cookie": jar.toHeader(), + }, + signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) + }); + jar.absorb(getSetCookies(res)); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`GET /agreement-status → ${res.status} ${res.statusText} ${text.slice(0, 200)}`); + } + return res.json().catch(() => null); +} + +async function createSessionForAccount({ address, privateKey }, idx) { + const jar = new CookieJar(); + try { + const { csrf_token } = await withRetry(() => createNewSession(jar)); + + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Obtained CSRF token after new session.`); + } + return { address, privateKey, idx, jar, csrf_token, ok: true }; + } catch (err) { + console.error(`[${address} - ${idx}] ERROR in session creation:`, err); + return { address, privateKey, idx, ok: false, error: String(err) }; + } +} + +async function signAgreementForAccount(sessionData) { + const { address, idx, jar, csrf_token } = sessionData; + if (!sessionData.ok) return sessionData; + + try { + const signature = await signMessageFromPrivateKey(sessionData.privateKey); + await withRetry(() => doSignPost(jar, csrf_token, { + address, + signature, + _csrf_token: csrf_token, + })); + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Signed service agreement`); + } + return { ...sessionData, ok: true }; + } catch (err) { + console.error(`[${address} - ${idx}] ERROR in signing:`, err); + return { ...sessionData, ok: false, error: String(err) }; + } +} + +async function handleDepositForAccount(sessionData) { + const { address, idx } = sessionData; + if (!sessionData.ok) return sessionData; + + try { + if (USED_CHAIN.id === sepolia.id) { + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Deposit skipped (Sepolia)`); + } + } else { + await depositIntoAligned(sessionData.privateKey); + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Deposit dispatched. Waiting ${DEPOSIT_WAIT_MS / 1000}s for the deposit to be processed...`); + } + await new Promise((r) => setTimeout(r, DEPOSIT_WAIT_MS)); + } + return { ...sessionData, ok: true }; + } catch (err) { + console.error(`[${address} - ${idx}] ERROR in deposit:`, err); + return { ...sessionData, ok: false, error: String(err) }; + } +} + +async function checkAgreementForAccount(sessionData) { + const { address, idx, jar } = sessionData; + if (!sessionData.ok) return sessionData; + + try { + const status = await withRetry(() => getAgreementStatus(jar, address)); + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Fetched agreement status to keep the session alive.`); + } + return { ...sessionData, status, ok: true }; + } catch (err) { + console.error(`[${address} - ${idx}] ERROR in agreement check:`, err); + return { ...sessionData, ok: false, error: String(err) }; + } +} + +async function generateProofForAccount(sessionData) { + const { address, idx, csrf_token } = sessionData; + if (!sessionData.ok) return sessionData; + + try { + const proofData = await generateProofVerificationData(address, sessionData.privateKey, idx); + const params = { + ...proofData, + _csrf_token: csrf_token + }; + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Generated proof, sending it to the server...`); + } + return { ...sessionData, proofParams: params, ok: true }; + } catch (err) { + console.error(`[${address} - ${idx}] ERROR in proof generation:`, err); + return { ...sessionData, ok: false, error: String(err) }; + } +} + +async function submitProofForAccount(sessionData) { + const { address, idx, jar, csrf_token, proofParams } = sessionData; + if (!sessionData.ok) return sessionData; + + try { + const submitResp = await withRetry(() => doSubmitPost(jar, csrf_token, proofParams)); + if (idx % 100 === 0) { + console.log(`[${address} - ${idx}] Proof submitted successfully`); + } + return { ...sessionData, submitResp, ok: true }; + } catch (err) { + console.error(`[${address} - ${idx}] ERROR in proof submission:`, err); + return { ...sessionData, ok: false, error: String(err) }; + } +} + +// Executes all accounts step by step, with improved jitter and retries to handle timeouts +async function runBatch(accounts) { + console.log(`Starting stress test with ${accounts.length} accounts...`); + + // Initialize account data with private keys + let accountsData = accounts.map((acc, idx) => ({ + ...acc, + idx, + ok: true + })); + + // Step 1: Create sessions for all accounts + console.log('\n=== STEP 1: Creating sessions for all accounts ==='); + // Use larger, more distributed jitter for high concurrency + const sessionPromises = accountsData.map((acc, idx) => { + const progressiveDelay = (idx / accountsData.length) * 3000; + return new Promise(resolve => setTimeout(() => resolve(createSessionForAccount(acc, idx)), progressiveDelay)); + }); + accountsData = await Promise.all(sessionPromises); + + const successfulSessions = accountsData.filter(acc => acc.ok).length; + console.log(`Session creation completed: ${successfulSessions}/${accountsData.length} successful`); + + // Step 2: Sign agreements for all accounts with successful sessions + console.log('\n=== STEP 2: Signing agreements for all accounts ==='); + const signPromises = accountsData.map((acc, idx) => { + const progressiveDelay = (idx / accountsData.length) * 2500; + return new Promise(resolve => setTimeout(() => resolve(signAgreementForAccount(acc)), progressiveDelay)); + }); + accountsData = await Promise.all(signPromises); + + const successfulSigns = accountsData.filter(acc => acc.ok).length; + console.log(`Agreement signing completed: ${successfulSigns}/${accountsData.length} successful`); + + // Step 3: Handle deposits for all accounts + console.log('\n=== STEP 3: Handling deposits for all accounts ==='); + const depositPromises = accountsData.map((acc, idx) => { + const progressiveDelay = (idx / accountsData.length) * 1500; + return new Promise(resolve => setTimeout(() => resolve(handleDepositForAccount(acc)), progressiveDelay)); + }); + accountsData = await Promise.all(depositPromises); + + const successfulDeposits = accountsData.filter(acc => acc.ok).length; + console.log(`Deposit handling completed: ${successfulDeposits}/${accountsData.length} successful`); + + // Step 4: Check agreement status for all accounts + console.log('\n=== STEP 4: Checking agreement status for all accounts ==='); + const statusPromises = accountsData.map((acc, idx) => { + const progressiveDelay = (idx / accountsData.length) * 2000; + return new Promise(resolve => setTimeout(() => resolve(checkAgreementForAccount(acc)), progressiveDelay)); + }); + accountsData = await Promise.all(statusPromises); + + const successfulStatuses = accountsData.filter(acc => acc.ok).length; + console.log(`Agreement status check completed: ${successfulStatuses}/${accountsData.length} successful`); + + // Step 5: Generate proofs for all accounts + console.log('\n=== STEP 5: Generating proofs for all accounts ==='); + const proofPromises = accountsData.map((acc, idx) => { + const progressiveDelay = (idx / accountsData.length) * 1000; + return new Promise(resolve => setTimeout(() => resolve(generateProofForAccount(acc)), progressiveDelay)); + }); + accountsData = await Promise.all(proofPromises); + + const successfulProofs = accountsData.filter(acc => acc.ok).length; + console.log(`Proof generation completed: ${successfulProofs}/${accountsData.length} successful`); + + // Step 6: Submit proofs for all accounts + console.log('\n=== STEP 6: Submitting proofs for all accounts ==='); + const submitPromises = accountsData.map((acc, idx) => { + const progressiveDelay = (idx / accountsData.length) * 4000; + return new Promise(resolve => setTimeout(() => resolve(submitProofForAccount(acc)), progressiveDelay)); + }); + accountsData = await Promise.all(submitPromises); + + const successfulSubmissions = accountsData.filter(acc => acc.ok).length; + console.log(`Proof submission completed: ${successfulSubmissions}/${accountsData.length} successful`); + + // Convert to the expected result format + return accountsData.map(acc => ({ + address: acc.address, + ok: acc.ok, + error: acc.error, + status: acc.status, + submitResp: acc.submitResp + })); +} + +(async () => { + try { + console.log(`Initializing process worker pool with ${WORKER_POOL_SIZE} workers...`); + proofWorkerPool = new ProcessWorkerPool('./src/proof_worker_process.js', WORKER_POOL_SIZE); + + const csvData = await new Promise(async (resolve, reject) => { + const records = []; + const content = await fs.readFile('data/sepolia_rich_accounts.csv', 'utf-8'); + parse(content, { + columns: true, + trim: true, + skip_empty_lines: true, + }) + .on('data', (data) => records.push(data)) + .on('end', () => resolve(records)) + .on('error', (err) => reject(err)); + }); + + let accounts = csvData.map((row, idx) => { + if (!row.privateKey || !row.address) { + throw new Error(`Missing privateKey or address in CSV at row ${idx + 1}`); + } + return { address: row.address, privateKey: row.privateKey }; + }); + + if (accounts.length === 0) { + console.warn("No accounts found to run the stress test. Using fallback rich_accounts.json"); + accounts = normalizeAccounts(rawRichAccounts); + } + + const results = await runBatch(accounts); + + const summary = { + total: results.length, + success: results.filter((r) => r.ok).length, + failed: results.filter((r) => !r.ok).length, + failedAddresses: results.filter((r) => !r.ok).map((r) => r.address), + }; + + console.log('\n=== FINAL SUMMARY ==='); + console.log(`Total accounts: ${summary.total}`); + console.log(`Successful completions: ${summary.success}`); + console.log(`Failed completions: ${summary.failed}`); + + if (summary.failed > 0) { + console.log('\nFailed accounts:'); + for (const r of results.filter(r => !r.ok)) { + console.log(` ${r.address}: ${r.error}`); + } + } + + console.log('\nTerminating process worker pool...'); + await proofWorkerPool.terminate(); + console.log('Process worker pool terminated.'); + + } catch (err) { + console.error("Fatal error in batch run:", err); + if (proofWorkerPool) { + await proofWorkerPool.terminate(); + } + process.exit(1); + } +})(); diff --git a/test/stress/src/utils/cookie_utils.js b/test/stress/src/utils/cookie_utils.js new file mode 100644 index 000000000..e59647330 --- /dev/null +++ b/test/stress/src/utils/cookie_utils.js @@ -0,0 +1,61 @@ +// Minimal client-side cookie store for Node `fetch` (undici). We need this because browsers automatically +// handle Set-Cookie/Cookie, but Node's `fetch` does not. This jar captures `Set-Cookie` headers from +// responses and builds a `Cookie` header for subsequent requests. +// It keeps the latest `name=value` pair for each cookie name, ignoring attributes like `Path`, `HttpOnly`, etc. +export class CookieJar { + constructor() { + this.map = new Map(); + } + + // Absorbs one or many `Set-Cookie` header values into the jar. Accepts either a string or an array of strings. + // If the cookie value is an empty string, the cookie is removed from the jar. + absorb(setCookieHeaders) { + if (!setCookieHeaders) return; + + // Normalize to an array of individual Set-Cookie strings. + const arr = Array.isArray(setCookieHeaders) + ? setCookieHeaders + : splitSetCookieFallback(setCookieHeaders); + + for (const sc of arr) { + // Keep only "name=value" (first segment before any attributes). + const pair = sc.split(';', 1)[0]; + + // Split on the FIRST '=' only (values can contain '='). + const eq = pair.indexOf('='); + if (eq <= 0) continue; + + const name = pair.slice(0, eq).trim(); + const value = pair.slice(eq + 1).trim(); + if (!name) continue; + + // Empty value acts as "delete cookie". + if (value === '') this.map.delete(name); + else this.map.set(name, value); + } + } + + // Builds the Cookie header value for outgoing requests. + toHeader() { + return Array.from(this.map.entries()) + .map(([k, v]) => `${k}=${v}`) + .join('; '); + } +} + +// Splits a possibly folded `Set-Cookie` header string into individual cookie lines. +// We use a regex to account for various edge cases, like commas in Expires attributes or quoted strings. +function splitSetCookieFallback(raw) { + return raw ? raw.split(/,(?=[^;,]+=[^;,]+)/g) : []; +} + +// Extracts all Set-Cookie headers from a Response object. +export function getSetCookies(res) { + // Preferred path (undici provides this helper) + const getSet = res?.headers?.getSetCookie?.(); + if (Array.isArray(getSet)) return getSet; + + // Fallback: single raw header string, may contain multiple cookies + const raw = res.headers.get('set-cookie'); + return splitSetCookieFallback(raw ?? ''); +} diff --git a/test/stress/src/utils/estimate_max_fee.js b/test/stress/src/utils/estimate_max_fee.js new file mode 100644 index 000000000..3331b108e --- /dev/null +++ b/test/stress/src/utils/estimate_max_fee.js @@ -0,0 +1,36 @@ +import { createPublicClient, http } from "viem"; +import { anvil } from "viem/chains"; + +import { RPC_URL } from "../constants.js"; + +export const GAS_ESTIMATION = { + DEFAULT_CONSTANT_GAS_COST: BigInt(537500), + ADDITIONAL_SUBMISSION_GAS_COST_PER_PROOF: BigInt(2000), + GAS_PRICE_PERCENTAGE_MULTIPLIER: BigInt(110), + PERCENTAGE_DIVIDER: BigInt(100), +}; + +export async function estimateMaxFeeForBatchOfProofs(numberProofsInBatch = 16) { + const client = createPublicClient({ + chain: anvil, + transport: http(RPC_URL), + }); + + const gasPrice = await client.getGasPrice(); + + const totalGas = + GAS_ESTIMATION.DEFAULT_CONSTANT_GAS_COST + + GAS_ESTIMATION.ADDITIONAL_SUBMISSION_GAS_COST_PER_PROOF * + BigInt(numberProofsInBatch); + + const estimatedGasPerProof = + BigInt(totalGas) / BigInt(numberProofsInBatch); + + const feePerProof = + (estimatedGasPerProof * + gasPrice * + BigInt(GAS_ESTIMATION.GAS_PRICE_PERCENTAGE_MULTIPLIER)) / + BigInt(GAS_ESTIMATION.PERCENTAGE_DIVIDER); + + return feePerProof; +} diff --git a/test/stress/src/utils/sign_agreement.js b/test/stress/src/utils/sign_agreement.js new file mode 100644 index 000000000..450fcab47 --- /dev/null +++ b/test/stress/src/utils/sign_agreement.js @@ -0,0 +1,11 @@ +import { privateKeyToAccount } from 'viem/accounts' + +export default async function signMessageFromPrivateKey(privateKey) { + const account = privateKeyToAccount(privateKey); + + const signature = await account.signMessage({ + message: "I agree with the service policy", + }); + + return signature; +} diff --git a/test/stress/src/worker_pool.js b/test/stress/src/worker_pool.js new file mode 100644 index 000000000..41f997840 --- /dev/null +++ b/test/stress/src/worker_pool.js @@ -0,0 +1,111 @@ +import { Worker } from 'node:worker_threads'; +import { cpus } from 'os'; +import path from 'path'; + +export class WorkerPool { + + constructor(workerScript, poolSize = cpus().length) { + this.workerScript = workerScript; + this.poolSize = poolSize; + this.workers = []; + this.taskQueue = []; + this.activeWorkers = new Set(); + + this.initialize(); + } + + initialize() { + console.log(`[WorkerPool] Initializing ${this.poolSize} workers...`); + for (let i = 0; i < this.poolSize; i++) { + const worker = new Worker(path.resolve(this.workerScript)); + this.workers.push({ + id: i, + worker, + busy: false + }); + } + console.log(`[WorkerPool] All workers initialized`); + } + + async exec(data) { + return new Promise((resolve, reject) => { + const task = { data, resolve, reject }; + + + const availableWorker = this.workers.find(w => !w.busy); + + if (availableWorker) { + + this.runTask(availableWorker, task); + } else { + + this.taskQueue.push(task); + } + }); + } + + runTask(workerInfo, task) { + workerInfo.busy = true; + this.activeWorkers.add(workerInfo.id); + + const { worker } = workerInfo; + + const messageHandler = (result) => { + cleanup(); + if (result.success) { + task.resolve(result); + } else { + task.reject(new Error(result.error)); + } + + + workerInfo.busy = false; + this.activeWorkers.delete(workerInfo.id); + + + if (this.taskQueue.length > 0) { + const nextTask = this.taskQueue.shift(); + this.runTask(workerInfo, nextTask); + } + }; + + const errorHandler = (error) => { + cleanup(); + task.reject(error); + workerInfo.busy = false; + this.activeWorkers.delete(workerInfo.id); + + + if (this.taskQueue.length > 0) { + const nextTask = this.taskQueue.shift(); + this.runTask(workerInfo, nextTask); + } + }; + + const cleanup = () => { + worker.off('message', messageHandler); + worker.off('error', errorHandler); + }; + + worker.once('message', messageHandler); + worker.once('error', errorHandler); + + worker.postMessage(task.data); + } + + async terminate() { + console.log('[WorkerPool] Terminating all workers...'); + const promises = this.workers.map(w => w.worker.terminate()); + await Promise.all(promises); + console.log('[WorkerPool] All workers terminated'); + } + + getStats() { + return { + poolSize: this.poolSize, + activeWorkers: this.activeWorkers.size, + queuedTasks: this.taskQueue.length, + availableWorkers: this.workers.filter(w => !w.busy).length + }; + } +} diff --git a/web/lib/zk_arcade_web/controllers/proof_controller.ex b/web/lib/zk_arcade_web/controllers/proof_controller.ex index aea327427..4d87cd9ba 100644 --- a/web/lib/zk_arcade_web/controllers/proof_controller.ex +++ b/web/lib/zk_arcade_web/controllers/proof_controller.ex @@ -562,5 +562,4 @@ defmodule ZkArcadeWeb.ProofController do Logger.warning("Could not resolve country for address #{address}: #{inspect(reason)}") end end - end