मध्य 2024 के लिए अपडेट: Dencun अपग्रेड के बाद से, calldata ऑप्टिमाइजेशन का उतना प्रभाव नहीं पड़ता है क्योंकि अधिकांश L2s पर ट्रांजैक्शन calldata के बजाय blobs पर स्टोर किए जाते हैं। हम इस लेख को ऐतिहासिक उद्देश्यों के लिए रख रहे हैं।
L2 पर एप्लिकेशन विकसित करते समय, अधिकांश gas की लागत calldata से आती है। इसलिए, L2 के लिए gas ऑप्टिमाइजेशन उस लागत को कम करने पर जोर देता है।
यह लेख बताता है कि calldata ऑप्टिमाइजेशन कैसे काम करता है, कुछ उदाहरण प्रदान करता है, और चेन-विशिष्ट तकनीकों पर चर्चा करता है।
पूर्वापेक्षाएँ (Prerequisites)
- पाठक को Solidity और Ethereum Virtual Machine (EVM) से परिचित होना चाहिए।
- पाठक को कम से कम कुछ सरल gas optimization तकनीकों के बारे में पता होना चाहिए।
- पाठक को यह पता होना चाहिए कि ABI एन्कोडिंग/डिकोडिंग क्या है, सीखने के लिए video on ABI encoding एक अच्छा शुरुआती बिंदु है।
लेखक (Authorship)
यह लेख Rati Montreewat (LinkedIn, Twitter) द्वारा लिखा गया है, जो एक ब्लॉकचेन इंजीनियर और Solid Grinder (एक L2 calldata ऑप्टिमाइजेशन टूल) के लेखक हैं, और RareSkills Solidity Bootcamp के पूर्व छात्र (alumni) हैं।
Calldata की लागत
Ethereum calldata के प्रत्येक बाइट के लिए चार्ज करता है, शून्य बाइट (zero byte) के लिए Gtxdatazero और गैर-शून्य बाइट (non-zero byte) के लिए Gtxdatanonzero, जो क्रमशः 4 gas और 16 gas हैं, जैसा कि यलोपेपर (yellowpaper) द्वारा दिखाया गया है:

Layer 2s calldata को layer 1 पर पोस्ट करते हैं, इसलिए उन्हें layer 1 calldata की लागत का भुगतान करना पड़ता है। इसके अलावा, layer 2 एक अतिरिक्त “सिक्योरिटी फीस (security fee)” लगाता है।
गणितीय रूप से, कुल layer2 ट्रांजैक्शन की gas को इस प्रकार परिभाषित किया गया है:

L1 gas अक्सर कुल gas लागत (L1 + L2 gas) का 90% से 99% तक हो सकती है। यह ध्यान दिया जाना चाहिए कि ये आंकड़े L1 पर नेटवर्क congestion (भीड़) पर बहुत अधिक निर्भर करते हैं।
विभिन्न L2 चेन्स के लिए अलग-अलग नियम
हालांकि यह सच है कि L2s पर खर्च की गई अधिकांश gas डेटा/सिक्योरिटी हिस्से से आती है, लेकिन विभिन्न L2s पर स्मार्ट कॉन्ट्रैक्ट्स का एक ही सेट अलग-अलग gas परिणाम दे सकता है। ऐसा इसलिए है क्योंकि विभिन्न L2 चेन्स (जैसे Arbitrum/ Optimism/ Starknet आदि) अलग-अलग नियमों और फॉर्मूलों का उपयोग करके यह गणना करती हैं कि वे L1 लागत के अतिरिक्त calldata के लिए उपयोगकर्ता से कितना शुल्क लेंगी। इसलिए, यदि कोई एक gas ऑप्टिमाइजेशन विधि किसी एक L2 चेन पर इष्टतम (optimal) परिणाम देती है, तो इसका मतलब यह नहीं है कि यह अन्य L2 चेन्स पर भी समान इष्टतम परिणाम देगी।
इसके अलावा, जैसे-जैसे क्लाइंट और Ethereum इकोसिस्टम समय के साथ परिपक्व हो रहा है, ये नियम भी विकसित हो रहे हैं। उदाहरण के लिए, EIP4844 (उर्फ Proto-Danksharding) gas के L2 डेटा/सिक्योरिटी घटक को और भी सस्ता बना देगा और L2 निष्पादन (execution) हिस्से को अधिक महत्वपूर्ण बना देगा, जिसके परिणामस्वरूप उचित प्रोत्साहन (incentive) और आर्थिक मॉडल को दर्शाने के लिए L2 निष्पादन शुल्क की गणना के तरीके में संभावित बदलाव हो सकते हैं।
यहां बताया गया है कि विभिन्न L2s के ट्रांजैक्शन gas की गणना कैसे की जाती है:
Arbitrum
नीचे वह फॉर्मूला दिया गया है जिसका उपयोग Arbitrum किसी ट्रांजैक्शन की gas लागत की गणना करने के लिए करता है:

ExecutionFee की गणना उसी तरह की जाती है जैसे EVM चेन पर ट्रांजैक्शन की गणना की जाती है, सिवाय इसके कि यह एक PriceFloor के अधीन होता है।
Arbitrum, calldata को L1 पर पोस्ट करने से पहले Brotli algorithm का उपयोग करके उसे कंप्रेस करने का प्रयास करता है।
Optimism
Optimism के पास calldata के लिए शुल्क लेने का थोड़ा अलग मॉडल है:

आप नीले रंग से रेखांकित (underlined) शब्दों को Ethereum द्वारा लिए जाने वाले शुल्क के रूप में और लाल रंग से रेखांकित शब्दों को Optimism के प्रॉफिट मार्जिन के रूप में सोच सकते हैं।
Calldata को ऑप्टिमाइज़ करने के तरीके
calldata घटक के लिए आवश्यक gas की मात्रा निर्धारित करने वाला प्रमुख कारक calldata का आकार है, और यह ABI encoding नियम द्वारा निर्दिष्ट किया जाता है। विशेष रूप से, ABI (Application Binary Interface), जैसा कि Solidity’s Official Documentation के अनुसार है।
calldata फ़ॉर्मेटिंग को समझने का सबसे अच्छा तरीका एक उदाहरण है।
सबसे पहले, आइए cast इंस्टॉल करें, जो EVM के साथ इंटरैक्ट करने के लिए एक टूलकिट है, और हम टूलचेन इंस्टॉलर के रूप में Foundryup का उपयोग करते हैं:
curl -L https://foundry.paradigm.xyz | bash
foundryup
फिर हम निम्नलिखित cast कमांड का उपयोग करते हैं जो दिखाता है कि Solidity तर्कों (arguments) के साथ फ़ंक्शन को कैसे एन्कोड करती है:
cast calldata "addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)" 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD 1200000000000000000000 2500000000000000000000 1000000000000000000000 2000000000000000000000 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD 100
परिणाम में कुल बाइट्स की संख्या 520 हेक्साडेसिमल है = 520/2 = 260 बाइट्स:
0xe8e33700000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000000000000000000000410d586a20a4c000000000000000000000000000000000000000000000000000878678326eac90000000000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000006c6b935b8bbd400000000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead0000000000000000000000000000000000000000000000000000000000000064
जैसा कि आप देख सकते हैं, calldata के पहले 4 बाइट्स फ़ंक्शन सिग्नेचर (addLiquidity(address,…)) के Keccak256 हैश के पहले चार बाइट्स हैं। function selector के बाद, 32 बाइट्स के अगले चंक्स फ़ंक्शन के तर्क (arguments) होते हैं। यदि तर्क 32 बाइट्स से छोटा है, तो यह डिफ़ॉल्ट रूप से 32 बाइट्स के अंदर फिट होने के लिए अतिरिक्त शून्यों (zeroes) के साथ “left padded” (बाईं ओर पैड) किया जाता है।
इसे स्पष्ट करने के लिए, calldata के चंक्स को इस प्रकार विभाजित किया जा सकता है:
- 0xe8e33700 function selector के रूप में
- 000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD के address के रूप में
- 000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD के address के रूप में
- 0000000000000000000000000000000000000000000000410d586a20a4c00000 1200000000000000000000 के uint256 के रूप में
- 0000000000000000000000000000000000000000000000878678326eac900000 2500000000000000000000 के uint256 के रूप में
- 00000000000000000000000000000000000000000000003635c9adc5dea00000 1000000000000000000000 के uint256 के रूप में
- 00000000000000000000000000000000000000000000006c6b935b8bbd400000 2000000000000000000000 के uint256 के रूप में
- 000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddeaddead 0xdeaDDeADDEaDdeaDdEAddEADDEAdDeadDEADDEaD के address के रूप में
- 0000000000000000000000000000000000000000000000000000000000000064 64 के uint256 के रूप में
जानकारी खोए बिना calldata के कुल बाइट्स को कम करने की कई तकनीकें हैं। कांसेप्ट यह है कि calldata को कम से कम बाइट्स का उपयोग करने के लिए कॉम्पैक्ट तरीके से एन्कोड करने का प्रयास किया जाए। फिर, एन्कोड किए गए डेटा को बाद में उपयोग योग्य प्रारूप में डिकोड किया जाता है।
calldata को कंप्रेस करके बचाई गई gas की तुलना में calldata को डीकंप्रेस करने का ओवरहेड (overhead) आमतौर पर नगण्य (negligible) होता है।
यहां चर्चा की गई ट्रिक्स हर संभव संदर्भ में काम नहीं करेंगी। बचाए गए बाइट्स की मात्रा स्मार्ट कॉन्ट्रैक्ट में विशिष्ट व्यावसायिक तर्क (business logic) पर बहुत अधिक निर्भर करती है।
सबसे अधिक उपयोग किए जाने वाले बाइट्स को बायपास करना
यदि हम फ़ंक्शन सिग्नेचर या फ़ंक्शन पैरामीटर का सटीक मान जानते हैं, तो हम आवश्यकता होने पर बाद में उपयोग करने के लिए उन्हें constant के रूप में हार्ड-कोड (hard-code) कर सकते हैं। चूंकि हमारे पास उदाहरण के लिए फ़ंक्शन्स का एक सीमित सेट है, इसलिए उन्हें पहचानने के लिए हमें पूरे चार बाइट्स की आवश्यकता नहीं है।
हम फ़ैक्टरी पैटर्न (factory pattern) का उपयोग करके स्मार्ट कॉन्ट्रैक्ट का सेट इस तरह डिज़ाइन कर सकते हैं कि फ़ैक्टरी विधियों (methods) और मापदंडों (parameters) के प्रत्येक संयोजन के साथ एक अद्वितीय कॉन्ट्रैक्ट डिप्लॉय करे। कुछ gas ऑप्टिमाइजेशन के उदाहरण नीचे दिए गए हैं:
Function signature
हम कॉन्ट्रैक्ट में केवल fallback फ़ंक्शन का उपयोग करके calldata के 4 बाइट्स बचा सकते हैं:
fallback() external payable {
// business logic
}
Function parameters
हम फ़ंक्शन से एक पैरामीटर हटाकर calldata के 32 बाइट्स बचा सकते हैं।
उदाहरण के लिए, ERC20 कॉन्ट्रैक्ट के address को constant के रूप में हार्डकोड किया जा सकता है और इसे फ़ंक्शन से हटाया जा सकता है। यह संभावित रूप से कुल 20 गैर-शून्य बाइट्स (address आकार के समान) और 12 शून्य बाइट्स (पूर्ण 32 बाइट्स के लिए पैड किए गए बाइट्स) को बचा सकता है।
address public constant USDC = <address>;
function TEST() external {
// business logic using USDC
}
यदि आप उत्सुक हैं और व्यवहार में कार्यान्वयन (implementation) के बारे में अधिक जानने की इच्छा रखते हैं तो आप दिलचस्प डिज़ाइनों के साथ निम्नलिखित प्रोजेक्ट्स को देख सकते हैं:
एड्रेस टेबल (address table) का उपयोग करके एड्रेस को कैश (Caching) करना
AddressTable को एक कैश्ड (cached) डेटाबेस के रूप में माना जा सकता है जो एक id का उपयोग करके पहले से रजिस्टर्ड एड्रेस को स्टोर करता है।
उदाहरण के लिए, उपयोगकर्ता पहले एड्रेस को रजिस्टर करता है, फिर एड्रेस अपने आप एक id के साथ मैप हो जाता है। बाद में, उपयोगकर्ता पूर्ण एड्रेस के बजाय केवल id का उपयोग कर सकता है। इसके परिणामस्वरूप calldata का आकार 20 बाइट्स से बहुत कम होकर केवल कुछ बाइट्स रह जाता है।
पर्दे के पीछे (Behind the scenes), टेबल केवल एक स्मार्ट कॉन्ट्रैक्ट है जो एड्रेस और इंडेक्स के बीच की मैपिंग को स्टोर करता है। इसमें प्रासंगिक मैप किए गए id का उपयोग करके रजिस्टर्ड एड्रेस को खोजने (look up) की कार्यक्षमता भी है।
इस डिज़ाइन को Arbitrum द्वारा अपनाया और लागू किया गया है। इंटरफ़ेस इस प्रकार है:
interface ArbAddressTable {
/**
* @notice Check whether an address exists in the address table
* @param addr address to check for presence in table
* @return true if address is in table
*/
function addressExists(address addr) external view returns (bool);
/**
* @notice compress an address and return the result
* @param addr address to compress
* @return compressed address bytes
*/
function compress(address addr) external returns (bytes memory);
/**
* @notice read a compressed address from a bytes buffer
* @param buf bytes buffer containing an address
* @param offset offset of target address
* @return resulting address and updated offset into the buffer (revert if buffer is too short)
*/
function decompress(bytes calldata buf, uint256 offset)
external
view
returns (address, uint256);
/**
* @param addr address to lookup
* @return index of an address in the address table (revert if address isn't in the table)
*/
function lookup(address addr) external view returns (uint256);
/**
* @param index index to lookup address
* @return address at a given index in address table (revert if index is beyond end of table)
*/
function lookupIndex(uint256 index) external view returns (address);
/**
* @notice Register an address in the address table
* @param addr address to register
* @return index of the address (existing index, or newly created index if not already registered)
*/
function register(address addr) external returns (uint256);
/**
* @return size of address table (= first unused index)
*/
function size() external view returns (uint256);
}
हालाँकि, इसका कार्यान्वयन (implementation) एक प्रीकंपाइल (precompile) कॉन्ट्रैक्ट है जो Go में लिखा गया है। आप यहाँ OffchainLabs की git repository देख सकते हैं। इसका उद्देश्य एक सिंगल यूनिवर्सल एड्रेस टेबल होना है जहाँ कोई भी रजिस्टर कर सकता है और इसका उपयोग कर सकता है।
यदि आप इसके एप्लिकेशन के साथ Solidity में लिखा गया एक अन्य कार्यान्वयन देखना चाहते हैं। इस Solid Grinder की git repository में UniswapV2 का संशोधित संस्करण शामिल है, जो अपना स्वयं का address table अपनाता है।
Data Serialization
Data Serialization मापदंडों (parameters) को पर्याप्त डेटा आकार के साथ सही प्रकार (type) में सीरियलाइज़ (serializing) और डीसीरियलाइज़ (deserializing) करके काम करता है।
उदाहरण के लिए, यदि हम uint256 के बजाय uint40 (5 बाइट्स) प्रकार के साथ समय अवधि को तर्कों के रूप में भेजकर calldata को कम करने का विकल्प चुनते हैं, तो calldata को सही ऑफ़सेट पर स्लाइस (sliced) किया जाना चाहिए और परिणाम (शून्य बाइट्स हटाए जाने के बाद) को अगले चरणों में सही ढंग से उपयोग किया जा सकता है।
आइए Solid Grinder द्वारा कार्यान्वयन को फिर से here देखें। यह कॉन्ट्रैक्ट एक अच्छा शुरुआती बिंदु है:

यह डिकोडर फ़ंक्शन Uniswapv2 के लिए एप्लिकेशन-विशिष्ट है और यह मूल अनऑप्टिमाइज़्ड फ़ंक्शन को देखकर Solid Grinder के CLI के साथ जनरेट किया गया है। इस मामले में, यह UniswapV2Router02 है। मूल रूप से, आप प्रयोग कर सकते हैं और यहाँ Quick Start पर विस्तृत चरणों का पालन कर सकते हैं।
ट्रेडऑफ़्स (Tradeoffs)
उपरोक्त calldata gas ऑप्टिमाइजेशन ट्रिक्स के सबसे स्पष्ट ट्रेडऑफ़ पठनीयता (readability) और जटिलता (complexity) हैं। उदाहरण के लिए,
स्मार्ट कॉन्ट्रैक्ट में एन्कोड और डिकोड लॉजिक जोड़ना और स्पष्ट रूप से फ़ंक्शन के मापदंडों को हटाना न केवल उन उपयोगकर्ताओं को भ्रमित करेगा जो सीधे Etherscan के माध्यम से कॉन्ट्रैक्ट के साथ इंटरैक्ट करते हैं, बल्कि उन डेवलपर्स के लिए भी काम कठिन बना देगा जो आपके संशोधित स्मार्ट कॉन्ट्रैक्ट के ऊपर (on-top) निर्माण करना चाहते हैं, जिससे कंपोज़िबिलिटी (composability) कम हो जाएगी जो कि परमिशनलेस (permissionless) दुनिया की अनूठी ताकत है।
अंतिम विचार
जैसा कि पहले बताया गया है, calldata gas ऑप्टिमाइजेशन एक नया विषय है, लेकिन हाल ही में यह अधिक प्रासंगिक होता जा रहा है क्योंकि layer 2/rollup तकनीक अधिक मुख्यधारा (mainstream) में आ रही है। इसके अलावा, अभी भी कोई स्पष्ट मानक और अभ्यास नहीं है। यह लेख केवल संभावित डिज़ाइन निर्णयों और दृष्टिकोणों को प्रदान और प्रस्तावित करता है। इस प्रतिमान (paradigm) को फिर से आविष्कार करने की बहुत गुंजाइश है।
संदर्भ (References)
- https://docs.arbitrum.io/arbos/l1-pricing#l1-fee-collection
- https://docs.arbitrum.io/stylus/reference/opcode-hostio-pricing#opcode-costs
- https://github.com/OffchainLabs/arbitrum-tutorials/tree/master/packages/address-table
- https://community.optimism.io/docs/developers/build/transaction-fees/
- https://scopelift.co/blog/calldata-optimizooooors
- https://github.com/clabby/op-kompressor
- https://github.com/Ratimon/solid-grinder
मूल रूप से 30 जनवरी, 2024 को प्रकाशित