परिचय
एक Ethereum access list ट्रांजेक्शन पहले से यह घोषित करके कि किस कॉन्ट्रैक्ट और स्टोरेज स्लॉट को एक्सेस किया जाएगा, क्रॉस-कॉन्ट्रैक्ट कॉल्स (cross-contract calls) पर गैस (gas) बचाने में सक्षम बनाता है। एक्सेस किए गए प्रति स्टोरेज स्लॉट पर 100 गैस तक की बचत की जा सकती है।
इस EIP को पेश करने का उद्देश्य EIP 2929 में ब्रेकिंग चेंजेस (breaking changes) को कम करना था, जिसने कोल्ड स्टोरेज एक्सेस (cold storage access) की लागत बढ़ा दी थी। EIP 2929 ने अंडरप्राइस्ड (underpriced) स्टोरेज एक्सेस ऑपरेशंस को ठीक किया, जो डिनायल ऑफ सर्विस (denial of service) हमलों का कारण बन सकते थे। हालाँकि, कोल्ड स्टोरेज एक्सेस की लागत बढ़ने से कुछ स्मार्ट कॉन्ट्रैक्ट्स टूट गए (broke), इसलिए इसे कम करने के लिए EIP 2930: Optional Access Lists पेश किया गया।
इन कॉन्ट्रैक्ट्स को फिर से काम करने लायक (unbrick) बनाने के लिए, EIP 2930 पेश किया गया, जिससे स्टोरेज स्लॉट्स को “प्री-वार्म” (pre-warmed) किया जा सके। यह कोई संयोग नहीं है कि EIP 2929 और EIP 2930 एक के बाद एक (contiguous) आते हैं।
लेखक
यह लेख RareSkills के एक ब्लॉकचेन रिसर्चर Jesse Raymond (LinkedIn, Twitter) द्वारा सह-लिखित है। इस तरह के मुफ्त उच्च-गुणवत्ता वाले लेखों का समर्थन करने के लिए, और अधिक एडवांस Ethereum डेवलपमेंट कॉन्सेप्ट्स सीखने के लिए, कृपया हमारा Solidity Bootcamp देखें।
यह कैसे काम करता है
एक EIP-2930 ट्रांजेक्शन किसी भी अन्य ट्रांजेक्शन की तरह ही किया जाता है, सिवाय इसके कि कोल्ड स्टोरेज लागत का भुगतान SLOAD ऑपरेशन के निष्पादन (execution) के दौरान करने के बजाय, डिस्काउंट के साथ अग्रिम (upfront) रूप से किया जाता है।
इसके लिए Solidity कोड में किसी भी संशोधन की आवश्यकता नहीं होती है और यह पूरी तरह से क्लाइंट-साइड (client-side) पर निर्दिष्ट (specified) होता है।
यह शुल्क स्टोरेज स्लॉट के कोल्ड एक्सेस का पूर्व भुगतान (prepay) करता है ताकि वास्तविक निष्पादन के दौरान, केवल वार्म (warm) शुल्क का ही भुगतान किया जाए। जब स्टोरेज कुंजियाँ (storage keys) पहले से ज्ञात होती हैं, तो Ethereum नोड क्लाइंट्स स्टोरेज वैल्यूज़ को प्री-फेच (pre-fetch) कर सकते हैं, जिससे कंप्यूट (compute) और स्टोरेज एक्सेस के बीच कुछ पैरेललाइजेशन (parallelization) की अनुमति मिलती है।
EIP-2930 एक्सेस लिस्ट के बाहर स्टोरेज एक्सेस को नहीं रोकता है; एक्सेस लिस्ट में एड्रेस-स्टोरेज संयोजन (address-storage combination) डालना इसका उपयोग करने की कोई प्रतिबद्धता (commitment) नहीं है। हालाँकि, इसका परिणाम बिना किसी उद्देश्य के कोल्ड स्टोरेज लोड (cold storage load) के लिए पूर्व-भुगतान करना होगा।
एक्सेस के लिए कम शुल्क लेना
EIP 2930 के अनुसार, Berlin हार्ड फोर्क ने अकाउंट एक्सेस ऑपकोड (account access opcodes) (जैसे BALANCE, सभी CALL(s), और EXT\*) की “कोल्ड” (cold) लागत को बढ़ाकर 2600 कर दिया और स्टेट एक्सेस ऑपकोड (state access opcode) (SLOAD) की “कोल्ड” लागत को 800 से बढ़ाकर 2100 कर दिया, जबकि दोनों के लिए “वार्म” (warm) लागत को घटाकर 100 कर दिया।
हालाँकि, EIP-2930 का एक अतिरिक्त लाभ यह है कि यह ट्रांजेक्शन के 200 गैस डिस्काउंट के कारण ट्रांजेक्शन लागत को कम करता है।
परिणामस्वरूप, क्रमशः CALL और SLOAD के लिए 2600 और 2100 गैस का भुगतान करने के बजाय, ट्रांजेक्शन में कोल्ड एक्सेस के लिए केवल 2400 और 1900 गैस की आवश्यकता होती है, और बाद के वार्म एक्सेस में केवल 100 गैस का खर्च आएगा।
एक्सेस लिस्ट ट्रांजेक्शन को लागू करना
इस सेक्शन में, हम एक एक्सेस लिस्ट लागू करेंगे, एक सामान्य ट्रांजेक्शन की तुलना EIP-2930 ट्रांजेक्शन से करेंगे, और कुछ गैस बेंचमार्क (gas benchmarks) प्रदान करेंगे।
आइए उस कॉन्ट्रैक्ट पर एक नज़र डालते हैं जिसे हम कॉल करने जा रहे हैं।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Calculator {
uint public x = 20;
uint public y = 20;
function getSum() public view returns (uint256) {
return x + y;
}
}
contract Caller {
Calculator calculator;
constructor(address \_calc) {
calculator = Calculator(\_calc);
}
// call the getSum function in the calculator contract
function callCalculator() public view returns (uint sum) {
sum = calculator.getSum();
}
}
हम निम्नलिखित स्क्रिप्ट के साथ लोकल hardhat नोड पर कॉन्ट्रैक्ट्स को डिप्लॉय (deploy) करेंगे और उनके साथ इंटरैक्ट (interact) करेंगे।
import { ethers } from "hardhat";
async function main() {
const [user] = await ethers.getSigners();
const data = "0xf4acc7b5"; // function selector for `callCalculator()`
const Calculator = await ethers.getContractFactory("Calculator");
const calculator = await Calculator.deploy();
await calculator.deployed();
console.log(`Calc contract deployed to ${calculator.address}`);
const Caller = await ethers.getContractFactory("Caller");
const caller = await Caller.deploy(calculator.address);
await caller.deployed();
console.log(`Caller contract deployed to ${caller.address}`);
const tx1 = {
from: user.address,
to: caller.address,
data: data,
value: 0,
type: 1,
accessList: [
{
address: calculator.address,
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000000",
"0x0000000000000000000000000000000000000000000000000000000000000001",
],
},
],
};
const tx2 = {
from: user.address,
to: caller.address,
data: data,
value: 0,
};
console.log("============== transaction with access list ==============");
const txCall = await user.sendTransaction(tx1);
const receipt = await txCall.wait();
console.log(
`gas cost for tx with access list: ${receipt.gasUsed.toString()}`
);
console.log("============== transaction without access list ==============");
const txCallNA = await user.sendTransaction(tx2);
const receiptNA = await txCallNA.wait();
console.log(
`gas cost for tx without access list: ${receiptNA.gasUsed.toString()}`
);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
एक्सेस लिस्ट के ठीक ऊपर 1 की वैल्यू वाला type सेक्शन यह निर्दिष्ट करता है कि यह ट्रांजेक्शन एक एक्सेस लिस्ट ट्रांजेक्शन है।
accessList उन ऑब्जेक्ट्स का एक ऐरे (array) है जिनमें वे एड्रेस और स्टोरेज स्लॉट्स होते हैं जिन्हें ट्रांजेक्शन एक्सेस करेगा।
कोड में परिभाषित स्टोरेज स्लॉट्स या storageKeys एक 32 बाइट्स की वैल्यू होनी चाहिए; यही कारण है कि हमारे पास वहां बहुत सारे शुरुआती शून्य (leading zeros) हैं।
हमारे पास स्टोरेज कीज़ (keys) के रूप में शून्य और एक के लिए 32 बाइट्स वैल्यू हैं क्योंकि getSum फ़ंक्शन जिसे हम Caller कॉन्ट्रैक्ट के माध्यम से कॉल करते हैं, Calculator कॉन्ट्रैक्ट में इन्हीं सटीक स्टोरेज स्लॉट्स को एक्सेस करता है। विशेष रूप से, x स्टोरेज स्लॉट शून्य में है और y स्टोरेज स्लॉट एक में है।
परिणाम
हमें निम्नलिखित आउटपुट मिलता है
Compiled 1 Solidity file successfully
Calc contract deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
Caller contract deployed to 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
============== transaction with access list ==============
gas cost for tx with access list: 30934
============== transaction without access list ==============
gas cost for tx without access list: 31234
हम देख सकते हैं कि हमने 300 गैस की बचत की (यह ऑप्टिमाइज़र सेटिंग की परवाह किए बिना सच होगा)।
बाहरी (external) कॉन्ट्रैक्ट के कॉल ने 200 गैस की बचत की, और दो स्टोरेज एक्सेस ने 200-200 की बचत की, जिससे 600 की संभावित बचत हुई। हालाँकि, वार्म एक्सेस का भुगतान अभी भी किया जाना चाहिए, और बाहरी कॉल तथा दो स्टोरेज वेरिएबल्स के लिए एक वार्म एक्सेस होता है, इन तीनों ऑपरेशनों में से प्रत्येक की लागत 100-100 गैस है। इस प्रकार, शुद्ध (net) बचत 300 गैस है।
विशिष्ट रूप से कहें तो, हमारे उदाहरण में यह फ़ॉर्मूला इस प्रकार काम करता है:
एक्सेस लिस्ट के बिना एक्सेस की लागत 2600 + 2100 2 = 6800 गैस होती।
लेकिन चूंकि हमने एक्सेस लिस्ट के लिए 2400 + 1900 2 = 6200 गैस का पूर्व-भुगतान किया है, इसलिए हमने वार्म एक्सेस के लिए केवल 100 + 100 2 = 300 गैस का भुगतान किया। तो हमने 6200 + 300 = 6500 गैस का भुगतान किया, जबकि हमने 6800 गैस खर्च की होती, जिससे 300 गैस की शुद्ध बचत हुई।
एक्सेस लिस्ट ट्रांजेक्शन के स्टोरेज स्लॉट्स प्राप्त करना
स्टोरेज स्लॉट को आसानी से निर्धारित करने के लिए Go-Ethereum (geth) क्लाइंट के पास eth_createAccessList rpc मेथड है (उदाहरण के लिए web3.js एपीआई देखें)।
RPC मेथड के साथ, क्लाइंट एक्सेस किए गए स्टोरेज स्लॉट्स को निर्धारित करता है और एक्सेस लिस्ट लौटाता है।
हम इस RPC मेथड का उपयोग foundry में cast access-list कमांड के साथ भी कर सकते हैं, जो बैकग्राउंड में eth_createAccessList का उपयोग करता है और एक्सेस लिस्ट लौटाता है।
आइए नीचे एक उदाहरण आज़माएँ; हम “allPairs” फ़ंक्शन को कॉल करके UniswapV2 फ़ैक्टरी कॉन्ट्रैक्ट (Göerli नेटवर्क में) के साथ इंटरैक्ट करेंगे, जो पास किए गए इंडेक्स (index) के आधार पर एक ऐरे से एक पेयर (pair) कॉन्ट्रैक्ट लौटाता है।
हम फोर्क किए गए (forked) Göerli टेस्टनेट में निम्नलिखित कमांड चलाते हैं।
cast access-list 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f "allPairs(uint256)" 0
यह ट्रांजेक्शन की एक्सेस लिस्ट लौटाएगा, और यदि यह सफल हुआ तो हमारे टर्मिनल में यह कुछ इस तरह दिखेगा।
gas used: 27983 // amount of gas used by the transaction
access-list:
- address: 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f // address of the uniswapv2 factory
keys:
0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b // slot of the pair address
0x0000000000000000000000000000000000000000000000000000000000000003 // slot of the array length
एक्सेस लिस्ट के साथ गैस बर्बाद करने का उदाहरण
यदि स्टोरेज स्लॉट की गणना गलत तरीके से की जाती है, तो ट्रांजेक्शन एक्सेस लिस्ट के लिए डिपॉजिट का भुगतान करेगा और इसके लिए कोई लाभ नहीं मिलेगा। निम्नलिखित उदाहरण में, हम एक गलत तरीके से calculated ethereum एक्सेस लिस्ट ट्रांजेक्शन को बेंचमार्क करेंगे।
निम्नलिखित बेंचमार्क स्लॉट 1 के लिए पूर्व-भुगतान करेगा जबकि वास्तव में स्लॉट 0 का उपयोग किया गया है।
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract Wrong {
uint256 private x = 1;
function getX() public view returns (uint256) {
return x;
}
}
आइए इसका परीक्षण करें। हम गलत स्टोरेज स्लॉट के साथ एक्सेस लिस्ट का उपयोग करके getX() फ़ंक्शन को कॉल करेंगे और फिर इसकी तुलना एक सामान्य ट्रांजेक्शन से करेंगे जो एक्सेस लिस्ट को निर्दिष्ट नहीं करता है।
लोकल hardhat नोड में कॉन्ट्रैक्ट को डिप्लॉय करने और चलाने के लिए यह स्क्रिप्ट है।
import { ethers } from "hardhat";
async function main() {
const [user] = await ethers.getSigners();
const data = "0x5197c7aa"; // function selector for the `getX` function
const Slot = await ethers.getContractFactory("Wrong");
const slot = await Slot.deploy();
await slot.deployed();
console.log(`Slot contract deployed to ${slot.address}`);
const badtx = {
from: user.address,
// to: calculator.address,
to: slot.address,
data: data,
value: 0,
type: 1,
accessList: [
{
address: slot.address,
storageKeys: [
"0x0000000000000000000000000000000000000000000000000000000000000001", // wrong slot number
],
},
],
};
const badTxResult = await user.sendTransaction(badtx);
const badTxReceipt = await badTxResult.wait();
console.log(
`gas cost for incorrect access list: ${badTxReceipt.gasUsed.toString()}`
);
const normaltx = {
from: user.address,
// to: calculator.address,
to: slot.address,
data: data,
value: 0,
};
const normalTxResult = await user.sendTransaction(normaltx);
const normalTxReceipt = await normalTxResult.wait();
console.log(
`gas cost for tx without access list: ${normalTxReceipt.gasUsed.toString()}`
);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
परिणाम इस प्रकार हैं:
Slot contract deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3
gas cost for incorrect access list: 27610
gas cost for tx without access list: 23310
हालाँकि हमारे पास गलत स्टोरेज स्लॉट था फिर भी ट्रांजेक्शन पूरा हो गया, हालाँकि गलत गणना की गई एक्सेस लिस्ट का उपयोग करने के बजाय एक्सेस लिस्ट का उपयोग न करना अधिक सस्ता होता।
जब स्टोरेज स्लॉट्स डिटरमिनिस्टिक (deterministic) न हों तो एक्सेस लिस्ट का उपयोग न करें
पिछले सेक्शन का अर्थ यह है कि जब एक्सेस किए गए स्टोरेज स्लॉट्स नॉन-डिटरमिनिस्टिक (non-deterministic) हों तो एक्सेस लिस्ट का उपयोग नहीं किया जाना चाहिए।
उदाहरण के लिए, यदि हम किसी निश्चित ब्लॉक नंबर के आधार पर निर्धारित स्टोरेज स्लॉट नंबर का उपयोग करते हैं, तो स्टोरेज स्लॉट आम तौर पर प्रेडिक्टेबल (predictable) नहीं होगा।
एक अन्य उदाहरण ऐसे स्टोरेज स्लॉट्स का है जो इस बात पर निर्भर करते हैं कि ट्रांजेक्शन कब हुआ। ERC-721 के कुछ कार्यान्वयन (implementations) मालिक के एड्रेस को एक ऐरे में पुश करते हैं और NFT ओनरशिप की पहचान करने के लिए ऐरे इंडेक्स का उपयोग करते हैं। परिणामस्वरूप, किसी टोकन का स्टोरेज स्लॉट उस क्रम (order) पर निर्भर करता है जिसमें उपयोगकर्ताओं ने मिंट (mint) किया है और इसकी भविष्यवाणी नहीं की जा सकती है।
एक्सेस लिस्ट गैस की बचत कब करती है?
जब भी आप कोई क्रॉस-कॉन्ट्रैक्ट कॉल करते हैं, तो एक्सेस लिस्ट ट्रांजेक्शन का उपयोग करने पर विचार करें
क्रॉस-कॉन्ट्रैक्ट कॉल करने पर आम तौर पर अतिरिक्त 2600 गैस लगती है, लेकिन एक एक्सेस लिस्ट ट्रांजेक्शन का उपयोग करने पर 2400 गैस लगती है और यह कॉन्ट्रैक्ट एक्सेस को प्री-वार्म कर देता है ताकि इसका केवल 100 गैस शुल्क लगे, जिसका अर्थ है कि शुद्ध लागत 2600 से घटकर 2500 हो जाती है।
यह किसी अन्य कॉन्ट्रैक्ट में स्टोरेज वेरिएबल्स को एक्सेस करने पर भी लागू होता है। इसमें आमतौर पर कोल्ड एक्सेस के लिए 2100 का खर्च आता है, लेकिन एक एक्सेस लिस्ट ट्रांजेक्शन स्टोरेज स्लॉट को प्री-वार्म करने के लिए 1900 गैस का भुगतान करता है, जिससे शुद्ध 100 गैस की बचत होती है।
हम इस repo में सामान्य क्रॉस-कॉन्ट्रैक्ट कॉल्स के लिए एक्सेस लिस्ट ट्रांजेक्शन के और उदाहरण प्रदान करते हैं, जैसे:
- Chainlink oracle में मूल्य (price) एक्सेस करना,
- एक कार्यान्वयन (implementation) कॉन्ट्रैक्ट पर delegatecall करने वाला एक proxy,
- कॉन्ट्रैक्ट-टू-कॉन्ट्रैक्ट कॉल के माध्यम से ERC-20 ट्रांसफर करना।
एक्सेस लिस्ट ट्रांजेक्शन का उपयोग कब नहीं करना चाहिए
किसी स्मार्ट कॉन्ट्रैक्ट को सीधे कॉल करने के लिए कोई “अतिरिक्त शुल्क” नहीं है, यह उस 21,000 गैस में शामिल होता है जिसका भुगतान सभी ट्रांजेक्शन को करना होता है। इसलिए, एक्सेस लिस्ट उन ट्रांजेक्शन के लिए कोई लाभ प्रदान नहीं करती है जो केवल एक स्मार्ट कॉन्ट्रैक्ट को एक्सेस करते हैं।
निष्कर्ष
EIP-2930 Ethereum एक्सेस लिस्ट ट्रांजेक्शन प्रति स्टोरेज स्लॉट 200 गैस तक बचाने का एक त्वरित तरीका है जब किसी क्रॉस-कॉन्ट्रैक्ट कॉल के एड्रेस और स्टोरेज स्लॉट की भविष्यवाणी की जा सकती है। इसका उपयोग तब नहीं किया जाना चाहिए जब कोई क्रॉस-कॉन्ट्रैक्ट कॉल नहीं किया जाता है या जब एड्रेस और स्टोरेज स्लॉट पेयर (pair) डिटरमिनिस्टिक नहीं होते हैं।
अधिक जानें
अधिक एडवांस Solidity कॉन्सेप्ट्स के लिए, हमारा Solidity Bootcamp देखें।
मूल रूप से 27 मार्च, 2023 को प्रकाशित