diff --git a/.github/workflows/ci-core-reusable.yml b/.github/workflows/ci-core-reusable.yml index 28d882b9870..15d26e0b469 100644 --- a/.github/workflows/ci-core-reusable.yml +++ b/.github/workflows/ci-core-reusable.yml @@ -185,6 +185,7 @@ jobs: SNAPSHOT_RECOVERY_LOGS_DIR=logs/snapshot_recovery/ GENESIS_RECOVERY_LOGS_DIR=logs/genesis_recovery/ EXTERNAL_NODE_LOGS_DIR=logs/external_node + FEES_LOGS_DIR=logs/fees REVERT_LOGS_DIR=logs/revert mkdir -p $SERVER_LOGS_DIR @@ -193,6 +194,7 @@ jobs: mkdir -p $SNAPSHOT_RECOVERY_LOGS_DIR mkdir -p $GENESIS_RECOVERY_LOGS_DIR mkdir -p $EXTERNAL_NODE_LOGS_DIR + mkdir -p $FEES_LOGS_DIR mkdir -p $REVERT_LOGS_DIR echo "SERVER_LOGS_DIR=$SERVER_LOGS_DIR" >> $GITHUB_ENV @@ -201,6 +203,7 @@ jobs: echo "SNAPSHOT_RECOVERY_LOGS_DIR=$SNAPSHOT_RECOVERY_LOGS_DIR" >> $GITHUB_ENV echo "GENESIS_RECOVERY_LOGS_DIR=$GENESIS_RECOVERY_LOGS_DIR" >> $GITHUB_ENV echo "EXTERNAL_NODE_LOGS_DIR=$EXTERNAL_NODE_LOGS_DIR" >> $GITHUB_ENV + echo "FEES_LOGS_DIR=$FEES_LOGS_DIR" >> $GITHUB_ENV echo "REVERT_LOGS_DIR=$REVERT_LOGS_DIR" >> $GITHUB_ENV - name: Initialize ecosystem @@ -220,9 +223,9 @@ jobs: - name: Read Custom Token address and set as environment variable run: | - address=$(awk -F": " '/tokens:/ {found_tokens=1} found_tokens && /DAI:/ {found_dai=1} found_dai && /address:/ {print $2; exit}' ./configs/erc20.yaml) - echo "address=$address" - echo "address=$address" >> $GITHUB_ENV + CUSTOM_TOKEN_ADDRESS=$(awk -F": " '/tokens:/ {found_tokens=1} found_tokens && /DAI:/ {found_dai=1} found_dai && /address:/ {print $2; exit}' ./configs/erc20.yaml) + echo "CUSTOM_TOKEN_ADDRESS=$CUSTOM_TOKEN_ADDRESS" + echo "CUSTOM_TOKEN_ADDRESS=$CUSTOM_TOKEN_ADDRESS" >> $GITHUB_ENV - name: Create and initialize Validium chain run: | @@ -256,7 +259,7 @@ jobs: --prover-mode no-proofs \ --wallet-creation localhost \ --l1-batch-commit-data-generator-mode rollup \ - --base-token-address ${{ env.address }} \ + --base-token-address ${{ env.CUSTOM_TOKEN_ADDRESS }} \ --base-token-price-nominator 3 \ --base-token-price-denominator 2 \ --set-as-default false \ @@ -315,7 +318,7 @@ jobs: --prover-mode no-proofs \ --wallet-creation localhost \ --l1-batch-commit-data-generator-mode validium \ - --base-token-address ${{ env.address }} \ + --base-token-address ${{ env.CUSTOM_TOKEN_ADDRESS }} \ --base-token-price-nominator 3 \ --base-token-price-denominator 2 \ --set-as-default false \ @@ -458,6 +461,27 @@ jobs: wait $PID3 wait $PID4 + - name: Fee projection tests + run: | + ci_run killall -INT zksync_server || true + + ci_run zk_supervisor test fees --no-deps --no-kill --chain era &> ${{ env.FEES_LOGS_DIR }}/era.log & + PID1=$! + + ci_run zk_supervisor test fees --no-deps --no-kill --chain validium &> ${{ env.FEES_LOGS_DIR }}/validium.log & + PID2=$! + + ci_run zk_supervisor test fees --no-deps --no-kill --chain custom_token &> ${{ env.FEES_LOGS_DIR }}/custom_token.log & + PID3=$! + + ci_run zk_supervisor test fees --no-deps --no-kill --chain consensus &> ${{ env.FEES_LOGS_DIR }}/consensus.log & + PID4=$! + + wait $PID1 + wait $PID2 + wait $PID3 + wait $PID4 + - name: Run revert tests run: | ci_run killall -INT zksync_server || true diff --git a/core/tests/ts-integration/src/context-owner.ts b/core/tests/ts-integration/src/context-owner.ts index 71c8227af2c..e77cdf1a053 100644 --- a/core/tests/ts-integration/src/context-owner.ts +++ b/core/tests/ts-integration/src/context-owner.ts @@ -7,6 +7,7 @@ import { lookupPrerequisites } from './prerequisites'; import { Reporter } from './reporter'; import { scaledGasPrice } from './helpers'; import { RetryProvider } from './retry-provider'; +import { killPidWithAllChilds } from 'utils/build/kill'; // These amounts of ETH would be provided to each test suite through its "main" account. // It is assumed to be enough to run a set of "normal" transactions. @@ -624,6 +625,11 @@ export class TestContextOwner { // Then propagate the exception. throw error; } + if (this.env.l2NodePid !== undefined) { + this.reporter.startAction(`Terminating L2 node process`); + await killPidWithAllChilds(this.env.l2NodePid, 9); + this.reporter.finishAction(); + } } /** @@ -648,6 +654,10 @@ export class TestContextOwner { // into account. If the same wallet would be reused (e.g. on stage), it'll just have to // deposit less next time. } + + setL2NodePid(newPid: number) { + this.env.l2NodePid = newPid; + } } /** diff --git a/core/tests/ts-integration/src/env.ts b/core/tests/ts-integration/src/env.ts index ffef0fce5ce..1de917c2362 100644 --- a/core/tests/ts-integration/src/env.ts +++ b/core/tests/ts-integration/src/env.ts @@ -6,7 +6,17 @@ import { DataAvailabityMode, NodeMode, TestEnvironment } from './types'; import { Reporter } from './reporter'; import * as yaml from 'yaml'; import { L2_BASE_TOKEN_ADDRESS } from 'zksync-ethers/build/utils'; -import { loadConfig, loadEcosystem, shouldLoadConfigFromFile } from 'utils/build/file-configs'; +import { FileConfig, loadConfig, loadEcosystem, shouldLoadConfigFromFile } from 'utils/build/file-configs'; +import { NodeSpawner } from './utils'; +import { logsTestPath } from 'utils/build/logs'; +import * as nodefs from 'node:fs/promises'; +import { exec } from 'utils'; + +const enableConsensus = process.env.ENABLE_CONSENSUS === 'true'; + +async function logsPath(chain: string, name: string): Promise { + return await logsTestPath(chain, 'logs/server/', name); +} /** * Attempts to connect to server. @@ -60,8 +70,10 @@ function getMainWalletPk(pathToHome: string): string { /* Loads the environment for file based configs. */ -async function loadTestEnvironmentFromFile(chain: string): Promise { +async function loadTestEnvironmentFromFile(fileConfig: FileConfig): Promise { + let chain = fileConfig.chain!; const pathToHome = path.join(__dirname, '../../../..'); + let spawnNode = process.env.SPAWN_NODE; let nodeMode; if (process.env.EXTERNAL_NODE == 'true') { nodeMode = NodeMode.External; @@ -75,18 +87,42 @@ async function loadTestEnvironmentFromFile(chain: string): Promise { - const { loadFromFile, chain } = shouldLoadConfigFromFile(); + const fileConfig = shouldLoadConfigFromFile(); - if (loadFromFile) { - return await loadTestEnvironmentFromFile(chain); + if (fileConfig.loadFromFile) { + return await loadTestEnvironmentFromFile(fileConfig); } return await loadTestEnvironmentFromEnv(); } @@ -257,6 +294,7 @@ export async function loadTestEnvironmentFromEnv(): Promise { network, mainWalletPK, l2NodeUrl, + l2NodePid: undefined, l1NodeUrl, wsL2NodeUrl, healthcheckPort, diff --git a/core/tests/ts-integration/src/jest-setup/global-setup.ts b/core/tests/ts-integration/src/jest-setup/global-setup.ts index ffb1a8c3503..27a47a1e933 100644 --- a/core/tests/ts-integration/src/jest-setup/global-setup.ts +++ b/core/tests/ts-integration/src/jest-setup/global-setup.ts @@ -18,10 +18,7 @@ async function performSetup(_globalConfig: any, _projectConfig: any) { console.log(''); globalThis.rawWriteToConsole = console.log; - // Before starting any actual logic, we need to ensure that the server is running (it may not - // be the case, for example, right after deployment on stage). - - const testEnvironment = await loadTestEnvironment(); + let testEnvironment = await loadTestEnvironment(); const testContextOwner = new TestContextOwner(testEnvironment); const testContext = await testContextOwner.setupContext(); diff --git a/core/tests/ts-integration/src/types.ts b/core/tests/ts-integration/src/types.ts index 4975b7b612c..c513480c1b4 100644 --- a/core/tests/ts-integration/src/types.ts +++ b/core/tests/ts-integration/src/types.ts @@ -55,6 +55,10 @@ export interface TestEnvironment { * Mode of the l2 node */ nodeMode: NodeMode; + /* + * L2 node PID + */ + l2NodePid: number | undefined; /** * Plaintext name of the L1 network name (i.e. `localhost` or `goerli`). */ diff --git a/core/tests/ts-integration/src/utils.ts b/core/tests/ts-integration/src/utils.ts new file mode 100644 index 00000000000..128d0be57d0 --- /dev/null +++ b/core/tests/ts-integration/src/utils.ts @@ -0,0 +1,197 @@ +import { spawn as _spawn, ChildProcessWithoutNullStreams, type ProcessEnvOptions } from 'child_process'; +import { assert } from 'chai'; +import { FileConfig } from 'utils/build/file-configs'; +import { killPidWithAllChilds } from 'utils/build/kill'; +import * as utils from 'utils'; +import fs from 'node:fs/promises'; +import * as zksync from 'zksync-ethers'; +import { + deleteInternalEnforcedL1GasPrice, + deleteInternalEnforcedPubdataPrice, + setInternalEnforcedL1GasPrice, + setInternalEnforcedPubdataPrice, + setTransactionSlots +} from '../tests/utils'; + +// executes a command in background and returns a child process handle +// by default pipes data to parent's stdio but this can be overridden +export function runServerInBackground({ + components, + stdio, + cwd, + env, + useZkInception, + chain +}: { + components?: string[]; + stdio: any; + cwd?: ProcessEnvOptions['cwd']; + env?: ProcessEnvOptions['env']; + useZkInception?: boolean; + newL1GasPrice?: string; + newPubdataPrice?: string; + chain?: string; +}): ChildProcessWithoutNullStreams { + let command = ''; + if (useZkInception) { + command = 'zk_inception server'; + if (chain) { + command += ` --chain ${chain}`; + } + } else { + command = 'zk server'; + } + if (components && components.length > 0) { + command += ` --components=${components.join(',')}`; + } + command = command.replace(/\n/g, ' '); + console.log(`Run command ${command}`); + return _spawn(command, { stdio: stdio, shell: true, detached: true, cwd, env }); +} + +export interface MainNodeSpawnOptions { + enableConsensus: boolean; + ethClientWeb3Url: string; + apiWeb3JsonRpcHttpUrl: string; + baseTokenAddress: string; +} + +export enum NodeType { + MAIN = 'zksync_server', + EXT = 'zksync_external_node' +} + +export class Node { + constructor(public proc: ChildProcessWithoutNullStreams, public l2NodeUrl: string, private readonly type: TYPE) {} + + public async terminate() { + try { + await killPidWithAllChilds(this.proc.pid!, 9); + } catch (err) { + console.log(`ignored error: ${err}`); + } + } + + /** + * Terminates all main node processes running. + * + * WARNING: This is not safe to use when running nodes on multiple chains. + */ + public static async killAll(type: NodeType) { + try { + await utils.exec(`killall -KILL ${type}`); + } catch (err) { + console.log(`ignored error: ${err}`); + } + } + + public async killAndWaitForShutdown() { + await this.terminate(); + // Wait until it's really stopped. + let iter = 0; + while (iter < 30) { + try { + let provider = new zksync.Provider(this.l2NodeUrl); + await provider.getBlockNumber(); + await utils.sleep(2); + iter += 1; + } catch (_) { + // When exception happens, we assume that server died. + return; + } + } + // It's going to panic anyway, since the server is a singleton entity, so better to exit early. + throw new Error(`${this.type} didn't stop after a kill request`); + } +} + +export class NodeSpawner { + public constructor( + private readonly pathToHome: string, + private readonly logs: fs.FileHandle, + private readonly fileConfig: FileConfig, + private readonly options: MainNodeSpawnOptions, + private env?: ProcessEnvOptions['env'] + ) {} + + public async spawnMainNode(newL1GasPrice?: string, newPubdataPrice?: string): Promise> { + const env = this.env ?? process.env; + const { fileConfig, pathToHome, options, logs } = this; + + const testMode = newPubdataPrice || newL1GasPrice; + + console.log('New L1 Gas Price: ', newL1GasPrice); + console.log('New Pubdata Price: ', newPubdataPrice); + + if (fileConfig.loadFromFile) { + setTransactionSlots(pathToHome, fileConfig, testMode ? 1 : 8192); + + if (newL1GasPrice) { + setInternalEnforcedL1GasPrice(pathToHome, fileConfig, parseFloat(newL1GasPrice)); + } else { + deleteInternalEnforcedL1GasPrice(pathToHome, fileConfig); + } + + if (newPubdataPrice) { + setInternalEnforcedPubdataPrice(pathToHome, fileConfig, parseFloat(newPubdataPrice)); + } else { + deleteInternalEnforcedPubdataPrice(pathToHome, fileConfig); + } + } else { + env['DATABASE_MERKLE_TREE_MODE'] = 'full'; + + if (newPubdataPrice) { + env['ETH_SENDER_GAS_ADJUSTER_INTERNAL_ENFORCED_PUBDATA_PRICE'] = newPubdataPrice; + } + + if (newL1GasPrice) { + // We need to ensure that each transaction gets into its own batch for more fair comparison. + env['ETH_SENDER_GAS_ADJUSTER_INTERNAL_ENFORCED_L1_GAS_PRICE'] = newL1GasPrice; + } + + if (testMode) { + // We need to ensure that each transaction gets into its own batch for more fair comparison. + env['CHAIN_STATE_KEEPER_TRANSACTION_SLOTS'] = '1'; + } + } + + let components = 'api,tree,eth,state_keeper,da_dispatcher,vm_runner_protective_reads'; + if (options.enableConsensus) { + components += ',consensus'; + } + if (options.baseTokenAddress != zksync.utils.LEGACY_ETH_ADDRESS) { + components += ',base_token_ratio_persister'; + } + let proc = runServerInBackground({ + components: [components], + stdio: ['ignore', logs, logs], + cwd: pathToHome, + env: env, + useZkInception: fileConfig.loadFromFile, + chain: fileConfig.chain + }); + + // Wait until the main node starts responding. + await waitForNodeToStart(proc, options.apiWeb3JsonRpcHttpUrl); + return new Node(proc, options.apiWeb3JsonRpcHttpUrl, NodeType.MAIN); + } +} + +async function waitForNodeToStart(proc: ChildProcessWithoutNullStreams, l2Url: string) { + while (true) { + try { + const l2Provider = new zksync.Provider(l2Url); + const blockNumber = await l2Provider.getBlockNumber(); + if (blockNumber != 0) { + console.log(`Initialized node API on ${l2Url}; latest block: ${blockNumber}`); + break; + } + } catch (err) { + if (proc.exitCode != null) { + assert.fail(`server failed to start, exitCode = ${proc.exitCode}`); + } + console.log(`Node waiting for API on ${l2Url}`); + await utils.sleep(1); + } + } +} diff --git a/core/tests/ts-integration/tests/fees.test.ts b/core/tests/ts-integration/tests/fees.test.ts index 8d5b7a23a94..2297d7232cc 100644 --- a/core/tests/ts-integration/tests/fees.test.ts +++ b/core/tests/ts-integration/tests/fees.test.ts @@ -9,23 +9,27 @@ * sure that the test is maintained does not get broken. * */ -import * as utils from 'utils'; -import * as fs from 'fs'; -import { TestMaster } from '../src'; +import fs from 'node:fs/promises'; +import { TestContextOwner, TestMaster } from '../src'; import * as zksync from 'zksync-ethers'; import * as ethers from 'ethers'; import { DataAvailabityMode, Token } from '../src/types'; import { SYSTEM_CONTEXT_ADDRESS, getTestContract } from '../src/helpers'; +import { loadConfig, shouldLoadConfigFromFile } from 'utils/build/file-configs'; +import { logsTestPath } from 'utils/build/logs'; +import path from 'path'; +import { NodeSpawner, Node, NodeType } from '../src/utils'; +import { deleteInternalEnforcedL1GasPrice, deleteInternalEnforcedPubdataPrice, setTransactionSlots } from './utils'; +import { killPidWithAllChilds } from 'utils/build/kill'; + +declare global { + var __ZKSYNC_TEST_CONTEXT_OWNER__: TestContextOwner; +} const UINT32_MAX = 2n ** 32n - 1n; const MAX_GAS_PER_PUBDATA = 50_000n; -const logs = fs.createWriteStream('fees.log', { flags: 'a' }); - -// Unless `RUN_FEE_TEST` is provided, skip the test suit -const testFees = process.env.RUN_FEE_TEST ? describe : describe.skip; - // The L1 gas prices under which the test will be conducted. // For CI we use only 2 gas prices to not slow it down too much. const L1_GAS_PRICES_TO_TEST = process.env.CI @@ -47,22 +51,84 @@ const L1_GAS_PRICES_TO_TEST = process.env.CI 2_000_000_000_000n // 2000 gwei ]; -testFees('Test fees', () => { +// Unless `RUN_FEE_TEST` is provided, skip the test suit +const testFees = process.env.RUN_FEE_TEST ? describe : describe.skip; + +testFees('Test fees', function () { let testMaster: TestMaster; let alice: zksync.Wallet; let tokenDetails: Token; let aliceErc20: zksync.Contract; - beforeAll(() => { + let mainLogs: fs.FileHandle; + let baseTokenAddress: string; + let ethClientWeb3Url: string; + let apiWeb3JsonRpcHttpUrl: string; + let mainNodeSpawner: NodeSpawner; + let mainNode: Node; + + const fileConfig = shouldLoadConfigFromFile(); + const pathToHome = path.join(__dirname, '../../../..'); + const enableConsensus = process.env.ENABLE_CONSENSUS == 'true'; + + async function logsPath(chain: string | undefined, name: string): Promise { + chain = chain ? chain : 'default'; + return await logsTestPath(chain, 'logs/server/fees', name); + } + + beforeAll(async () => { testMaster = TestMaster.getInstance(__filename); - alice = testMaster.mainAccount(); + let l2Node = testMaster.environment().l2NodePid; + if (l2Node !== undefined) { + await killPidWithAllChilds(l2Node, 9); + } + + if (!fileConfig.loadFromFile) { + ethClientWeb3Url = process.env.ETH_CLIENT_WEB3_URL!; + apiWeb3JsonRpcHttpUrl = process.env.API_WEB3_JSON_RPC_HTTP_URL!; + baseTokenAddress = process.env.CONTRACTS_BASE_TOKEN_ADDR!; + } else { + const generalConfig = loadConfig({ + pathToHome, + chain: fileConfig.chain, + config: 'general.yaml' + }); + const secretsConfig = loadConfig({ + pathToHome, + chain: fileConfig.chain, + config: 'secrets.yaml' + }); + const contractsConfig = loadConfig({ + pathToHome, + chain: fileConfig.chain, + config: 'contracts.yaml' + }); + + ethClientWeb3Url = secretsConfig.l1.l1_rpc_url; + apiWeb3JsonRpcHttpUrl = generalConfig.api.web3_json_rpc.http_url; + baseTokenAddress = contractsConfig.l1.base_token_addr; + } + + const pathToMainLogs = await logsPath(fileConfig.chain, 'server.log'); + mainLogs = await fs.open(pathToMainLogs, 'a'); + console.log(`Writing server logs to ${pathToMainLogs}`); + + mainNodeSpawner = new NodeSpawner(pathToHome, mainLogs, fileConfig, { + enableConsensus, + ethClientWeb3Url, + apiWeb3JsonRpcHttpUrl, + baseTokenAddress + }); + + mainNode = await mainNodeSpawner.spawnMainNode(); + alice = testMaster.mainAccount(); tokenDetails = testMaster.environment().erc20Token; aliceErc20 = new ethers.Contract(tokenDetails.l1Address, zksync.utils.IERC20, alice.ethWallet()); }); - test('Test fees', async () => { + test('Test all fees', async () => { const receiver = ethers.Wallet.createRandom().address; // Getting ETH price in gas. @@ -110,6 +176,10 @@ testFees('Test fees', () => { 'ERC20 transfer (to old):\n\n' ]; for (const gasPrice of L1_GAS_PRICES_TO_TEST) { + // For the sake of simplicity, we'll use the same pubdata price as the L1 gas price. + await mainNode.killAndWaitForShutdown(); + mainNode = await mainNodeSpawner.spawnMainNode(gasPrice.toString(), gasPrice.toString()); + reports = await appendResults( alice, [feeTestL1Receipt, feeTestL1Receipt, feeTestL1ReceiptERC20, feeTestL1ReceiptERC20], @@ -163,8 +233,8 @@ testFees('Test fees', () => { // that the gasLimit is indeed over u32::MAX, which is the most important tested property. const requiredPubdataPrice = minimalL2GasPrice * 100_000n; - await setInternalL1GasPrice( - alice._providerL2(), + await mainNode.killAndWaitForShutdown(); + mainNode = await mainNodeSpawner.spawnMainNode( requiredPubdataPrice.toString(), requiredPubdataPrice.toString() ); @@ -207,10 +277,16 @@ testFees('Test fees', () => { }); afterAll(async () => { + await testMaster.deinitialize(); + await mainNode.killAndWaitForShutdown(); // Returning the pubdata price to the default one - await setInternalL1GasPrice(alice._providerL2(), undefined, undefined, true); - await testMaster.deinitialize(); + // Restore defaults + setTransactionSlots(pathToHome, fileConfig, 8192); + deleteInternalEnforcedL1GasPrice(pathToHome, fileConfig); + deleteInternalEnforcedPubdataPrice(pathToHome, fileConfig); + mainNode = await mainNodeSpawner.spawnMainNode(); + __ZKSYNC_TEST_CONTEXT_OWNER__.setL2NodePid(mainNode.proc.pid!); }); }); @@ -221,9 +297,6 @@ async function appendResults( newL1GasPrice: bigint, reports: string[] ): Promise { - // For the sake of simplicity, we'll use the same pubdata price as the L1 gas price. - await setInternalL1GasPrice(sender._providerL2(), newL1GasPrice.toString(), newL1GasPrice.toString()); - if (originalL1Receipts.length !== reports.length && originalL1Receipts.length !== transactionRequests.length) { throw new Error('The array of receipts and reports have different length'); } @@ -274,75 +347,3 @@ async function updateReport( return oldReport + gasReport; } - -async function killServerAndWaitForShutdown(provider: zksync.Provider) { - await utils.exec('pkill zksync_server'); - // Wait until it's really stopped. - let iter = 0; - while (iter < 30) { - try { - await provider.getBlockNumber(); - await utils.sleep(2); - iter += 1; - } catch (_) { - // When exception happens, we assume that server died. - return; - } - } - // It's going to panic anyway, since the server is a singleton entity, so better to exit early. - throw new Error("Server didn't stop after a kill request"); -} - -async function setInternalL1GasPrice( - provider: zksync.Provider, - newL1GasPrice?: string, - newPubdataPrice?: string, - disconnect?: boolean -) { - // Make sure server isn't running. - try { - await killServerAndWaitForShutdown(provider); - } catch (_) {} - - // Run server in background. - let command = 'zk server --components api,tree,eth,state_keeper,da_dispatcher,vm_runner_protective_reads'; - command = `DATABASE_MERKLE_TREE_MODE=full ${command}`; - - if (newPubdataPrice) { - command = `ETH_SENDER_GAS_ADJUSTER_INTERNAL_ENFORCED_PUBDATA_PRICE=${newPubdataPrice} ${command}`; - } - - if (newL1GasPrice) { - // We need to ensure that each transaction gets into its own batch for more fair comparison. - command = `ETH_SENDER_GAS_ADJUSTER_INTERNAL_ENFORCED_L1_GAS_PRICE=${newL1GasPrice} ${command}`; - } - - const testMode = newPubdataPrice || newL1GasPrice; - if (testMode) { - // We need to ensure that each transaction gets into its own batch for more fair comparison. - command = `CHAIN_STATE_KEEPER_TRANSACTION_SLOTS=1 ${command}`; - } - - const zkSyncServer = utils.background({ command, stdio: [null, logs, logs] }); - - if (disconnect) { - zkSyncServer.unref(); - } - - // Server may need some time to recompile if it's a cold run, so wait for it. - let iter = 0; - let mainContract; - while (iter < 30 && !mainContract) { - try { - mainContract = await provider.getMainContractAddress(); - } catch (_) { - await utils.sleep(2); - iter += 1; - } - } - if (!mainContract) { - throw new Error('Server did not start'); - } - - await utils.sleep(10); -} diff --git a/core/tests/ts-integration/tests/utils.ts b/core/tests/ts-integration/tests/utils.ts new file mode 100644 index 00000000000..24df8a170c2 --- /dev/null +++ b/core/tests/ts-integration/tests/utils.ts @@ -0,0 +1,81 @@ +import * as fs from 'fs'; +import { getConfigPath } from 'utils/build/file-configs'; + +export function setInternalEnforcedPubdataPrice(pathToHome: string, fileConfig: any, value: number) { + setGasAdjusterProperty(pathToHome, fileConfig, 'internal_enforced_pubdata_price', value); +} + +export function setInternalEnforcedL1GasPrice(pathToHome: string, fileConfig: any, value: number) { + setGasAdjusterProperty(pathToHome, fileConfig, 'internal_enforced_l1_gas_price', value); +} + +export function deleteInternalEnforcedPubdataPrice(pathToHome: string, fileConfig: any) { + deleteProperty(pathToHome, fileConfig, 'internal_enforced_pubdata_price'); +} + +export function deleteInternalEnforcedL1GasPrice(pathToHome: string, fileConfig: any) { + deleteProperty(pathToHome, fileConfig, 'internal_enforced_l1_gas_price'); +} + +export function setTransactionSlots(pathToHome: string, fileConfig: any, value: number) { + setPropertyInGeneralConfig(pathToHome, fileConfig, 'transaction_slots', value); +} + +function setPropertyInGeneralConfig(pathToHome: string, fileConfig: any, property: string, value: number) { + const generalConfigPath = getConfigPath({ + pathToHome, + chain: fileConfig.chain, + configsFolder: 'configs', + config: 'general.yaml' + }); + const generalConfig = fs.readFileSync(generalConfigPath, 'utf8'); + + const regex = new RegExp(`${property}:\\s*\\d+(\\.\\d+)?`, 'g'); + const newGeneralConfig = generalConfig.replace(regex, `${property}: ${value}`); + + fs.writeFileSync(generalConfigPath, newGeneralConfig, 'utf8'); +} + +function setGasAdjusterProperty(pathToHome: string, fileConfig: any, property: string, value: number) { + const generalConfigPath = getConfigPath({ + pathToHome, + chain: fileConfig.chain, + configsFolder: 'configs', + config: 'general.yaml' + }); + const generalConfig = fs.readFileSync(generalConfigPath, 'utf8'); + + // Define the regex pattern to check if the property already exists + const propertyRegex = new RegExp(`(^\\s*${property}:\\s*\\d+(\\.\\d+)?$)`, 'm'); + const gasAdjusterRegex = new RegExp('(^\\s*gas_adjuster:.*$)', 'gm'); + + let newGeneralConfig; + + if (propertyRegex.test(generalConfig)) { + // If the property exists, modify its value + newGeneralConfig = generalConfig.replace(propertyRegex, ` ${property}: ${value}`); + } else { + // If the property does not exist, add it under the gas_adjuster section + newGeneralConfig = generalConfig.replace(gasAdjusterRegex, `$1\n ${property}: ${value}`); + } + + fs.writeFileSync(generalConfigPath, newGeneralConfig, 'utf8'); +} + +function deleteProperty(pathToHome: string, fileConfig: any, property: string) { + const generalConfigPath = getConfigPath({ + pathToHome, + chain: fileConfig.chain, + configsFolder: 'configs', + config: 'general.yaml' + }); + const generalConfig = fs.readFileSync(generalConfigPath, 'utf8'); + + // Define the regex pattern to find the property line and remove it completely + const propertyRegex = new RegExp(`^\\s*${property}:.*\\n?`, 'm'); + + // Remove the line if the property exists + const newGeneralConfig = generalConfig.replace(propertyRegex, ''); + + fs.writeFileSync(generalConfigPath, newGeneralConfig, 'utf8'); +} diff --git a/etc/utils/src/kill.ts b/etc/utils/src/kill.ts index 7fdab85afad..71b76e71d80 100644 --- a/etc/utils/src/kill.ts +++ b/etc/utils/src/kill.ts @@ -13,7 +13,10 @@ export async function killPidWithAllChilds(pid: number, signalNumber: number) { } // We always run the test using additional tools, that means we have to kill not the main process, but the child process for (let i = childs.length - 1; i >= 0; i--) { - console.log(`kill ${childs[i]}`); - await promisify(exec)(`kill -${signalNumber} ${childs[i]}`); + try { + await promisify(exec)(`kill -${signalNumber} ${childs[i]}`); + } catch (e) { + console.log(`Failed to kill ${childs[i]} with ${e}`); + } } } diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/test/args/fees.rs b/zk_toolbox/crates/zk_supervisor/src/commands/test/args/fees.rs new file mode 100644 index 00000000000..1337566e536 --- /dev/null +++ b/zk_toolbox/crates/zk_supervisor/src/commands/test/args/fees.rs @@ -0,0 +1,12 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; + +use crate::messages::{MSG_NO_DEPS_HELP, MSG_NO_KILL_HELP}; + +#[derive(Debug, Serialize, Deserialize, Parser)] +pub struct FeesArgs { + #[clap(short, long, help = MSG_NO_DEPS_HELP)] + pub no_deps: bool, + #[clap(short, long, help = MSG_NO_KILL_HELP)] + pub no_kill: bool, +} diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/test/args/mod.rs b/zk_toolbox/crates/zk_supervisor/src/commands/test/args/mod.rs index d74d5e64a7d..b951608e768 100644 --- a/zk_toolbox/crates/zk_supervisor/src/commands/test/args/mod.rs +++ b/zk_toolbox/crates/zk_supervisor/src/commands/test/args/mod.rs @@ -1,3 +1,4 @@ +pub mod fees; pub mod integration; pub mod recovery; pub mod revert; diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/test/fees.rs b/zk_toolbox/crates/zk_supervisor/src/commands/test/fees.rs new file mode 100644 index 00000000000..4e69c0efb97 --- /dev/null +++ b/zk_toolbox/crates/zk_supervisor/src/commands/test/fees.rs @@ -0,0 +1,65 @@ +use std::path::PathBuf; + +use anyhow::Context; +use common::{cmd::Cmd, config::global_config, logger}; +use config::EcosystemConfig; +use xshell::{cmd, Shell}; + +use super::{ + args::fees::FeesArgs, + utils::{build_contracts, install_and_build_dependencies, TS_INTEGRATION_PATH}, +}; +use crate::{ + commands::test::utils::{TestWallets, TEST_WALLETS_PATH}, + messages::{ + MSG_CHAIN_NOT_FOUND_ERR, MSG_DESERIALIZE_TEST_WALLETS_ERR, + MSG_INTEGRATION_TESTS_RUN_SUCCESS, + }, +}; + +pub async fn run(shell: &Shell, args: FeesArgs) -> anyhow::Result<()> { + let ecosystem_config = EcosystemConfig::from_file(shell)?; + shell.change_dir(ecosystem_config.link_to_code.join(TS_INTEGRATION_PATH)); + let chain_config = ecosystem_config + .load_chain(global_config().chain_name.clone()) + .expect(MSG_CHAIN_NOT_FOUND_ERR); + + if !args.no_deps { + logger::info("Installing dependencies"); + build_contracts(shell, &ecosystem_config)?; + install_and_build_dependencies(shell, &ecosystem_config)?; + } + + logger::info(format!( + "Running fees tests on chain: {}", + ecosystem_config.current_chain() + )); + + let wallets_path: PathBuf = ecosystem_config.link_to_code.join(TEST_WALLETS_PATH); + let wallets: TestWallets = serde_json::from_str(shell.read_file(&wallets_path)?.as_ref()) + .context(MSG_DESERIALIZE_TEST_WALLETS_ERR)?; + + wallets + .init_test_wallet(&ecosystem_config, &chain_config) + .await?; + + let mut command = cmd!(shell, "yarn jest fees.test.ts --testTimeout 240000") + .env("SPAWN_NODE", "1") + .env("RUN_FEE_TEST", "1") + .env("NO_KILL", args.no_kill.to_string()) + .env("CHAIN_NAME", ecosystem_config.current_chain()) + .env("MASTER_WALLET_PK", wallets.get_test_pk(&chain_config)?); + + if global_config().verbose { + command = command.env( + "ZKSYNC_DEBUG_LOGS", + format!("{:?}", global_config().verbose), + ) + } + + Cmd::new(command).with_force_run().run()?; + + logger::outro(MSG_INTEGRATION_TESTS_RUN_SUCCESS); + + Ok(()) +} diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/test/integration.rs b/zk_toolbox/crates/zk_supervisor/src/commands/test/integration.rs index fb3e1436acc..562cf4f7b9b 100644 --- a/zk_toolbox/crates/zk_supervisor/src/commands/test/integration.rs +++ b/zk_toolbox/crates/zk_supervisor/src/commands/test/integration.rs @@ -7,15 +7,16 @@ use xshell::{cmd, Shell}; use super::{ args::integration::IntegrationArgs, - utils::{build_contracts, install_and_build_dependencies, TestWallets, TEST_WALLETS_PATH}, + utils::{ + build_contracts, install_and_build_dependencies, TestWallets, TEST_WALLETS_PATH, + TS_INTEGRATION_PATH, + }, }; use crate::messages::{ msg_integration_tests_run, MSG_CHAIN_NOT_FOUND_ERR, MSG_DESERIALIZE_TEST_WALLETS_ERR, MSG_INTEGRATION_TESTS_RUN_SUCCESS, }; -const TS_INTEGRATION_PATH: &str = "core/tests/ts-integration"; - pub async fn run(shell: &Shell, args: IntegrationArgs) -> anyhow::Result<()> { let ecosystem_config = EcosystemConfig::from_file(shell)?; diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs b/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs index 7d2af71ae9c..ae6b4518e6d 100644 --- a/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs +++ b/zk_toolbox/crates/zk_supervisor/src/commands/test/mod.rs @@ -1,6 +1,6 @@ use args::{ - integration::IntegrationArgs, recovery::RecoveryArgs, revert::RevertArgs, rust::RustArgs, - upgrade::UpgradeArgs, + fees::FeesArgs, integration::IntegrationArgs, recovery::RecoveryArgs, revert::RevertArgs, + rust::RustArgs, upgrade::UpgradeArgs, }; use clap::Subcommand; use xshell::Shell; @@ -14,6 +14,7 @@ use crate::messages::{ mod args; mod build; mod db; +mod fees; mod integration; mod l1_contracts; mod loadtest; @@ -29,6 +30,8 @@ mod wallet; pub enum TestCommands { #[clap(about = MSG_INTEGRATION_TESTS_ABOUT, alias = "i")] Integration(IntegrationArgs), + #[clap(about = "Run fees test", alias = "i")] + Fees(FeesArgs), #[clap(about = MSG_REVERT_TEST_ABOUT, alias = "r")] Revert(RevertArgs), #[clap(about = MSG_RECOVERY_TEST_ABOUT, alias = "rec")] @@ -52,6 +55,7 @@ pub enum TestCommands { pub async fn run(shell: &Shell, args: TestCommands) -> anyhow::Result<()> { match args { TestCommands::Integration(args) => integration::run(shell, args).await, + TestCommands::Fees(args) => fees::run(shell, args).await, TestCommands::Revert(args) => revert::run(shell, args).await, TestCommands::Recovery(args) => recovery::run(shell, args).await, TestCommands::Upgrade(args) => upgrade::run(shell, args), diff --git a/zk_toolbox/crates/zk_supervisor/src/commands/test/utils.rs b/zk_toolbox/crates/zk_supervisor/src/commands/test/utils.rs index 3a5cfd179cc..8656ff44d31 100644 --- a/zk_toolbox/crates/zk_supervisor/src/commands/test/utils.rs +++ b/zk_toolbox/crates/zk_supervisor/src/commands/test/utils.rs @@ -17,7 +17,7 @@ use crate::messages::{ pub const TEST_WALLETS_PATH: &str = "etc/test_config/constant/eth.json"; const AMOUNT_FOR_DISTRIBUTION_TO_WALLETS: u128 = 1000000000000000000000; -const TS_INTEGRATION_PATH: &str = "core/tests/ts-integration"; +pub const TS_INTEGRATION_PATH: &str = "core/tests/ts-integration"; const CONTRACTS_TEST_DATA_PATH: &str = "etc/contracts-test-data"; #[derive(Deserialize)]