ABI encoding वह डेटा फॉर्मेट है जिसका उपयोग स्मार्ट कॉन्ट्रैक्ट्स में function calls करने के लिए किया जाता है। यह वह तरीका भी है जिससे स्मार्ट कॉन्ट्रैक्ट्स अन्य स्मार्ट कॉन्ट्रैक्ट्स को कॉल करते समय डेटा को एन्कोड करते हैं।
यह गाइड बताएगी कि ABI एन्कोडेड डेटा की व्याख्या (interpret) कैसे करें, ABI encoding की गणना (compute) कैसे करें, और function signature तथा ABI encoding के बीच के संबंध को सिखाएगी।
चलिए शुरू करते हैं…
Solidity abi.encodeWithSignature और low-level calls
यदि हमें किसी अन्य स्मार्ट कॉन्ट्रैक्ट में एक public function foo(uint256 x) (आर्गुमेंट के रूप में x = 5 पास करते हुए) के लिए low-level call करनी हो, तो हम निम्नलिखित कार्य करेंगे:
otherContractAddr.call(abi.encodeWithSignature("foo(uint256)", (5));
हम निम्नलिखित कोड के साथ abi.encodeWithSignature("foo(uint256)", (5)) द्वारा लौटाए गए वास्तविक डेटा को देख सकते हैं:
function seeEncoding() external pure returns (bytes memory) {
return abi.encodeWithSignature("foo(uint256)", (5));
}
और हमें निम्नलिखित परिणाम मिलेगा (जो कि ABI एन्कोडेड है):
0x2fbebd380000000000000000000000000000000000000000000000000000000000000005
इस तरह के डेटा की व्याख्या करना और उसे समझना ही इस लेख का लक्ष्य है।
एक ABI encoded function call के प्रमुख घटक
एक एन्कोडेड ABI function call, function selector और function के एन्कोडेड आर्गुमेंट्स (यदि function आर्गुमेंट्स लेता है) का संयोजन (concatenation) होता है।
Function signature
Function signature, function के नाम और उसके आर्गुमेंट प्रकारों (argument types) का बिना स्पेस वाला संयोजन है।
उदाहरण के लिए, नीचे दिए गए function के लिए function signature:
function transfer(address _to, uint256 amount) public {
//
}
transfer(address,uint256) है। ध्यान दें कि आपको पूर्ण आर्गुमेंट डेटा प्रकारों का उपयोग करना चाहिए, जैसे कि uint के बजाय uint256। साथ ही, _to और amount जैसे वेरिएबल नाम function signature का हिस्सा नहीं हैं। यह भी महत्वपूर्ण है कि स्ट्रिंग में कोई स्पेस न हो जैसे कि transfer(address, uint256)।
Solidity documentation के अनुसार, function signature की गणना करते समय कुछ “corner cases” का ध्यान रखना आवश्यक है:
- Structs को tuples की तरह माना जाता है
- Payable addresses, interfaces, और contract types को addresses के रूप में माना जाता है
- “memory” और “calldata” मॉडिफायर्स को इग्नोर कर दिया जाता है
- एक enum, uint8 होता है
- एक user-defined type को उसके underlying type के रूप में माना जाता है
Function selector
function selector केवल एक function signature के Keccak-256 हैश के पहले 4 बाइट्स होते हैं, जिसका उपयोग Solidity किसी function को पहचानने के लिए करता है। उदाहरण के लिए, हमारे पहले बताए गए function signature transfer(address,uint256) का Keccak-256 हैश यह हेक्साडेसिमल (hexadecimal) वैल्यू है:
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b
हालाँकि, हैश परिणाम के केवल पहले 4 बाइट्स 0xa9059cbb का उपयोग function की पहचान करने के लिए किया जाता है; वे चार बाइट्स ही function selector हैं।
आप transfer() function signature को उसके selector में बदलने के लिए ethers JavaScript लाइब्रेरी का उपयोग कर सकते हैं, जैसा कि नीचे दिखाया गया है:
const ethers = require('ethers'); // Ethers v6
const functionSignature = 'transfer(address,uint256)';
const functionSelector = ethers.id(functionSignature).substring(0, 10)
console.log(functionSelector);
इसका परिणाम कुछ इस तरह होगा:

Solidity में, यह function function selector की गणना करता है:
function getSelector() public pure returns (bytes4 ret) {
return bytes4(keccak256("transfer(address,uint256)")); // 0xa9059cbb
}
आप बिना कोई कोड लिखे इस कन्वर्शन को देखने के लिए इस keccak256 conversion website का भी उपयोग कर सकते हैं:

अब जब हमें स्पष्ट समझ हो गई है कि function selector क्या होता है, तो आइए function calls के लिए ABI encoding के अगले घटक पर विचार करें — function inputs या arguments।
Function inputs या arguments
जब किसी ऐसे function को कॉल किया जाता है जो कोई आर्गुमेंट नहीं लेता है, तो function को कॉल करने के लिए केवल function selector ही पर्याप्त एन्कोडिंग होगी। उदाहरण के लिए, function play() को इसके function selector 0x93e84cd9 द्वारा पहचाना जाएगा और यही पूरा आवश्यक डेटा होगा।
हालाँकि, यह जटिल हो जाता है यदि function आर्गुमेंट्स लेता है, जैसे कि transfer(address to, uint256 amount), तब function आर्गुमेंट्स को ABI एन्कोड किया जाना चाहिए और function selector के साथ जोड़ा (concatenate) जाना चाहिए।
आइए आर्गुमेंट एन्कोडिंग कैसे की जाती है, यह समझने के लिए transfer(address to, uint256 amount) को एक निरंतर उदाहरण के रूप में उपयोग करें:
function transfer(address to, uint256 amount) public {
//
}
Function call के लिए यह डेटा function या कॉन्ट्रैक्ट के भीतर स्थायी रूप से स्टोर नहीं होता है। इसके बजाय, यह “calldata” नामक स्पेस में रहता है। आप calldata में डेटा को संशोधित (modify) नहीं कर सकते, क्योंकि यह ट्रांज़ैक्शन भेजने वाले द्वारा बनाया जाता है और फिर read-only हो जाता है।
आप Etherscan में किसी ट्रांज़ैक्शन का calldata देख सकते हैं। नीचे एक स्क्रीनशॉट दिया गया है जो एक उदाहरण ERC-20 token के लिए transfer transaction calldata का उदाहरण दिखाता है:

Etherscan function विवरण के नीचे function selector को MethodID के रूप में संदर्भित करता है (नीचे स्क्रीनशॉट में लाल बॉक्स देखें)। इसलिए transfer() के लिए methodID, 0xa9059cbb है।

इसके बाद दो लंबी हेक्साडेसिमल वैल्यूज़ आती हैं जिन्हें [0] और [1] के रूप में टैग किया गया है। वे हेक्साडेसिमल वैल्यूज़ दो इनपुट डेटा आर्गुमेंट्स को दर्शाती हैं: _to, address और _value, uint256।
Etherscan हमें calldata जानकारी को प्रति लाइन 32-बाइट्स (64 कैरेक्टर्स) में अलग करने और व्याख्या करने में मदद करता है। हालाँकि, वास्तविक calldata एक साथ बंडल किया जाएगा और एक लंबी स्ट्रिंग के रूप में भेजा जाएगा और यह नीचे दिए गए जैसा दिखना चाहिए:
0xa9059cbb000000000000000000000000f89d7b9c864f589bbf53a82105107622b35eaa4000000000000000000000000000000000000000000000028a857425466f800000
आप Etherscan पर “View input as” पर क्लिक करके और नीचे दिखाए गए अनुसार “Original” विकल्प चुनकर वास्तविक संपूर्ण calldata को उसके मूल स्वरूप में भी देख सकते हैं:

बैकग्राउंड में क्या हो रहा है, यह बेहतर ढंग से समझने के लिए, आइए calldata को विभाजित करें और ट्रांज़ैक्शन की प्रासंगिक जानकारी निकालें।
Calldata को विभाजित करना
आइए नीचे दिए गए calldata पर विचार करें और इसके घटकों की पहचान करें:
0xa9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000
सबसे पहले, हमें function signature जानने की आवश्यकता है — उसके बिना हम डेटा को डिकोड नहीं कर सकते। इसलिए, उपरोक्त calldata के लिए function signature यहाँ दिया गया है:
transfer(address,uint256)
फिर हम हेक्साडेसिमल नोटेशन (0x) और function selector को उनकी अपनी लाइन पर रखते हैं। Function selector हमेशा 4 बाइट्स (8 हेक्स कैरेक्टर्स) का होता है। अंत में, हम निम्नलिखित प्रत्येक 32 बाइट्स को उनकी अपनी लाइन में विभाजित करते हैं। जैसा कि हम बाद में देखेंगे, Solidity डेटा को 32-बाइट इंक्रीमेंट में एन्कोड करता है।
0x <---------- Hexadecimal notation
a9059cbb <---- Function selector
0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f44
00000000000000000000000000000000000000000000011c9a62d04ed0c80000
Function selector
Function selector calldata के पहले 4 बाइट्स 0xa9059cbb होता है।

Address
transfer(address,uint256) में address अगला 32 बाइट्स का मान है। वास्तविक address 20 बाइट्स का होता है लेकिन इसे 32 बाइट्स का बनाने के लिए बाईं ओर शून्य (leading zeros) के साथ पैड (left padded) किया जाता है।
Address:
000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f440

मूल रूप से, रिसीवर address उपरोक्त मान होगा लेकिन अतिरिक्त शून्य पैडिंग के बिना: यानी 0x3F5047BDb647Dc39C88625E17BDBffee905A9F44।
Amount
और अंत में, transfer(address,uint256) में अंतिम आइटम amount है। Amount (0000000000000000000011c9a62d04ed0c80000) को 32 बाइट्स का बनाने के लिए बाईं ओर शून्य के साथ पैड किया जाता है, जैसा कि नीचे दिखाया गया है:

यहाँ हेक्साडेसिमल को डेसिमल में जल्दी से बदलने में आपकी मदद करने के लिए एक पायथन स्निपेट दिया गया है:
>>> int("0x11c9a62d04ed0c80000", 16)
5250000000000000000000
और हम डेसिमल को वापस हेक्साडेसिमल में भी बदल सकते हैं जैसा कि नीचे दिखाया गया है:
>>> hex(5250000000000000000000)
0x11c9a62d04ed0c80000
डेटा प्रकार (Data types) और पैडिंग
हमने यह स्थापित किया है कि calldata में प्रत्येक आइटम को 32-बाइट शब्द (word) के रूप में एन्कोड किया जाता है और शून्य के साथ पैड किया जाता है यदि आइटम पूरे 32-बाइट शब्द को नहीं घेरता है।
एक नियम के रूप में, हर निश्चित-आकार (fixed-size) के डेटा प्रकार जैसे कि int, bool, और सभी आकारों (unit8-uint256) के uint को आवश्यकता पड़ने पर बाईं ओर शून्य के साथ पैड किए गए 32 बाइट्स शब्द के रूप में एन्कोड किया जाएगा।
उदाहरण के लिए, यदि आपके पास 5 मान वाला uint8 है, तो इसे इस प्रकार एन्कोड किया जाएगा
0x0000000000000000000000000000000000000000000000000000000000000005।
इसी तरह, true मान वाला एक bool भी बाईं ओर पैड किया जाएगा और इसे 0x0000000000000000000000000000000000000000000000000000000000000001 के रूप में एन्कोड किया जाएगा।
हालाँकि, डायनेमिक आकार (dynamic sized) के डेटा प्रकार bytes और string दाईं ओर पैड (right padded) किए जाते हैं। उदाहरण के लिए, बाइट्स 0x68656c6c6f जो hello को दर्शाता है, दाईं ओर शून्य के साथ पैड किए गए 32-बाइट्स शब्द के रूप में एन्कोड किया जाएगा 0x68656c6c6f000000000000000000000000000000000000000000000000000000।
Solidity में निश्चित-आकार के डेटा प्रकारों में शामिल हैं:
- bool
- uints
- निश्चित आकार के bytes (bytesN)
- address
- tuple, निश्चित डेटा के साथ struct
- निश्चित आकार का array (fixed-size array)
नीचे Solidity में डायनेमिक डेटा प्रकार दिए गए हैं:
- bytes
- string
- डायनेमिक array
- एक निश्चित आकार का array जिसमें डायनेमिक प्रकार होते हैं
- एक struct जिसमें उपरोक्त डायनेमिक प्रकारों में से कोई भी शामिल है
डायनेमिक calldata के साथ काम करना
अब तक, हमारा ध्यान address और uint256 जैसे स्टैटिक calldata आर्गुमेंट प्रकारों पर रहा है। जहाँ स्टैटिक प्रकारों को एन्कोड करना काफी सीधा (straightforward) है, वहीं arrays और strings को एन्कोड करना थोड़ा जटिल हो सकता है क्योंकि वे जो डेटा रखते हैं उसका आकार भिन्न होता है।
आइए एक ऐसे function पर विचार करें जो uints का एक array और एक एकल address लेता है। यद्यपि हमारे function के लिए इम्प्लीमेंटेशन का विवरण यहाँ प्रासंगिक नहीं है, function signature कुछ इस तरह दिखना चाहिए:
transfer(uint256[],address)
अब, आइए अपना ध्यान array को एन्कोड करने की ओर ले जाएँ। मान लें कि हम transfer function को निम्नलिखित डेटा पास कर रहे हैं:
transfer([5769, 14894, 7854], 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1)
यह ऊपर दिए गए हमारे उदाहरण function के लिए calldata है जिसका signature transfer(uint256[],address) है। आइए इसका परीक्षण करें और देखें कि हमारे द्वारा ऊपर बताए गए पैटर्न का पालन करते हुए हर हिस्से को कैसे एन्कोड किया गया है।
सबसे पहले, हम array uint256[] के “offset” को एन्कोड करने से शुरू करेंगे, लेकिन offset क्या होता है?

Offset
Offset का उपयोग calldata के भीतर यह पता लगाने के लिए किया जाता है कि विशिष्ट डायनेमिक डेटा कहाँ से शुरू होता है या कहाँ पाया जा सकता है।
हमारे उदाहरण का अनुसरण करते हुए, हमारे पास एक डायनेमिक डेटा प्रकार uint256[] और एक स्टैटिक प्रकार address है। उपरोक्त calldata में uint256[] का offset हेक्साडेसिमल में 40 (डेसिमल में 64) है और इसकी एन्कोडिंग 32 बाइट शब्दों को घेरती है।
चूँकि डायनेमिक array function का पहला आर्गुमेंट है, इसलिए offset calldata में पहला 32-बाइट शब्द है:

Offset कैसे काम करता है, इसे और स्पष्ट करने के लिए, ऊपर दी गई इमेज यह उजागर करती है कि array का offset calldata में कहाँ स्थित है। प्रत्येक बाइट शब्द को इस प्रकार गिना गया है:
- 0-31 (32 बाइट्स की पहली पंक्ति)
- 32-63 (32 बाइट्स की दूसरी पंक्ति)
- 64-95 (32 बाइट्स की तीसरी पंक्ति)
- आदि
तो 64 (40 हेक्स) तीसरी पंक्ति में सबसे बाईं ओर का बाइट (हेक्स कैरेक्टर्स का जोड़ा) है जहाँ हरा हाइलाइट समाप्त होता है। यही वह जगह है जहाँ offset इशारा कर रहा है।
इस उदाहरण में, offset function selector के बाद के पहले बाइट की शुरुआत से लेकर डायनेमिक डेटा (array) के शुरू होने की जगह तक की दूरी है। हालाँकि, हम बाद में देखेंगे कि offset का मतलब हमेशा “function selector के बाद के पहले बाइट से offset” नहीं होता है।
स्टैटिक डेटा को एन्कोड करना — address
अगली पंक्ति address है, जो एक स्टैटिक 32-बाइट शब्द है जिसे शून्य (leading zeros) के साथ पैड किया गया है। यह वही address है जिसे हमने पहले ही पास किया है, क्योंकि address पहले से ही हेक्साडेसिमल फॉर्मेट में है।

डायनेमिक डेटा की लंबाई को एन्कोड करना — array
अगली पंक्ति array की लंबाई है, जो array में आइटम्स की संख्या है। जैसा कि आप देख सकते हैं, हमारे array में 3 आइटम्स हैं: [5769, 14894, 7854]। Array की लंबाई 3 है जैसा कि नीचे दी गई इमेज में दर्शाया गया है:

Array एलिमेंट्स को Hex में एन्कोड करना
अब तक, हमने स्टैटिक प्रकार, offset, और array की लंबाई को एन्कोड किया है। इसके बाद, आइए वास्तविक array एलिमेंट्स को एन्कोड करें। Array के प्रत्येक एलिमेंट को नीचे दी गई इमेज की तरह एक हेक्साडेसिमल संख्या के रूप में दर्शाया जाएगा:

हम प्रत्येक पूर्णांक (integers) को उनके हेक्साडेसिमल प्रतिनिधित्व में बदलते हैं और उनमें शून्य (leading zeros) जोड़ते हैं। इसलिए, array के आइटम्स निम्नलिखित होंगे:

यह इस ABI एन्कोडिंग के calldata की हमारी चर्चा को पूरा करता है।
निम्नलिखित वीडियो उस हर चीज़ को सारांशित करता है जो हमने transfer(uint[], address) के लिए calldata को एन्कोड करने के तरीके के बारे में सीखा:
एक string आर्गुमेंट की ABI एन्कोडिंग
स्ट्रिंग को एन्कोड करना सीधा है, आपको केवल निम्नलिखित को एन्कोड करने की आवश्यकता है:
- offset
- स्ट्रिंग की लंबाई
- स्ट्रिंग की सामग्री (UTF-8 एन्कोडेड)
यहाँ एक function का उपयोग करके एक उदाहरण दिया गया है जिसमें एक स्ट्रिंग आर्गुमेंट है:
play(string)
जब हम एक मान पास करते हैं:
play("Eze")
calldata नीचे दिया गया टेक्स्ट होगा:
0x
718e6302
0000000000000000000000000000000000000000000000000000000000000020
0000000000000000000000000000000000000000000000000000000000000003
457a650000000000000000000000000000000000000000000000000000000000
Offset को हेक्साडेसिमल में 20 के रूप में दर्शाया गया है, क्योंकि स्ट्रिंग एन्कोडिंग की स्थिति function selector के बाद calldata की शुरुआत से ठीक 32 बाइट्स (डेसिमल में 32 हेक्साडेसिमल में 20 है) दूर है। हम यह भी देख सकते हैं कि स्ट्रिंग (Eze) की लंबाई 3 है क्योंकि स्ट्रिंग में केवल 3 कैरेक्टर हैं, प्रत्येक एक बाइट का है (एक बाइट दो हेक्स कैरेक्टर होता है)।

स्ट्रिंग “Eze” केवल ASCII कैरेक्टर्स का उपयोग कर रही है जो प्रत्येक में एक बाइट लेते हैं (इसलिए लंबाई 3 है)। हालाँकि, “好” जैसे यूनिकोड कैरेक्टर्स 3 बाइट्स लेते हैं। एक utf-8 कैरेक्टर का अधिकतम आकार 4 बाइट्स हो सकता है। स्ट्रिंग “你好” की लंबाई 6 बाइट्स है।
Calldata में structs/tuples को एन्कोड करना
Tuples और structs को समान रूप से एन्कोड किया जाता है क्योंकि structs को ABI प्रकार tuple में मैप किया जाता है।
Solidity ABI एन्कोडिंग विनिर्देश (specification) के अनुसार, एक struct की एन्कोडिंग इसके सदस्यों (members) की एन्कोडिंग का संयोजन (concatenation) है, जिसमें स्टैटिक प्रकारों को 32 बाइट्स तक पैड किया जाता है।
मान लीजिए हमारे पास निम्नलिखित कॉन्ट्रैक्ट है:
contract C {
struct Point {
uint256 x;
uint256 y;
}
function foo(Point memory point) external pure {
//...
}
}
foo का function signature foo((uint256, uint256)) होगा। यह इससे अलग नहीं है कि यदि यह एक tuple को इनपुट के रूप में लेता। यदि यह पॉइंट्स (structs) का डायनेमिक array लेता, तो function signature foo((uint256, uint256)[]) होता।
यदि किसी struct के एलिमेंट सभी निश्चित-आकार (fixed-size) के डेटा हैं, तो हम संपूर्ण struct को स्टैटिक प्रकार के रूप में एन्कोड करेंगे और किसी offset की आवश्यकता नहीं होगी। हालाँकि, struct की एन्कोडिंग बदल जाएगी यदि इसके कम से कम एक फील्ड में डायनेमिक-आकार का डेटा प्रकार है।
उदाहरण के लिए, नीचे दिए गए जैसा एक struct:
RareToken {
uint256 n;
}
send(RareToken,address)
एक स्टैटिक प्रकार के रूप में एन्कोड किया जाएगा यदि हम इसे निम्नलिखित आर्गुमेंट्स पास करते हैं send( RareToken(1), 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1)) जैसा कि अगली इमेज में प्रदर्शित किया गया है:

हालाँकि, यदि struct में कोई डायनेमिक प्रकार है, तो हमें struct को डायनेमिक प्रकार के रूप में एन्कोड करना होगा। आइए इसे एक उदाहरण के रूप में लें:
RareToken {
uint256;
string;
}
send(RareToken)
यदि हम इसे निम्नलिखित डेटा send(RareToken(50,"Eze")) पास करते हैं, तो उपरोक्त function की एन्कोडिंग नीचे दी गई इमेज का कोड होगी। इस आरेख (diagram) में दिखाए गए अनुसार struct में एक डायनेमिक प्रकार और एक स्टैटिक प्रकार शामिल है:

एक से अधिक struct आर्गुमेंट्स की ABI एन्कोडिंग
अब, मान लीजिए कि हमारा function send() एक के बजाय 3 structs को आर्गुमेंट्स के रूप में लेता है। Calldata इस प्रकार होगा:

पहले तीन 32-बाइट शब्द offset हैं क्योंकि function इनपुट के रूप में 3 आर्गुमेंट्स लेता है, और वे प्रत्येक आर्गुमेंट्स डायनेमिक डेटा प्रकार (एक डायनेमिक प्रकार फील्ड वाले structs) हैं।
बाईं ओर 0x00, 0x20, …, 0x1c0 का कॉलम दिखाता है कि offset calldata में स्थान (location) को कैसे इंगित करता है। ध्यान दें कि offset पहले offset से शुरू होता है, न कि वहाँ से जहाँ offset स्थित है। जब हम नेस्टेड (nested) डायनेमिक डेटा को देखेंगे तो हम offsets का अधिक अन्वेषण (explore) करेंगे।
स्टैटिक प्रकार के साथ एक निश्चित आकार (fixed sized) के array को एन्कोड करना
Fixed size arrays को एन्कोड करना array की सामग्री पर निर्भर करता है, यदि fixed size array में डायनेमिक प्रकार शामिल हैं तो fixed size array को एक डायनेमिक प्रकार के रूप में एन्कोड किया जाएगा। यदि इसमें केवल स्टैटिक प्रकार हैं, तो इसे एक स्टैटिक प्रकार माना जाएगा और उसी तरह एन्कोड किया जाएगा। यह उसी तर्क का अनुसरण करता है जैसे एक struct जिसमें डायनेमिक डेटा शामिल था जैसा कि ऊपर दिए गए अनुभाग में दिखाया गया है।
आइए 3 की लंबाई वाले केवल स्टैटिक प्रकारों के एक fixed size array से शुरू करें:
play(uint256[3])
और इसे यह डेटा पास करें:
play([1,2,3])
और यहाँ array की एन्कोडिंग है:

जैसा कि आप देख सकते हैं calldata में कोई offset नहीं हैं। यह केवल array के एलिमेंट्स की एन्कोडिंग है।
डायनेमिक प्रकार रखने वाले निश्चित आकार के array को एन्कोड करना
आइए एक ऐसे परिदृश्य पर विचार करें जहाँ fixed size array में डायनेमिक डेटा होता है। नीचे दिए गए function में दो strings का एक array शामिल होगा।
plays(string[2])
यदि हम इसे निम्नलिखित strings पास करते हैं:
play(["Eze","Sunday"])
एन्कोड होने पर हमें यह calldata मिलेगा:

क्योंकि fixed size array डायनेमिक डेटा प्रकारों का एक array था, इसे पूरी तरह से एक डायनेमिक array के रूप में एन्कोड किया गया था, एकमात्र अंतर यह है कि array की लंबाई को एन्कोड नहीं किया गया था क्योंकि function signature ने इसे 2 की निश्चित लंबाई वाले array के रूप में परिभाषित किया था। यदि हम उसी function पर विचार करें लेकिन डायनेमिक लंबाई के साथ:
plays(string[])
हम देखेंगे कि array की लंबाई को भी एन्कोड किया जाएगा:

Calldata में एक से अधिक array आर्गुमेंट्स और नेस्टेड arrays
Calldata में एक से अधिक और नेस्टेड (nested) arrays के साथ काम करना थोड़ा जटिल और पेचीदा हो सकता है। हालाँकि, सामान्य पैटर्न समान रहता है। इस अनुभाग में, हम सीखेंगे कि नेस्टेड arrays को कैसे एन्कोड और डिकोड किया जाए और offset कैसे काम करता है, इसकी बेहतर समझ (intuition) प्राप्त करेंगे।
हम एक उदाहरण के रूप में निम्नलिखित function signature का उपयोग करेंगे:
transfer(uint256[][],address[])
आइए function के आर्गुमेंट्स के रूप में निम्नलिखित डेटा भी पास करें:
transfer([[123, 456], [789]], [0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0x7b38da6a701c568545dcfcb03fcb875f56bedfb3])
इसलिए, इस function और आर्गुमेंट के लिए calldata नीचे दिया गया हेक्साडेसिमल होगा:
0x7a63729a
0000000000000000000000000000000000000000000000000000000000000040
0000000000000000000000000000000000000000000000000000000000000140
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000000000000000000000000000000000000000000040
00000000000000000000000000000000000000000000000000000000000000a0
0000000000000000000000000000000000000000000000000000000000000002
000000000000000000000000000000000000000000000000000000000000007b
000000000000000000000000000000000000000000000000000000000000007b
0000000000000000000000000000000000000000000000000000000000000001
000000000000000000000000000000000000000000000000000000000000007b
0000000000000000000000000000000000000000000000000000000000000002
0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
0000000000000000000000007b38da6a701c568545dcfcb03fcb875f56bedfb3
हमारे transfer() function की नई विशेषता यह है कि इसमें दो arrays हैं, और उन arrays में से एक में दो सब-arrays हैं।
यहाँ उच्च स्तर पर नेस्टेड और बहु-array (multiple array) के लिए calldata संरचना कैसे बनाई जाती है:
- Offsets. यह arrays के विभिन्न स्थानों के लिए offset को परिभाषित करने से शुरू होता है। यह मानते हुए कि कई array आर्गुमेंट्स हैं, उन arrays के offset की एन्कोडिंग को पहले परिभाषित किया जाएगा। हमारे उदाहरण के लिए, दो offset होंगे।
- पहले डायनेमिक प्रकार का Offset
- दूसरे डायनेमिक प्रकार का Offset
nthडायनेमिक प्रकार का Offset (जहाँ लागू हो)
- पहले array पैरामीटर की लंबाई (हमारे उदाहरण में 2:
[[123, 456], [789]]) इसके बाद आती है और वहाँ रहती है जहाँ पहला offset इंगित करता है। यह संपूर्ण array की लंबाई है। इससे पहले कि आप प्रत्येक array पर काम करना शुरू करें, आप उसकी लंबाई को परिभाषित करने से शुरू करते हैं। इसलिए प्रत्येक सब-array के लिए, आप उनकी लंबाई को परिभाषित करेंगे (बाद में ABI एन्कोडिंग में)। - इसके बाद, पहले array पैरामीटर के सब-arrays को एन्कोड करें
- पहले सब-array (
[123, 456]) का Offset - दूसरे सब-array (
[789]) का Offset- पहले सब-array की लंबाई (हमारे उदाहरण में 2)
- पहले सब-array का पहला आइटम (
123) - पहले सब-array का दूसरा आइटम (
456)
- पहले सब-array का पहला आइटम (
- दूसरे सब-array की लंबाई (हमारे उदाहरण में 1)
- दूसरे सब-array का पहला आइटम (
789)
- दूसरे सब-array का पहला आइटम (
- पहले सब-array की लंबाई (हमारे उदाहरण में 2)
- पहले सब-array (
- एक बार जब आप पहले array पैरामीटर के सभी आइटम्स के साथ काम पूरा कर लेते हैं, तो अगले array पैरामीटर को एन्कोड करना शुरू करें
- दूसरे पैरामीटर array की लंबाई (हमारे उदाहरण में 2 addresses)
- और दूसरे पैरामीटर के एलिमेंट्स (हमारे उदाहरण में addresses), यदि दूसरे array पैरामीटर में सब-arrays हैं, तो आप ऊपर वर्णित समान पैटर्न का पालन करते हैं।
अब, आइए हमारे उदाहरण के calldata की कल्पना करें।
सबसे पहले, इसे पढ़ने में आसान बनाने के लिए, 0x और function selector को छोड़कर, इसे प्रति लाइन 32 बाइट्स (64 कैरेक्टर्स) में व्यवस्थित करें। Function signature और calldata इमेज के शीर्ष पर हैं:

पहले array आर्गुमेंट का Offset
Calldata स्ट्रिंग का पहला 32-बाइट शब्द offset है, जो यह दर्शाता है कि पहले array पैरामीटर का डेटा कहाँ से शुरू होता है। यहाँ एक दृश्य प्रतिनिधित्व (visual representation) है:

दूसरे array आर्गुमेंट का Offset
अगला कदम दूसरे array आर्गुमेंट के लिए offset की एन्कोडिंग है।
जैसा कि हमने कई डायनेमिक structs के उदाहरण के साथ देखा था, यह offset offset के स्थान (location) से “गिनना शुरू” नहीं करता है, बल्कि पहले offset से करता है।
Offsets सामान्य तौर पर अपने वर्तमान स्थान से “गिनना शुरू” नहीं करते हैं, बल्कि पहले offset से करते हैं जो नेस्टेड डेटा संरचना के उस “स्तर (level)” का वर्णन करता है। यह अवधारणा अधिक स्पष्ट हो जाएगी जब हम सब-arrays का अन्वेषण करेंगे।
नीचे दी गई इमेज इस बात पर प्रकाश डालती है कि दूसरा offset दूसरे आर्गुमेंट डेटा (addresses का array) को कैसे इंगित करता है। ध्यान दें कि offset “2” की ओर इशारा कर रहा है क्योंकि दूसरा आर्गुमेंट दो addresses का एक array है।

चूँकि प्रत्येक बाइट शब्द इस प्रकार गिना जाता है:
- 0-31
- 32-63
- 64-95
- आदि
320 बाइट्स (140 हेक्स) उस पंक्ति पर सबसे बाईं ओर का 0 है जहाँ हाइलाइट समाप्त होता है।
पहले array की लंबाई
इसके बाद, हमें पहले array की लंबाई को एन्कोड करने की आवश्यकता है। सब-array आइटम्स में [123, 456] और [789] शामिल हैं, जो [[123, 456], [789]] बनाता है। चूँकि दो नेस्टेड arrays हैं, लंबाई 2 है। लंबाई को calldata में नीचे दिखाए गए अनुसार दर्शाया गया है:

पहले array आर्गुमेंट में सब-arrays के Offsets
Array की लंबाई के बाद, हमारे पास offset हैं जो दिखाते हैं कि उन arrays की सामग्री कहाँ संग्रहीत (stored) है। दो offset हैं, क्योंकि दो सब-arrays हैं: [123, 456] और [789]।
पहले सब-array का Offset
पहले सब-array ([123,456]) का offset दाईं ओर बॉक्स द्वारा इंगित 40 है। वे दोनों array को परिभाषित करने वाली लंबाई के बाद पहले शब्द से “गिनना शुरू” करते हैं। फिर से, ध्यान दें कि यह 2 वाले शब्द की ओर इशारा कर रहा है क्योंकि [123,456] की लंबाई 2 है।

दूसरे सब-array का Offset
दूसरे सब-array ([789]) का offset calldata की शुरुआत से शुरू नहीं होता है। यह a0 (नीचे दी गई इमेज में अंतिम लाल हाइलाइट) पर स्थित है जो पहले सब-array offset घोषित (declared) किए जाने के स्थान से 160 बाइट्स (डेसिमल में) है।
याद रखें कि offsets सामान्य रूप से अपने वर्तमान स्थान से “गिनना शुरू” नहीं करते हैं, बल्कि पहले offset से करते हैं जो नेस्टेड डेटा संरचना के उस “स्तर” का वर्णन करता है। हम अब नेस्टेड array में एक स्तर (level) गहरे हैं, इसलिए हमारा पहला offset नीचे बैंगनी (purple) रंग में हाइलाइट किया गया 40 है। यह offset अपने स्वयं के स्थान से गिनना शुरू नहीं करता है:

पहले सब-array की लंबाई
पहले सब-array की लंबाई इसके बाद आती है। पहले सब-array में 2 आइटम्स होते हैं और इसे नीचे पीले हाइलाइट द्वारा पहचाना जाता है:

पहले सब-array में आइटम्स
अगले दो शब्द पहले सब-array में दो आइटम्स का हेक्स प्रतिनिधित्व (hex representations) हैं जैसा कि नीचे दी गई इमेज में दिखाया गया है:

दूसरे सब-array की लंबाई
फिर हम दूसरे सब-array की लंबाई की ओर बढ़ते हैं जिसमें केवल एक आइटम होता है:

दूसरे सब-array में आइटम
दूसरे array में केवल एक ही आइटम है, जैसा कि आप इसकी लंबाई से देख सकते हैं। हम array में एकल आइटम का मान (315) भी दर्शाते हैं, जो 789 का हेक्स मान है, अगले 32 बाइट्स में, जैसा कि नीचे दिखाया गया है।

दूसरे array की लंबाई
अंत में, हम दूसरे पैरामीटर, address array, की लंबाई दर्शाते हैं। हमारे पास 2 addresses हैं, इसलिए लंबाई 2 है; और दो पैरामीटर इस आरेख (diagram) में दर्शाए गए addresses हैं।

दूसरे array में address आइटम्स
और इसी तरह हम EVM के लिए function transfer([[123, 123], [123]], [0x5b38da6a701c568545dcfcb03fcb875f56beddc4,0x7b38da6a701c568545dcfcb03fcb875f56bedfb3]) को इसके हेक्साडेसिमल प्रतिनिधित्व में बदलते हैं।

निम्नलिखित वीडियो इस अनुभाग के उदाहरण से calldata को सारांशित करता है:
एक ट्रिपल नेस्टेड (triple nested) array एनीमेशन
नीचे हम एक वीडियो दिखा रहे हैं जो प्रदर्शित करता है कि 3D uint array कैसे ABI एन्कोड किया जाता है: f(uint[][][] memory data):
Calldata की लंबाई और गैस लागत (gas cost)
एक Solidity डेवलपर के रूप में, आपकी प्रमुख चिंताओं में से एक गैस बचाना है। और तो और, calldata के साथ काम करने में अतिरिक्त लागत आती है — calldata में प्रत्येक बाइट के लिए गैस की लागत होती है।
कॉल डेटा की लागत निर्धारित करने के लिए हमें सबसे पहले बाइट्स गिनकर calldata की लंबाई का पता लगाना होगा। आइए अपने पिछले calldata स्ट्रिंग को एक केस स्टडी के रूप में उपयोग करें:
0xa9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000
हम पहले 0x को हटा देंगे क्योंकि यह केवल हमारे लिए यह समझने का एक उपसर्ग (prefix) है कि यह एक Ethereum से संबंधित हेक्साडेसिमल है। अब, हमारे पास यह बचेगा:
a9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000
यह स्ट्रिंग 136 हेक्स अंक लंबी है, जो 68 बाइट्स को दर्शाती है। प्रत्येक बाइट को calldata स्ट्रिंग में दो कैरेक्टर्स (हेक्स अंक) द्वारा दर्शाया जाता है। इसलिए, हम 136 को 2 से भाग देकर लंबाई की गणना कर सकते हैं = 68।
calldata में प्रत्येक नॉन-जीरो (non-zero) बाइट की लागत 16 गैस है, जबकि जीरो (zero) बाइट्स की लागत 4 गैस है। इसलिए, हमें अपनी गणना (calculation) के साथ आगे बढ़ने के लिए उन्हें अलग करना होगा।
हमारे पास है:
Calldata के लिए कुल गैस लागत ।
चूँकि जीरो बाइट्स सस्ते होते हैं, कुछ डेवलपर्स ऐसे addresses या स्मार्ट कॉन्ट्रैक्ट addresses को माइन (mine) करते हैं जिनमें शुरुआत में कई जीरो बाइट्स (leading zero bytes) होते हैं क्योंकि यह उस address को एक आर्गुमेंट के रूप में पास करने की गैस लागत को कम कर देता है।
निष्कर्ष
इस पूरे गाइड में, हमने function calls के लिए ABI एन्कोडिंग की मूल बातें, एक ABI-एन्कोडेड function call के प्रमुख घटकों को सीखा है, और calldata की अधिक विस्तृत समझ प्राप्त की है। हमने यह भी खोजा है कि calldata की गैस लागत की गणना कैसे की जाती है और ज्ञान को मजबूत करने में मदद करने के लिए अधिक जटिल calldata डिकोडिंग और एन्कोडिंग अभ्यास का पता लगाने के लिए और भी आगे गए हैं, मुझे उम्मीद है कि आपको यह उपयोगी लगा होगा। इस लेख में आपने जो सीखा है उसे और मजबूत करने के लिए, मैं Ethereum ABI encoding spec के बारे में अधिक पढ़ने और अगले अनुभाग में समस्याओं (problems) का अभ्यास करने की सलाह देता हूँ।
हैप्पी एन्कोडिंग (Happy encoding)!
अभ्यास समस्याएँ (Practice Problems)
foo(uint16 x)पर कॉल के लिए calldata में कितने बाइट्स हैं?(2, [5, 9])पास किए जाने परfoo(uint256 x, uint256[])के लिए ABI एन्कोडिंग क्या है?foo(S[] memory s)के लिए ABI एन्कोडिंग क्या है जहाँSएक struct है जिसके फील्डuint256 x; uint256[] a;हैं? (प्रेरणा के लिए इस tweet को श्रेय)।
Capture the Flag अभ्यास (Exercises)
RareSkills Solidity Riddles: Forwarder
DamnVulnerableDeFi: ABI Smuggling
लेखक (Authorship)
यह लेख RareSkills के सहयोग से Eze Sunday द्वारा लिखा गया था।
मूल रूप से 29 मई को प्रकाशित (Originally Published May 29)