diff --git a/content/tutorials/max-gas-pub-data/10.index.md b/content/tutorials/max-gas-pub-data/10.index.md new file mode 100644 index 00000000..ebf72935 --- /dev/null +++ b/content/tutorials/max-gas-pub-data/10.index.md @@ -0,0 +1,425 @@ +# Understanding Pubdata Costs in the ZKsync Era + +ZKsync Era, as a state diff-based ZK rollup, introduces a unique approach to charging for pubdata. + +Unlike traditional rollups that publish transaction data (calldata), ZKsync Era publishes state changes as pubdata. This includes: + +- Modified storage slots +- Smart contract bytecodes +- L2 → L1 messages and logs + +The charge is calculated as `pubdata_bytes_published * gas_per_pubdata` directly from the context of the execution. +This approach allows for more efficient handling of applications that frequently modify the same storage slot, such as oracles. +These applications can update storage slots multiple times while maintaining a constant footprint on L1 pubdata. + +## Key Differences from Ethereum + +Before diving into ZKsync Era's model, it's important to understand key differences from Ethereum's gas model: + +1. **Volatile L1 Gas Prices**: L2 transaction prices depend on fluctuating L1 gas prices, making it impossible to use hardcoded gas costs. +2. **Zero-Knowledge Proofs**: As a zkRollup, ZKsync Era must prove every operation with zero-knowledge proofs, adding complexity to the fee structure. +3. **Separation of Computation and Pubdata Costs**: Unlike Ethereum, where all costs are represented by gas, +ZKsync Era distinguishes between computational costs and pubdata costs. + +## ZKsync Era's Approach to Pubdata Costs + +ZKsync Era has developed a unique solution for charging pubdata costs in a state diff rollup: + +1. **Dynamic Pricing**: Pubdata costs are calculated dynamically based on current L1 gas prices. +2. **Post-Execution Pubdata Charging**: Users are charged for pubdata based on a counter that tracks pubdata usage during transaction execution. +This includes storage writes, bytecode deployment, and L2->L1 messages. The final cost depends on the amount of pubdata used and the gas price +per pubdata unit, which is determined at the time of execution. +3. **Efficient for Repeated Operations**: Applications that repeatedly modify the same storage slots benefit from this model, +as they're not charged for each individual update. + +## How ZKsync Era Charges for Pubdata (Process) + +ZKsync Era employs a sophisticated method for charging pubdata costs, addressing several challenges inherent to state diff-based rollups: + +- **Post-Charging Approach**: Instead of pre-charging for pubdata (which is impossible due to the nature of state diffs), +ZKsync Era uses a post-charging mechanism. +- **Pubdata Counter**: ZKsync Era uses a pubdata counter that tracks potential pubdata usage during transaction execution. +This counter is modified by the operator for storage writes (which can be positive or negative) and incremented by system contracts for L1 data publication. +The counter can revert along with other state changes. The final value of this counter, combined with the gas price per pubdata unit, determines the +pubdata cost of the transaction. + - The system maintains a counter that tracks how much pubdata has been spent during a transaction. +- **Execution Process**: + 1. The current pubdata spent is recorded as basePubdataSpent. + 2. Transaction validation is executed. + 3. The system checks if (getPubdataSpent() - basePubdataSpent) * gasPerPubdata <= gasLeftAfterValidation. + 4. If the check fails, the transaction is rejected (not included in the block). → Note this one. + 5. The main transaction is executed. + 6. The pubdata check is repeated. If it fails at this stage, the transaction is reverted (user pays for computation but no state changes occur). + 7. If a paymaster is used, steps d-f are repeated for the paymaster's postTransaction method. +- **Pubdata Counter Modifications**: + - **Storage writes:** The operator specifies the increment for the pubdata counter. Note that this value can be negative if, for example, + the storage diff is being reversed, such as in the case of a reentrancy guard. + - **Publishing bytes to L1:** The counter is incremented by the number of bytes published. + - **Transaction revert:** The pubdata counter value reverts along with storage and events. +- **Advantages of Post-Charging**: + - Removes unnecessary overhead. + - Decouples execution gas from data availability gas. + - Eliminates caps on `gasPerPubdata.` + +## Implications for Users and Developers + +1. **Cost Predictability**: While costs may vary with L1 gas prices, users can estimate costs based on the state changes their transactions will cause. +2. **Optimization Opportunities**: Developers can optimize their applications to minimize state changes, potentially reducing users' costs. +3. **Efficient for Certain Use Cases**: Applications like oracles or high-frequency trading platforms may find this model particularly cost-effective. +4. **Transaction Behavior**: Users should be aware that transactions may be rejected or reverted based on +pubdata costs, even if they seem to have sufficient gas for execution. +5. **Flexible Pricing**: The absence of hard caps on `gasPerPubdata` allows for more flexible pricing models. + +The following section provides practical insights into measuring gas costs and setting the `DEFAULT_GAS_PER_PUBDATA_LIMIT` in the ZKsync era. + +## Measuring Gas Costs and Setting Different Values for Pubdata Gas Limit + +This guide demonstrates how to measure gas costs and set the `DEFAULT_GAS_PER_PUBDATA_LIMIT` in the ZKsync Era using a practical example: +a ZKFest Voting Contract. **What is Max Gas Per Pubdata?** Max gas per pubdata is a value attached to each transaction on ZKsync Era, +representing the maximum amount of gas a user is willing to pay for each byte of pubdata (public data) published on Ethereum. + +**Key points:** + +1. Default value: 50,000 gas per pubdata byte +2. Can be customized per transaction; in this case, we're testing three cases +3. Affects transaction success and cost + +## Objective + +We'll deploy and interact with a smart contract using various gas settings to understand how different parameters affect transaction execution and +costs on ZKsync Era. + +## Running the Experiment + +1. Set up your environment by creating a new project with ZKsync CLI +2. Create the ZKFestVoting contract and the deploy script based on the code below +3. Analyze the output for each scenario, paying attention to: + - Successful transactions and their gas usage + - Rejected transactions and their error messages + +## Step 1: Smart Contract (ZKFestVoting.sol) + +```solidity [ZKFestVoting.sol] +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +contract ZKFestVoting { + enum Stage { Culture, DeFi, ElasticChain } + mapping(address => uint8) public participation; + event Voted(address indexed voter, Stage stage); + + function vote(string memory stageName) external { + uint256 stageIndex = getStageIndex(stageName); + require(stageIndex < 3, "Invalid stage"); + uint256 stageBit = 1 << stageIndex; + require((participation[msg.sender] & stageBit) == 0, "Already voted for this stage"); + participation[msg.sender] |= uint8(stageBit); + emit Voted(msg.sender, Stage(stageIndex)); + } + + // Helper functions omitted for brevity +} +``` + +
+Full code for ZKFestVoting.sol +
+ + ```solidity + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.17; + + contract ZKFestVoting { + // Enum: A user-defined type that consts of a set of named constants, we're defining the stages of ZKFest + enum Stage { Culture, DeFi, ElasticChain } + + // Mapping: A key-value data structure that allows for efficient data lookup + mapping(address => uint8) public participation; + + // Event: A way to emit logs on the blockchain, useful for off-chain applications + event Voted(address indexed voter, Stage stage); + + /* + Vote function: allows a user to vote for a stage + - converts stage name to index + - checks for valid state and prevents double voting + - uses bit manipulation (1 << stageIndex) to efficiently store votes in single uint8 + - updates participation using bitwise OR (|=) + */ + function vote(string memory stageName) external { + uint256 stageIndex = getStageIndex(stageName); + require(stageIndex < 3, "Invalid stage"); + uint256 stageBit = 1 << stageIndex; // 1 left shifted by stageIndex + + // Check if the user has already voted for the stage, the participation is checking the transaction sender and the stageBit is the stage the user is voting for + require((participation[msg.sender] & stageBit) == 0, "Already voted for this stage"); + + // we're updating the participation mapping with the stageBit, the |= is the bitwise OR assignment operator, it's used to update the participation mapping with the stageBit + participation[msg.sender] |= uint8(stageBit); + + // emit does not store data, it only logs the event, in this case it's logging the voter and the stage + emit Voted(msg.sender, Stage(stageIndex)); + } + + // Helper function to get the index of the stage + function getStageIndex(string memory stageName) internal pure returns (uint256) { + // the goal of the if statements is to convert the stageName to an index, the keccak256 is a hash function that converts the stageName to a hash, + // and we're checking if the hash of the stageName is equal to the hash of the string "Culture" + // we're using bytes to convert the stageName to a byte array, because keccak256 expects a byte array + if (keccak256(bytes(stageName)) == keccak256(bytes("Culture"))) return 0; + if (keccak256(bytes(stageName)) == keccak256(bytes("DeFi"))) return 1; + if (keccak256(bytes(stageName)) == keccak256((bytes("ElasticChain")))) return 2; + revert("Invalid stage name"); + } + + function hasVoted(address voter, Stage stage) external view returns (bool) { + return (participation[voter] & (1 << uint256(stage))) != 0; + // explain in detail what the above line does: + // 1. participation[voter] is the bitmask of the voter's votes, bitmask in solidity is a way to store multiple boolean values in a single variable + // 2. (1 << uint256(stage)) is the bitmask of the stage, it's a 1 left shifted by the stage index + // 3. & is the bitwise AND operator, it's used to check if the voter has voted for the stage + // 4. != 0 is used to check if the voter has voted for the stage + } + + function voterStages(address voter) external view returns (bool[3] memory) { + uint8 participationBits = participation[voter]; + + return [ + participationBits & (1 << 0) != 0, // Check Culture stage + participationBits & (1 << 1) != 0, // Check DeFi stage + participationBits & (1 << 2) != 0 // Check ElsticChain stage + ]; + } + } + ``` + +
+ +
+This contract uses bit manipulation to store voting data efficiently, minimizing storage costs and pubdata usage. + +The `ZKFestVoting` contract allows participants to vote for different stages of ZKFest. + +**Key features:** + +- Supports voting for three stages: Culture, DeFi, and ElasticChain. +- Prevents double voting for the same stage. +- Tracks voter participation across all stages. + +## Step 2: Deployment Script (deploy.ts) + +```tsx +import { deployContract, getProvider, getWallet } from "./utils"; +import { Deployer } from "@matterlabs/hardhat-zksync"; +import * as hre from "hardhat"; +import { ethers } from "ethers"; +import { utils } from "zksync-ethers"; + +export default async function () { + const wallet = getWallet(); + const provider = getProvider(); + const deployer = new Deployer(hre, wallet); + + // Deploy the contract + const artifact = await deployer.loadArtifact("ZKFestVoting"); + const deploymentFee = await deployer.estimateDeployFee(artifact, []); + console.log(`Estimated deployment fee: ${ethers.formatEther(deploymentFee)} ETH`); + + const contract = await deployContract("ZKFestVoting", [], { + wallet: wallet, + noVerify: false + }); + + const contractAddress = await contract.getAddress(); + console.log(`ZKFestVoting deployed to: ${contractAddress}`); + + // Test different scenarios + const stageNames = ["Culture", "DeFi", "ElasticChain"]; + let currentStageIndex = 0; + + const sendAndExplainTx = async (gasPerPubdata: string | number, gasLimit: string | number) => { + // Implementation details omitted for brevity + } + + const createCustomTx = async (gasPerPubdata: string | number, gasLimit: string | number, stageName: string) => { + // Implementation details omitted for brevity + } + + // Test different scenarios + await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "2000000"); + await sendAndExplainTx("100", "2000000"); // Very low gasPerPubdata + await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "100000000"); // Very high gasLimit +} +``` + +
+Full code for deploy.ts script +
+ + ```tsx + import { deployContract, getProvider, getWallet } from "./utils"; + import { Deployer } from "@matterlabs/hardhat-zksync"; + + import * as hre from "hardhat"; + import { ethers } from "ethers"; + import { utils } from "zksync-ethers"; + + export default async function () { + console.log("Deploying ZKFestVoting contract..."); + + // Get the wallet to deploy from + const wallet = getWallet(); + const provider = getProvider(); + console.log(`Deploying from address: ${wallet.address}`); + + const deployer = new Deployer(hre, wallet); + + try { + // Deploy the contract + // Note: We're not passing any constructor arguments here + const artifact = await deployer.loadArtifact("ZKFestVoting"); + const deploymentFee = await deployer.estimateDeployFee(artifact, []); + console.log(`Estimated deployment fee: ${ethers.formatEther(deploymentFee)} ETH`); + + const contract = await deployContract("ZKFestVoting", [], { + wallet: wallet, + // Set to false if you want the contract to be verified automatically + noVerify: false + }); + + const contractAddress = await contract.getAddress(); + console.log(`ZKFestVoting deployed to: ${contractAddress}`); + + // Test different scenarios + const stageNames = ["Culture", "DeFi", "ElasticChain"]; + let currentStageIndex = 0; + + const sendAndExplainTx = async (gasPerPubdata: string | number, gasLimit: string | number) => { + console.log(`\nTesting with gasPerPubdata: ${gasPerPubdata}, gasLimit: ${gasLimit}`); + try { + const stageName = stageNames[currentStageIndex]; + currentStageIndex++; + + const customTx = await createCustomTx(gasPerPubdata, gasLimit, stageName); + console.log(`Voting for stage: ${stageName}`); + + console.log("Custom transaction created, attempting to send..."); + + const txResponse = await wallet.sendTransaction(customTx); + console.log(`Transaction sent. Hash: ${txResponse.hash}`); + console.log("Transaction sent, waiting for confirmation..."); + + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Transaction confirmation timeout")), 60000) // 60 second timeout + ); + + // const receipt = await txResponse.wait(); + const receiptPromise = txResponse.wait(); + + // const receipt = await Promise.race([receiptPromise, timeoutPromise]); + const receipt = await Promise.race([receiptPromise, timeoutPromise]) as ethers.TransactionReceipt; + + console.log("Transaction successful!"); + console.log(`Gas used: ${receipt.gasUsed}`); + } catch (error) { + console.log("Transaction failed!"); + console.error("Error details:", error); + if (error instanceof Error) { + console.error("Error message:", error.message); + } + } + } + + const createCustomTx = async (gasPerPubdata: string | number, gasLimit: string | number, stageName: string) => { + + // const voteFunctionData = contract.interface.encodeFunctionData("vote", [0]); // Vote for Culture stage + const voteFunctionData = contract.interface.encodeFunctionData("vote", [stageName]); // Vote for Culture stage + // we add stageName as a string to the array because the encodeFunctionData expects an array + + const gasPrice = await provider.getGasPrice(); + + let customTx = { + to: contractAddress, + from: wallet.address, + data: voteFunctionData, + gasLimit: ethers.getBigInt(gasLimit), + gasPrice: gasPrice, + chainId: (await provider.getNetwork()).chainId, + nonce: await provider.getTransactionCount(wallet.address), + type: 113, + customData: { + gasPerPubdata: ethers.getBigInt(gasPerPubdata) + }, + value: ethers.getBigInt(0), + }; + + return customTx; + } + + // Test different scenarios + await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "2000000"); + await sendAndExplainTx("100", "2000000"); // Very low gasPerPubdata, + await sendAndExplainTx(utils.DEFAULT_GAS_PER_PUBDATA_LIMIT, "100000000"); // Very high gasLimit + + // // Get and log the transaction receipt for the vote + // const receipt = await deployContract + } catch (error) { + console.error("Deployment or interaction failed: ", error); + process.exitCode = 1; + } + } + ``` + +
+
+ +This deployment script serves as a practical example of how to interact with ZKsync Era, showcasing the impact +of different gas settings on transaction execution and costs. + +This script deploys the contract and tests scenarios with varying `gasPerPubdata` and `gasLimit` values. + +**The script includes:** + +1. Deploy the `ZKFestVoting` contract. +2. Execute a series of test votes with different `gasPerPubdata` and `gasLimit` settings. +3. Log the results of each transaction, including gas usage and any errors encountered. + +## How It Works + +1. **Transaction Submission**: When sending a transaction, specify the max gas per pubdata. + +2. **Execution**: ZKsync Era executes the transaction and calculates the actual pubdata cost. + +3. **Comparison**: The actual cost is compared against your specified max value. + +4. **Outcome**: + +- If actual cost ≤ specified max: Transaction succeeds +- If actual cost > specified max: Transaction is rejected + - Which in this case, it's happening with the very low gasPerPubdata example `await sendAndExplainTx("100", "2000000");` + +## Key Aspects of Gas Measurement and DEFAULT_GAS_PER_PUBDATA_LIMIT + +- **Deployment Fee Estimation**: +The script estimates the deployment fee before deploying the contract, giving insight into the initial cost. +- **Custom Transaction Creation**: +The `createCustomTx` function allows custom `gasPerPubdata` and `gasLimit` values to be set for each transaction. +- **Testing Different Scenarios**: + - failsDefault `DEFAULT_GAS_PER_PUBDATA_LIMIT` with a moderate `gasLimit` + - Very low `gasPerPubdata` to see how it affects transaction execution + - Default `DEFAULT_GAS_PER_PUBDATA_LIMIT` with a very high `gasLimit` +- **Transaction Monitoring**: +The script logs transaction details, including gas used and any errors encountered. +- **Error Handling**: +Comprehensive error handling and logging provide insights into transaction failures. + +## Key Takeaways + +1. The `DEFAULT_GAS_PER_PUBDATA_LIMIT` (accessed via zksync-ethers library on `utils.DEFAULT_GAS_PER_PUBDATA_LIMIT`) +serves for setting default pubdata gas limits. +2. Very low gas per pubdata values may lead to transaction rejection due to insufficient pubdata gas. + - In this case, the transaction won’t be included in the blockchain, and the user is not charged anything. +3. Extremely high `gasLimit` values may not necessarily improve transaction success rates but could lead to higher upfront costs. +4. The optimal values depend on the specific operation and current network conditions. + +By experimenting with these parameters, developers can find the right balance between transaction success rate cost efficiency for their ZKsync Era applications. diff --git a/content/tutorials/max-gas-pub-data/_dir.yml b/content/tutorials/max-gas-pub-data/_dir.yml new file mode 100644 index 00000000..7d5527f3 --- /dev/null +++ b/content/tutorials/max-gas-pub-data/_dir.yml @@ -0,0 +1,23 @@ +title: How Max Gas Per Pubdata Works on ZKsync Era +featured: false +authors: + - name: MatterLabs + url: https://matter-labs.io + avatar: https://avatars.githubusercontent.com/u/42489169?s=200&v=4 +github_repo: https://github.com/ZKsync-Community-Hub +tags: + - ZKsync Era Pubdata + - Fee Model +summary: ZKsync Era Pubdata Cost Model Explained +description: + This guide aims to explain how ZKSync Era handles pubdata costs, highlighting the differences from Ethereum's gas + model and exploring the implications for users and developers. +what_you_will_learn: + - Understand ZKSync Era's pubdata cost model. + - Deploy a voting contract on ZKSync Era. + - Experiment with different gas settings on transactions. +updated: 2024-11-25 +tools: + - zksync-cli + - hardhat + - zksync-ethers