From 8db7e9306e5fa23f066be106363e6455531bbc09 Mon Sep 17 00:00:00 2001 From: Artur Puzio Date: Wed, 30 Oct 2024 16:13:50 +0100 Subject: [PATCH] feat: base token integration tests (#2509) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ - Introduce 2 new tests for base token: - gas calculation - proper application of dynamic changes to base token ratio - Improve forced price client fluctuations implementation to limit difference between consecutive values. ## Why ❔ - We want better integration test coverage of custom base token ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Tests for the changes have been added / updated. - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zk fmt` and `zk lint`. --------- Co-authored-by: Ivan Schasny --- .github/workflows/ci-core-reusable.yml | 8 +- .../src/configs/external_price_api_client.rs | 17 +- core/lib/config/src/testonly.rs | 1 + .../src/external_price_api_client.rs | 2 + .../src/forced_price_client.rs | 84 ++++++--- .../src/external_price_api_client.rs | 5 + .../config/external_price_api_client.proto | 1 + .../src/base_token_l1_behaviour.rs | 10 +- core/tests/ts-integration/src/env.ts | 3 +- core/tests/ts-integration/src/utils.ts | 174 +++++++++++++++--- .../ts-integration/tests/base-token.test.ts | 5 +- core/tests/ts-integration/tests/fees.test.ts | 160 +++++++++++++--- core/tests/ts-integration/tests/utils.ts | 81 -------- etc/env/base/external_price_api.toml | 4 +- etc/env/file_based/general.yaml | 4 +- .../zkstack/src/commands/chain/common.rs | 3 + 16 files changed, 388 insertions(+), 174 deletions(-) delete mode 100644 core/tests/ts-integration/tests/utils.ts diff --git a/.github/workflows/ci-core-reusable.yml b/.github/workflows/ci-core-reusable.yml index fb43133868b0..c245e7341d03 100644 --- a/.github/workflows/ci-core-reusable.yml +++ b/.github/workflows/ci-core-reusable.yml @@ -276,8 +276,8 @@ jobs: --wallet-creation localhost \ --l1-batch-commit-data-generator-mode rollup \ --base-token-address ${{ env.CUSTOM_TOKEN_ADDRESS }} \ - --base-token-price-nominator 3 \ - --base-token-price-denominator 2 \ + --base-token-price-nominator 314 \ + --base-token-price-denominator 1000 \ --set-as-default false \ --ignore-prerequisites @@ -332,8 +332,8 @@ jobs: --wallet-creation localhost \ --l1-batch-commit-data-generator-mode validium \ --base-token-address ${{ env.CUSTOM_TOKEN_ADDRESS }} \ - --base-token-price-nominator 3 \ - --base-token-price-denominator 2 \ + --base-token-price-nominator 314 \ + --base-token-price-denominator 1000 \ --set-as-default false \ --ignore-prerequisites diff --git a/core/lib/config/src/configs/external_price_api_client.rs b/core/lib/config/src/configs/external_price_api_client.rs index 15cc7d29d848..c1092f3a7275 100644 --- a/core/lib/config/src/configs/external_price_api_client.rs +++ b/core/lib/config/src/configs/external_price_api_client.rs @@ -4,16 +4,21 @@ use serde::Deserialize; pub const DEFAULT_TIMEOUT_MS: u64 = 10_000; +pub const DEFAULT_FORCED_NEXT_VALUE_FLUCTUATION: u32 = 3; + #[derive(Debug, Clone, PartialEq, Deserialize)] pub struct ForcedPriceClientConfig { /// Forced conversion ratio pub numerator: Option, pub denominator: Option, - /// Forced fluctuation. It defines how much percent numerator / - /// denominator should fluctuate from their forced values. If it's None or 0, then ForcedPriceClient - /// will return the same quote every time it's called. Otherwise, ForcedPriceClient will return - /// forced_quote +/- forced_fluctuation % from its values. + /// Forced fluctuation. It defines how much percent the ratio should fluctuate from its forced + /// value. If it's None or 0, then the ForcedPriceClient will return the same quote every time + /// it's called. Otherwise, ForcedPriceClient will return quote with numerator +/- fluctuation %. pub fluctuation: Option, + /// In order to smooth out fluctuation, consecutive values returned by forced client will not + /// differ more than next_value_fluctuation percent. If it's None, a default of 3% will be applied. + #[serde(default = "ExternalPriceApiClientConfig::default_forced_next_value_fluctuation")] + pub next_value_fluctuation: u32, } #[derive(Debug, Clone, PartialEq, Deserialize)] @@ -31,6 +36,10 @@ impl ExternalPriceApiClientConfig { DEFAULT_TIMEOUT_MS } + fn default_forced_next_value_fluctuation() -> u32 { + DEFAULT_FORCED_NEXT_VALUE_FLUCTUATION + } + pub fn client_timeout(&self) -> Duration { Duration::from_millis(self.client_timeout_ms) } diff --git a/core/lib/config/src/testonly.rs b/core/lib/config/src/testonly.rs index 45c776242630..49c5cff1dca0 100644 --- a/core/lib/config/src/testonly.rs +++ b/core/lib/config/src/testonly.rs @@ -1113,6 +1113,7 @@ impl Distribution, fluctuation: Option, + next_value_fluctuation: u32, } impl ForcedPriceClient { @@ -29,42 +35,70 @@ impl ForcedPriceClient { let fluctuation = forced_price_client_config .fluctuation .map(|x| x.clamp(0, 100)); + let next_value_fluctuation = forced_price_client_config + .next_value_fluctuation + .clamp(0, 100); - Self { - ratio: BaseTokenAPIRatio { + let ratio = if numerator < 100 && fluctuation.is_some_and(|f| f > 0) { + // If numerator is too small we need to multiply by 100 to make sure fluctuations can be applied + BaseTokenAPIRatio { + numerator: NonZeroU64::new(numerator * 100).unwrap(), + denominator: NonZeroU64::new(denominator * 100).unwrap(), + ratio_timestamp: chrono::Utc::now(), + } + } else { + BaseTokenAPIRatio { numerator: NonZeroU64::new(numerator).unwrap(), denominator: NonZeroU64::new(denominator).unwrap(), ratio_timestamp: chrono::Utc::now(), - }, + } + }; + + Self { + ratio, + previous_numerator: Mutex::new(NonZeroU64::new(numerator).unwrap()), fluctuation, + next_value_fluctuation, } } } #[async_trait] impl PriceAPIClient for ForcedPriceClient { - // Returns a ratio which is 10% higher or lower than the configured forced ratio. + /// Returns the configured ratio with fluctuation applied if enabled async fn fetch_ratio(&self, _token_address: Address) -> anyhow::Result { - if let Some(x) = self.fluctuation { - if x != 0 { - let mut rng = rand::thread_rng(); - - let mut adjust_range = |value: NonZeroU64| { - let value_f64 = value.get() as f64; - let min = (value_f64 * (1.0 - x as f64 / 100.0)).round() as u64; - let max = (value_f64 * (1.0 + x as f64 / 100.0)).round() as u64; - rng.gen_range(min..=max) - }; - let new_numerator = adjust_range(self.ratio.numerator); - let new_denominator = adjust_range(self.ratio.denominator); + if let Some(fluctation) = self.fluctuation { + let mut previous_numerator = self.previous_numerator.lock().await; + let mut rng = rand::thread_rng(); + let numerator_range = ( + max( + (self.ratio.numerator.get() as f64 * (1.0 - (fluctation as f64 / 100.0))) + .round() as u64, + (previous_numerator.get() as f64 + * (1.0 - (self.next_value_fluctuation as f64 / 100.0))) + .round() as u64, + ), + min( + (self.ratio.numerator.get() as f64 * (1.0 + (fluctation as f64 / 100.0))) + .round() as u64, + (previous_numerator.get() as f64 + * (1.0 + (self.next_value_fluctuation as f64 / 100.0))) + .round() as u64, + ), + ); - return Ok(BaseTokenAPIRatio { - numerator: NonZeroU64::new(new_numerator).unwrap_or(self.ratio.numerator), - denominator: NonZeroU64::new(new_denominator).unwrap_or(self.ratio.denominator), - ratio_timestamp: chrono::Utc::now(), - }); - } + let new_numerator = + NonZeroU64::new(rng.gen_range(numerator_range.0..=numerator_range.1)) + .unwrap_or(self.ratio.numerator); + let adjusted_ratio = BaseTokenAPIRatio { + numerator: new_numerator, + denominator: self.ratio.denominator, + ratio_timestamp: chrono::Utc::now(), + }; + *previous_numerator = new_numerator; + Ok(adjusted_ratio) + } else { + Ok(self.ratio) } - Ok(self.ratio) } } diff --git a/core/lib/protobuf_config/src/external_price_api_client.rs b/core/lib/protobuf_config/src/external_price_api_client.rs index e5ed809a1284..dbc341c1865a 100644 --- a/core/lib/protobuf_config/src/external_price_api_client.rs +++ b/core/lib/protobuf_config/src/external_price_api_client.rs @@ -17,6 +17,9 @@ impl ProtoRepr for proto::ExternalPriceApiClient { numerator: self.forced_numerator, denominator: self.forced_denominator, fluctuation: self.forced_fluctuation, + next_value_fluctuation: self.forced_next_value_fluctuation.unwrap_or( + configs::external_price_api_client::DEFAULT_FORCED_NEXT_VALUE_FLUCTUATION, + ), }), }, ) @@ -26,6 +29,7 @@ impl ProtoRepr for proto::ExternalPriceApiClient { let numerator = this.forced.as_ref().and_then(|x| x.numerator); let denominator = this.forced.as_ref().and_then(|x| x.denominator); let fluctuation = this.forced.as_ref().and_then(|x| x.fluctuation); + let next_value_fluctuation = this.forced.as_ref().map(|x| x.next_value_fluctuation); Self { source: Some(this.source.clone()), @@ -35,6 +39,7 @@ impl ProtoRepr for proto::ExternalPriceApiClient { forced_numerator: numerator, forced_denominator: denominator, forced_fluctuation: fluctuation, + forced_next_value_fluctuation: next_value_fluctuation, } } } diff --git a/core/lib/protobuf_config/src/proto/config/external_price_api_client.proto b/core/lib/protobuf_config/src/proto/config/external_price_api_client.proto index 646bcfbd7647..63f3233c575f 100644 --- a/core/lib/protobuf_config/src/proto/config/external_price_api_client.proto +++ b/core/lib/protobuf_config/src/proto/config/external_price_api_client.proto @@ -10,4 +10,5 @@ message ExternalPriceApiClient { optional uint64 forced_numerator = 5; optional uint64 forced_denominator = 6; optional uint32 forced_fluctuation = 7; + optional uint32 forced_next_value_fluctuation = 8; } diff --git a/core/node/base_token_adjuster/src/base_token_l1_behaviour.rs b/core/node/base_token_adjuster/src/base_token_l1_behaviour.rs index 0922101e59de..599aba36f3e9 100644 --- a/core/node/base_token_adjuster/src/base_token_l1_behaviour.rs +++ b/core/node/base_token_adjuster/src/base_token_l1_behaviour.rs @@ -220,10 +220,16 @@ impl BaseTokenL1Behaviour { if receipt.status == Some(1.into()) { return Ok(receipt.gas_used); } + let reason = (*l1_params.eth_client) + .as_ref() + .failure_reason(hash) + .await + .context("failed getting failure reason of `setTokenMultiplier` transaction")?; return Err(anyhow::Error::msg(format!( - "`setTokenMultiplier` transaction {:?} failed with status {:?}", + "`setTokenMultiplier` transaction {:?} failed with status {:?}, reason: {:?}", hex::encode(hash), - receipt.status + receipt.status, + reason ))); } else { tokio::time::sleep(sleep_duration).await; diff --git a/core/tests/ts-integration/src/env.ts b/core/tests/ts-integration/src/env.ts index 1de917c2362c..596872ab9c57 100644 --- a/core/tests/ts-integration/src/env.ts +++ b/core/tests/ts-integration/src/env.ts @@ -117,7 +117,8 @@ async function loadTestEnvironmentFromFile(fileConfig: FileConfig): Promise { } } +interface MainNodeOptions { + newL1GasPrice?: bigint; + newPubdataPrice?: bigint; + customBaseToken?: boolean; + externalPriceApiClientForcedNumerator?: number; + externalPriceApiClientForcedDenominator?: number; + externalPriceApiClientForcedFluctuation?: number; + baseTokenPricePollingIntervalMs?: number; + baseTokenAdjusterL1UpdateDeviationPercentage?: number; +} export class NodeSpawner { + private readonly generalConfigPath: string | undefined; + private readonly originalConfig: string | undefined; + public mainNode: Node | null; + public constructor( private readonly pathToHome: string, private readonly logs: fs.FileHandle, private readonly fileConfig: FileConfig, private readonly options: MainNodeSpawnOptions, private env?: ProcessEnvOptions['env'] - ) {} + ) { + this.mainNode = null; + if (fileConfig.loadFromFile) { + this.generalConfigPath = getConfigPath({ + pathToHome, + chain: fileConfig.chain, + configsFolder: 'configs', + config: 'general.yaml' + }); + this.originalConfig = fsSync.readFileSync(this.generalConfigPath, 'utf8'); + } + } + + public async killAndSpawnMainNode(configOverrides: MainNodeOptions | null = null): Promise { + if (this.mainNode != null) { + await this.mainNode.killAndWaitForShutdown(); + this.mainNode = null; + } + this.mainNode = await this.spawnMainNode(configOverrides); + } - public async spawnMainNode(newL1GasPrice?: string, newPubdataPrice?: string): Promise> { + private async spawnMainNode(overrides: MainNodeOptions | null): Promise> { const env = this.env ?? process.env; const { fileConfig, pathToHome, options, logs } = this; - const testMode = newPubdataPrice || newL1GasPrice; + const testMode = overrides?.newPubdataPrice != null || overrides?.newL1GasPrice != null; - console.log('New L1 Gas Price: ', newL1GasPrice); - console.log('New Pubdata Price: ', newPubdataPrice); + console.log('Overrides: ', overrides); if (fileConfig.loadFromFile) { - setTransactionSlots(pathToHome, fileConfig, testMode ? 1 : 8192); + this.restoreConfig(); + const config = this.readFileConfig(); + config['state_keeper']['transaction_slots'] = testMode ? 1 : 8192; - if (newL1GasPrice) { - setInternalEnforcedL1GasPrice(pathToHome, fileConfig, parseFloat(newL1GasPrice)); - } else { - deleteInternalEnforcedL1GasPrice(pathToHome, fileConfig); - } + if (overrides != null) { + if (overrides.newL1GasPrice) { + config['eth']['gas_adjuster']['internal_enforced_l1_gas_price'] = overrides.newL1GasPrice; + } + + if (overrides.newPubdataPrice) { + config['eth']['gas_adjuster']['internal_enforced_pubdata_price'] = overrides.newPubdataPrice; + } + + if (overrides.externalPriceApiClientForcedNumerator !== undefined) { + config['external_price_api_client']['forced_numerator'] = + overrides.externalPriceApiClientForcedNumerator; + } + + if (overrides.externalPriceApiClientForcedDenominator !== undefined) { + config['external_price_api_client']['forced_denominator'] = + overrides.externalPriceApiClientForcedDenominator; + } + + if (overrides.externalPriceApiClientForcedFluctuation !== undefined) { + config['external_price_api_client']['forced_fluctuation'] = + overrides.externalPriceApiClientForcedFluctuation; + } + + if (overrides.baseTokenPricePollingIntervalMs !== undefined) { + const cacheUpdateInterval = overrides.baseTokenPricePollingIntervalMs / 2; + // To reduce price polling interval we also need to reduce base token receipt checking and tx sending sleeps as they are blocking the poller. Also cache update needs to be reduced appropriately. + + config['base_token_adjuster']['l1_receipt_checking_sleep_ms'] = + overrides.baseTokenPricePollingIntervalMs; + config['base_token_adjuster']['l1_tx_sending_sleep_ms'] = overrides.baseTokenPricePollingIntervalMs; + config['base_token_adjuster']['price_polling_interval_ms'] = + overrides.baseTokenPricePollingIntervalMs; + config['base_token_adjuster']['price_cache_update_interval_ms'] = cacheUpdateInterval; + } - if (newPubdataPrice) { - setInternalEnforcedPubdataPrice(pathToHome, fileConfig, parseFloat(newPubdataPrice)); - } else { - deleteInternalEnforcedPubdataPrice(pathToHome, fileConfig); + if (overrides.baseTokenAdjusterL1UpdateDeviationPercentage !== undefined) { + config['base_token_adjuster']['l1_update_deviation_percentage'] = + overrides.baseTokenAdjusterL1UpdateDeviationPercentage; + } } + + this.writeFileConfig(config); } else { env['DATABASE_MERKLE_TREE_MODE'] = 'full'; - if (newPubdataPrice) { - env['ETH_SENDER_GAS_ADJUSTER_INTERNAL_ENFORCED_PUBDATA_PRICE'] = newPubdataPrice; - } + if (overrides != null) { + if (overrides.newPubdataPrice) { + env['ETH_SENDER_GAS_ADJUSTER_INTERNAL_ENFORCED_PUBDATA_PRICE'] = + overrides.newPubdataPrice.toString(); + } - 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 (overrides.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'] = overrides.newL1GasPrice.toString(); + } + + if (overrides.externalPriceApiClientForcedNumerator !== undefined) { + env['EXTERNAL_PRICE_API_CLIENT_FORCED_NUMERATOR'] = + overrides.externalPriceApiClientForcedNumerator.toString(); + } + + if (overrides.externalPriceApiClientForcedDenominator !== undefined) { + env['EXTERNAL_PRICE_API_CLIENT_FORCED_DENOMINATOR'] = + overrides.externalPriceApiClientForcedDenominator.toString(); + } + + if (overrides.externalPriceApiClientForcedFluctuation !== undefined) { + env['EXTERNAL_PRICE_API_CLIENT_FORCED_FLUCTUATION'] = + overrides.externalPriceApiClientForcedFluctuation.toString(); + } + + if (overrides.baseTokenPricePollingIntervalMs !== undefined) { + const cacheUpdateInterval = overrides.baseTokenPricePollingIntervalMs / 2; + // To reduce price polling interval we also need to reduce base token receipt checking and tx sending sleeps as they are blocking the poller. Also cache update needs to be reduced appropriately. + env['BASE_TOKEN_ADJUSTER_L1_RECEIPT_CHECKING_SLEEP_MS'] = + overrides.baseTokenPricePollingIntervalMs.toString(); + env['BASE_TOKEN_ADJUSTER_L1_TX_SENDING_SLEEP_MS'] = + overrides.baseTokenPricePollingIntervalMs.toString(); + env['BASE_TOKEN_ADJUSTER_PRICE_POLLING_INTERVAL_MS'] = + overrides.baseTokenPricePollingIntervalMs.toString(); + env['BASE_TOKEN_ADJUSTER_PRICE_CACHE_UPDATE_INTERVAL_MS'] = cacheUpdateInterval.toString(); + } + + if (overrides.baseTokenAdjusterL1UpdateDeviationPercentage !== undefined) { + env['BASE_TOKEN_ADJUSTER_L1_UPDATE_DEVIATION_PERCENTAGE'] = + overrides.baseTokenAdjusterL1UpdateDeviationPercentage.toString(); + } } if (testMode) { @@ -175,6 +271,26 @@ export class NodeSpawner { await waitForNodeToStart(proc, options.apiWeb3JsonRpcHttpUrl); return new Node(proc, options.apiWeb3JsonRpcHttpUrl, NodeType.MAIN); } + + public restoreConfig() { + if (this.generalConfigPath != void 0 && this.originalConfig != void 0) + fsSync.writeFileSync(this.generalConfigPath, this.originalConfig, 'utf8'); + } + + private readFileConfig() { + if (this.generalConfigPath == void 0) + throw new Error('Trying to set property in config while not in file mode'); + const generalConfig = fsSync.readFileSync(this.generalConfigPath, 'utf8'); + return YAML.parse(generalConfig); + } + + private writeFileConfig(config: any) { + if (this.generalConfigPath == void 0) + throw new Error('Trying to set property in config while not in file mode'); + + const newGeneralConfig = YAML.stringify(config); + fsSync.writeFileSync(this.generalConfigPath, newGeneralConfig, 'utf8'); + } } async function waitForNodeToStart(proc: ChildProcessWithoutNullStreams, l2Url: string) { diff --git a/core/tests/ts-integration/tests/base-token.test.ts b/core/tests/ts-integration/tests/base-token.test.ts index 8ecc9de3ddb9..432ce70ae17f 100644 --- a/core/tests/ts-integration/tests/base-token.test.ts +++ b/core/tests/ts-integration/tests/base-token.test.ts @@ -39,9 +39,8 @@ describe('base ERC20 contract checks', () => { const numerator = Number(await zksyncContract.baseTokenGasPriceMultiplierNominator()); const denominator = Number(await zksyncContract.baseTokenGasPriceMultiplierDenominator()); - // checking that the numerator and denominator don't have their default values - expect(numerator).toBe(3); - expect(denominator).toBe(2); + expect(numerator).toBe(314); + expect(denominator).toBe(1000); }); test('Can perform a deposit', async () => { diff --git a/core/tests/ts-integration/tests/fees.test.ts b/core/tests/ts-integration/tests/fees.test.ts index e99d3b67911b..fc156e03f16d 100644 --- a/core/tests/ts-integration/tests/fees.test.ts +++ b/core/tests/ts-integration/tests/fees.test.ts @@ -15,13 +15,15 @@ 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 { SYSTEM_CONTEXT_ADDRESS, getTestContract, waitForNewL1Batch, anyTransaction } 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 { sleep } from 'utils/build'; import { killPidWithAllChilds } from 'utils/build/kill'; +import path from 'path'; +import { NodeSpawner } from '../src/utils'; +import { sendTransfers } from '../src/context-owner'; +import { Reporter } from '../src/reporter'; declare global { var __ZKSYNC_TEST_CONTEXT_OWNER__: TestContextOwner; @@ -60,13 +62,13 @@ testFees('Test fees', function () { let tokenDetails: Token; let aliceErc20: zksync.Contract; + let isETHBasedChain: boolean; 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, '../../../..'); @@ -121,11 +123,41 @@ testFees('Test fees', function () { baseTokenAddress }); - mainNode = await mainNodeSpawner.spawnMainNode(); + await mainNodeSpawner.killAndSpawnMainNode(); alice = testMaster.mainAccount(); tokenDetails = testMaster.environment().erc20Token; aliceErc20 = new ethers.Contract(tokenDetails.l1Address, zksync.utils.IERC20, alice.ethWallet()); + + const mainWallet = new zksync.Wallet( + testMaster.environment().mainWalletPK, + alice._providerL2(), + alice._providerL1() + ); + + isETHBasedChain = baseTokenAddress == zksync.utils.ETH_ADDRESS_IN_CONTRACTS; + + // On non ETH based chains the standard deposit is not enough to run all this tests + if (!isETHBasedChain) { + const depositTx = await mainWallet.deposit({ + token: baseTokenAddress, + amount: ethers.parseEther('100'), + approveERC20: true, + approveBaseERC20: true + }); + await depositTx.wait(); + await Promise.all( + await sendTransfers( + zksync.utils.ETH_ADDRESS, + mainWallet, + { alice: alice.privateKey }, + ethers.parseEther('100'), + undefined, + undefined, + new Reporter() + ) + ); + } }); test('Test all fees', async () => { @@ -177,8 +209,10 @@ testFees('Test fees', function () { ]; 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()); + await mainNodeSpawner.killAndSpawnMainNode({ + newL1GasPrice: gasPrice, + newPubdataPrice: gasPrice + }); reports = await appendResults( alice, @@ -213,6 +247,96 @@ testFees('Test fees', function () { console.log(`Full report: \n\n${reports.join('\n\n')}`); }); + test('Test gas price expected value', async () => { + const l1GasPrice = 2_000_000_000n; /// set to 2 gwei + await mainNodeSpawner.killAndSpawnMainNode({ + newL1GasPrice: l1GasPrice, + newPubdataPrice: l1GasPrice + }); + + // wait for new batch so gas price is updated with new config set above + await waitForNewL1Batch(alice); + + const receipt = await anyTransaction(alice); + + const feeParams = await alice._providerL2().getFeeParams(); + const feeConfig = feeParams.V2.config; + // type is missing conversion_ratio field + const conversionRatio: { numerator: bigint; denominator: bigint } = (feeParams.V2 as any)['conversion_ratio']; + if (isETHBasedChain) { + expect(conversionRatio.numerator).toBe(1); //number not bigint for some reason + expect(conversionRatio.denominator).toBe(1); + } else { + expect(conversionRatio.numerator).toBeGreaterThan(1n); + } + + // the minimum + compute overhead of 0.01gwei in validium mode + const expectedETHGasPrice = + feeConfig.minimal_l2_gas_price + + (feeConfig.compute_overhead_part * feeParams.V2.l1_gas_price * feeConfig.batch_overhead_l1_gas) / + feeConfig.max_gas_per_batch; + const expectedConvertedGasPrice = + (expectedETHGasPrice * conversionRatio.numerator) / conversionRatio.denominator; + + expect(receipt.gasPrice).toBe(BigInt(expectedConvertedGasPrice)); + }); + + test('Test base token ratio fluctuations', async () => { + const l1GasPrice = 2_000_000_000n; /// set to 2 gwei + + if (isETHBasedChain) return; + + await mainNodeSpawner.killAndSpawnMainNode({ + newL1GasPrice: l1GasPrice, + newPubdataPrice: l1GasPrice, + externalPriceApiClientForcedNumerator: 300, + externalPriceApiClientForcedDenominator: 100, + externalPriceApiClientForcedFluctuation: 20, + baseTokenPricePollingIntervalMs: 1000, + baseTokenAdjusterL1UpdateDeviationPercentage: 0 + }); + + const beginFeeParams = await alice._providerL2().getFeeParams(); + const mainContract = await alice.getMainContract(); + const beginL1Nominator = await mainContract.baseTokenGasPriceMultiplierNominator(); + let changedL2 = false; + let changedL1 = false; + for (let i = 0; i < 20; i++) { + await sleep(0.5); + const newFeeParams = await alice._providerL2().getFeeParams(); + // we need any as FeeParams is missing existing conversion_ratio field + + if ( + ((newFeeParams.V2 as any)['conversion_ratio'].numerator as number) != + ((beginFeeParams.V2 as any)['conversion_ratio'].numerator as number) + ) { + // @ts-ignore + const diff = + (newFeeParams.V2 as any)['conversion_ratio'].numerator - + (beginFeeParams.V2 as any)['conversion_ratio'].numerator; + // Deviation is 20%, Adding 5% extra for any arithmetic precision issues, 25%*300 = 75 + expect(diff).toBeLessThan(75); + expect(diff).toBeGreaterThan(-75); + changedL2 = true; + break; + } + } + expect(changedL2).toBeTruthy(); + for (let i = 0; i < 10; i++) { + const newL1Nominator = await mainContract.baseTokenGasPriceMultiplierNominator(); + if (newL1Nominator != beginL1Nominator) { + const diff = newL1Nominator - beginL1Nominator; + expect(diff).toBeLessThan(75); // as above + expect(diff).toBeGreaterThan(-75); + changedL1 = true; + break; + } + await sleep(0.5); + } + + expect(changedL1).toBeTruthy(); + }); + test('Test gas consumption under large L1 gas price', async () => { if (testMaster.environment().l1BatchCommitDataGeneratorMode === DataAvailabityMode.Validium) { // We skip this test for Validium mode, since L1 gas price has little impact on the gasLimit in this mode. @@ -233,11 +357,10 @@ testFees('Test fees', function () { // that the gasLimit is indeed over u32::MAX, which is the most important tested property. const requiredPubdataPrice = minimalL2GasPrice * 100_000n; - await mainNode.killAndWaitForShutdown(); - mainNode = await mainNodeSpawner.spawnMainNode( - requiredPubdataPrice.toString(), - requiredPubdataPrice.toString() - ); + await mainNodeSpawner.killAndSpawnMainNode({ + newL1GasPrice: requiredPubdataPrice, + newPubdataPrice: requiredPubdataPrice + }); const l1Messenger = new ethers.Contract(zksync.utils.L1_MESSENGER_ADDRESS, zksync.utils.L1_MESSENGER, alice); @@ -278,16 +401,11 @@ testFees('Test fees', function () { }); afterAll(async () => { - await mainNode.killAndWaitForShutdown(); + await mainNodeSpawner.killAndSpawnMainNode(); // Returning the pubdata price to the default one - - // Restore defaults - setTransactionSlots(pathToHome, fileConfig, 8192); - deleteInternalEnforcedL1GasPrice(pathToHome, fileConfig); - deleteInternalEnforcedPubdataPrice(pathToHome, fileConfig); - mainNode = await mainNodeSpawner.spawnMainNode(); + // Spawning with no options restores defaults. await testMaster.deinitialize(); - __ZKSYNC_TEST_CONTEXT_OWNER__.setL2NodePid(mainNode.proc.pid!); + __ZKSYNC_TEST_CONTEXT_OWNER__.setL2NodePid(mainNodeSpawner.mainNode!.proc.pid!); }); }); diff --git a/core/tests/ts-integration/tests/utils.ts b/core/tests/ts-integration/tests/utils.ts deleted file mode 100644 index 24df8a170c20..000000000000 --- a/core/tests/ts-integration/tests/utils.ts +++ /dev/null @@ -1,81 +0,0 @@ -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/env/base/external_price_api.toml b/etc/env/base/external_price_api.toml index bb22e86c432b..fe88e71e82a2 100644 --- a/etc/env/base/external_price_api.toml +++ b/etc/env/base/external_price_api.toml @@ -6,5 +6,5 @@ source = "forced" [external_price_api_client.forced] -numerator = 3 -denominator = 2 +numerator = 314 +denominator = 1000 diff --git a/etc/env/file_based/general.yaml b/etc/env/file_based/general.yaml index 5abee904765b..94758d92e180 100644 --- a/etc/env/file_based/general.yaml +++ b/etc/env/file_based/general.yaml @@ -299,8 +299,8 @@ base_token_adjuster: external_price_api_client: source: "forced" client_timeout_ms: 10000 - forced_numerator: 3 - forced_denominator: 2 + forced_numerator: 314 + forced_denominator: 1000 house_keeper: diff --git a/zkstack_cli/crates/zkstack/src/commands/chain/common.rs b/zkstack_cli/crates/zkstack/src/commands/chain/common.rs index e0aa0b4e0470..0c35b3ee4fe0 100644 --- a/zkstack_cli/crates/zkstack/src/commands/chain/common.rs +++ b/zkstack_cli/crates/zkstack/src/commands/chain/common.rs @@ -27,6 +27,9 @@ pub async fn distribute_eth( if let Some(deployer) = chain_wallets.deployer { addresses.push(deployer.address) } + if let Some(setter) = chain_wallets.token_multiplier_setter { + addresses.push(setter.address) + } common::ethereum::distribute_eth( wallets.operator, addresses,