رمزگذاری ABI یک قالب داده است که توسعهدهندگان از آن برای فراخوانی توابع در قراردادهای هوشمند استفاده میکنند. آنها هنگام برقراری ارتباط بین دو قرارداد هوشمند، دادههای مورد نیاز را نیز با همین روش رمزگذاری میکنند.
در این راهنما، نحوه تفسیر دادههای رمزگذاریشده ABI را توضیح میدهیم، روش محاسبه آن را بررسی میکنیم، و ارتباط بین امضای تابع (function signature) و ساختار رمزگذاری ABI را نشان میدهیم.
بیایید وارد جزئیات شویم…
استفاده از abi.encodeWithSignature و فراخوانی در سطح پایین در سالیدیتی
فرض کنید قصد داریم از طریق یک فراخوانی سطح پایین (low-level call) تابع عمومی foo(uint256 x)
را در یک قرارداد دیگر اجرا کنیم و مقدار 5 را به عنوان آرگومان وارد کنیم. در این حالت، از دستور زیر استفاده می کنیم:
1 |
otherContractAddr.call(abi.encodeWithSignature("foo(uint256)", (5)); |
abi.encodeWithSignature("foo(uint256)", (5))
تولید می شود را مشاهده کنیم، می توانیم تابع زیر را بنویسیم:
1 2 3 |
function seeEncoding() external pure returns (bytes memory) { return abi.encodeWithSignature("foo(uint256)", (5)); } |
1 |
0x2fbebd380000000000000000000000000000000000000000000000000000000000000005 |
اجزای اصلی یک فراخوانی تابع رمزگذاری شده به روش ABI
فراخوانی تابعی که با روش ABI رمزگذاری میشود، از دو بخش اصلی تشکیل میگیرد: شناسه تابع (Function Selector) و آرگومان های رمزگذاری شده. اگر تابع دارای ورودی باشد، سیستم این آرگومان ها را رمزگذاری کرده و به شناسه تابع اضافه میکند.
امضای تابع (Function Signature)
امضای تابع از ترکیب نام تابع و نوع داده آرگومان ها (بدون فاصله) بهدست میآید.
برای مثال، امضای تابع زیر را در نظر بگیرید:
1 2 3 4 5 |
function transfer(address _to, uint256 amount) public { // } |
1 |
transfer(address,uint256) |
چند نکته مهم وجود دارد:
-
نوع داده ها باید به صورت کامل نوشته شوند. مثلاً باید از
uint256
استفاده کنید، نهuint
. -
نام متغیرها (مانند
_to
وamount
) در امضای تابع نقشی ندارند. -
نباید هیچ فاصله ای در رشته امضای تابع قرار دهید. برای مثال،
transfer(address, uint256)
ساختار درستی ندارد.
طبق مستندات Solidity، هنگام محاسبه امضای تابع باید به چند مورد خاص توجه کنید:
-
باید ساختارها (Structs) را مانند Tuple در نظر بگیرید.
-
باید آدرس های قابل پرداخت (payable)، اینترفیس ها (interfaces) و انواع قرارداد (contract types) را به صورت
address
در نظر بگیرید. -
باید اصلاح کننده های حافظه مانند
memory
وcalldata
را نادیده بگیرید. -
باید نوع enum را معادل
uint8
در نظر بگیرید. -
باید نوع تعریف شده توسط کاربر (user-defined type) را معادل نوع پایه ای آن پردازش کنید.
شناسه تابع (Function Selector)
function selector در سالیدیتی، از اولین ۴ بایت هش Keccak-256 امضای تابع به دست می آید. سالیدیتی از این شناسه برای شناسایی و تفکیک توابع استفاده می کند.
برای مثال، هش Keccak-256 مربوط به امضای تابع transfer(address,uint256)
که در بخش قبل بررسی کردیم، به صورت زیر است:
1 |
0xa9059cbb2ab09eb219583f4a59a5d0623ade346d962bcd4e46b11da047c9049b |
می توانید با استفاده از کتابخانه ethers
(نسخه ۶)، امضای تابع transfer(address,uint256)
را به شناسه تابع تبدیل کنید. کد زیر این کار را انجام می دهد:
1 2 3 4 |
const ethers = require('ethers'); // Ethers v6 const functionSignature = 'transfer(address,uint256)'; const functionSelector = ethers.id(functionSignature).substring(0, 10) console.log(functionSelector); |
خروجی این کد به صورت زیر خواهد بود:
در زبان سالیدیتی، می توانید با نوشتن تابعی ساده، شناسه تابع را محاسبه کنید:
1 2 3 |
function getSelector() public pure returns (bytes4 ret) { return bytes4(keccak256("transfer(address,uint256)")); // 0xa9059cbb } |
اگر نمی خواهید کد بنویسید، می توانید از وبسایت های آنلاین برای محاسبه Keccak-256 استفاده کنید و امضای تابع را به شناسه آن تبدیل کنید.
حالا که مفهوم شناسه تابع را به طور کامل بررسی کردیم، در ادامه به سراغ بخش بعدی رمزگذاری ABI در فراخوانی توابع می رویم: آرگومان های تابع (function inputs یا arguments).
ورودی ها یا آرگومان های تابع
وقتی تابعی هیچ آرگومانی نداشته باشد، فراخوانی آن فقط به شناسه تابع (function selector) وابسته است. همین شناسه بهتنهایی تمام داده مورد نیاز برای اجرای تابع را فراهم میکند. برای مثال، تابع play()
با این شناسه شناسایی می شود: 0x93e84cd9
در این حالت، همین شناسه به تنهایی برای فراخوانی تابع کافی است.
اما وقتی تابعی دارای آرگومان باشد — مانند transfer(address to, uint256 amount)
— در آن صورت باید آرگومان ها را طبق استاندارد ABI رمزگذاری کنیم و آن ها را به شناسه تابع بچسبانیم.
بیایید برای درک بهتر روند رمزگذاری آرگومان ها، از تابع transfer(address to, uint256 amount)
به عنوان مثال اصلی استفاده کنیم:
1 2 3 |
function transfer(address to, uint256 amount) public { // } |
calldata
قرار میدهد. calldata
بخشی از ورودی تراکنش است که فقط قابل خواندن است و امکان تغییر آن وجود ندارد. قرارداد یا تابع نیز این داده ها را ذخیره نمیکند، بلکه فقط آنها را از طریق calldata
دریافت و پردازش میکند.
در سایت Etherscan، می توانید calldata
مربوط به یک تراکنش را مشاهده کنید. تصویر زیر نمونه ای از calldata
مربوط به یک تراکنش انتقال توکن ERC-20 را نمایش می دهد:
در Etherscan، شناسه تابع را با عنوان MethodID نمایش می دهند. همان طور که در کادر قرمز زیر توضیح تابع دیده می شود، شناسه تابع transfer()
برابر است با: 0xa9059cbb.
بعد از آن، دو مقدار هگزادسیمال طولانی مشاهده می شوند که با [0]
و [1]
مشخص شد اند. این دو مقدار، به ترتیب نمایانگر آرگومان اول یعنی آدرس to
و آرگومان دوم یعنی مقدار amount
هستند.
Etherscan برای افزایش خوانایی، داده های calldata
را به بخشهای ۳۲ بایتی (یعنی هر خط شامل ۶۴ کاراکتر هگزادسیمال) تقسیم میکند. این نوع نمایش به توسعه دهنده ها کمک میکند تا بتوانند ورودی های تابع را راحتتر تفسیر و بررسی کنند.
با این حال، در واقعیت، تمام این داده ها بهصورت یک رشته طولانی و پیوسته (بدون فاصله یا خط جدید) در قالب یک تراکنش ارسال میشوند. برای نمونه، calldata
کامل مربوط به تابع transfer()
ممکن است به شکل زیر ارسال شود:
1 |
0xa9059cbb000000000000000000000000f89d7b9c864f589bbf53a82105107622b35eaa4000000000000000000000000000000000000000000000028a857425466f800000 |
calldata
را دقیقاً به همان شکلی ببینید که در تراکنش ارسال شده، کافی است روی گزینه “View input as” کلیک کنید و حالت “Original” را انتخاب کنید. این گزینه داده ها را بدون هیچگونه پردازشی، بهصورت رشتهای خام نمایش میدهد.
در ادامه، برای درک بهتر فرآیند رمزگذاری، ساختار calldata
را خط به خط بررسی می کنیم و اطلاعات کلیدی آن را مرحله به مرحله استخراج خواهیم کرد.
بررسی calldata
در این بخش، قصد داریم calldata
زیر را بررسی کنیم و اجزای آن را شناسایی کنیم:
1 |
0xa9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000 |
برای شروع، ابتدا باید امضای تابعی را که این calldata
به آن مربوط میشود، مشخص کنیم. بدون داشتن امضای دقیق تابع، امکان رمزگشایی داده وجود ندارد.
در این مثال، امضای تابع بهصورت زیر تعریف شده است:
1 |
transfer(address,uint256) |
اکنون، calldata
را به بخش های مشخص تقسیم می کنیم تا ساختار آن را بهتر درک کنیم:
-
ابتدا پیشوند
0x
را که نشان دهنده مقدار هگزادسیمال است، جدا می کنیم. -
سپس، شناسه تابع (function selector) را که همیشه ۴ بایت (۸ کاراکتر هگزادسیمال) است در یک خط می آوریم.
-
در نهایت، هر آرگومان را در بلوک های ۳۲ بایتی (۶۴ کاراکتر) جداگانه نمایش می دهیم.
ساختار تفکیک شده به صورت زیر است:
1 2 3 4 |
0x <---------- پیشوند هگزادسیمال a9059cbb <---------- شناسه تابع 0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f44 00000000000000000000000000000000000000000000011c9a62d04ed0c80000 |
شناسه تابع
در ابتدای calldata
، اولین ۴ بایت داده (۸ کاراکتر هگزادسیمال) شناسه تابع را تشکیل می دهد: 0xa9059cbb.
آدرس (Address)
پس از شناسه تابع، ۳۲ بایت بعدی مربوط به آرگومان اول یعنی آدرس دریافت کننده (address
) است. با وجود اینکه آدرس در سالیدیتی فقط ۲۰ بایت است، برای سازگاری با استاندارد ABI آن را با صفرهای ابتدایی پَدگذاری (padding) می کنند تا به ۳۲ بایت برسد.
مقدار ۳۲ بایتی مربوط به آدرس در calldata
:
1 |
0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f44 |

برای بهدست آوردن آدرس واقعی، باید صفرهای اضافی را حذف کنیم و تنها ۲۰ بایت پایانی (۴۰ کاراکتر هگزادسیمال آخر) را نگه داریم. در نتیجه، آدرس گیرنده به صورت زیر خواهد بود:
1 |
0x3F5047BDb647Dc39C88625E17BDBffee905A9F44 |
مقدار (Amount)
و در نهایت، آخرین آرگومان در تابع transfer(address,uint256)
مربوط به مقدار (amount) است. مقدار مورد نظر در calldata
به صورت زیر نمایش داده شده است:
1 |
00000000000000000000000000000000000000000000011c9a62d04ed0c80000 |

برای اینکه این مقدار با فرمت ABI سازگار باشد، سالیدیتی آن را با صفرهای ابتدایی (leading zeros) پدگذاری کرده تا دقیقاً ۳۲ بایت (۶۴ کاراکتر هگزادسیمال) را تشکیل دهد.
برای تبدیل این عدد هگزادسیمال به عدد دسیمال، می توان از زبان پایتون استفاده کرد:
1 2 |
>>> int("0x11c9a62d04ed0c80000", 16) 5250000000000000000000 |
همچنین، برای تبدیل عدد دسیمال به مقدار هگزادسیمال معادل:
1 2 |
>>> hex(5250000000000000000000) 0x11c9a62d04ed0c80000 |
نوع داده ها و پدگذاری (Padding)
در مراحل قبل دیدیم که در رمزگذاری calldata
، هر مقدار ورودی به صورت یک کلمه ۳۲ بایتی (۳۲-byte word) رمزگذاری می شود. اگر اندازه واقعی داده از ۳۲ بایت کمتر باشد، سیستم آن را با صفر پدگذاری می کند تا به طول ۳۲ بایت برسد.
بر اساس قانون کلی برای نوع داده های با اندازه ثابت (Fixed-size types) مانند int
، bool
، و تمام انواع uint
(از uint8
تا uint256
)، رمزگذاری به صورت ۳۲ بایت انجام می شود و در صورت نیاز، با صفرهای سمت چپ (left-padding) پر می شوند.
مثال:
اگر یک متغیر uint8
با مقدار ۵ داشته باشیم، رمزگذاری آن به شکل زیر خواهد بود:
1 |
0x0000000000000000000000000000000000000000000000000000000000000005 |
bool
برابر true
داشته باشیم، به صورت زیر رمزگذاری می شود:
1 |
0x0000000000000000000000000000000000000000000000000000000000000001 |
bool
که برابر با true
باشد، نیز با صفرهای سمت چپ پُر می شود و به صورت زیر رمزگذاری خواهد شد:
1 |
0x0000000000000000000000000000000000000000000000000000000000000001 |
اما نوع داده هایی که اندازه متغیر دارند مانند bytes
و string
، با صفرهای سمت راست پُر می شوند. برای مثال، مقدار 0x68656c6c6f
که متن "hello"
را نشان می دهد، در قالب یک کلمه ۳۲ بایتی به صورت زیر رمزگذاری می شود:
1 |
0x68656c6c6f000000000000000000000000000000000000000000000000000000 |
نوع داده های با اندازه ثابت (Fixed-size) در سالیدیتی عبارتند از:
-
bool
-
انواع
uint
(مثلuint8
تاuint256
) -
bytes
با اندازه ثابت (مثلاًbytes1
تاbytes32
) -
address
-
tuple
یاstruct
هایی که فقط شامل داده های با اندازه ثابت باشند -
آرایه هایی با اندازه مشخص که فقط شامل نوع داده ثابت باشند
نوع داده های با اندازه متغیر (Dynamic-size) در سالیدیتی عبارتند از:
-
bytes
(بدون عدد مشخص پس از آن) -
string
-
آرایه های پویا (dynamic array)
-
آرایه های با اندازه مشخص که داخل خود نوع داده متغیر داشته باشند
-
struct
هایی که شامل هر یک از نوع داده های متغیر بالا باشند
کار با calldata
پویا (Dynamic Calldata)
تا اینجا، ما روی نوع دادههای با اندازه ثابت مانند address
و uint256
تمرکز کردهایم. رمزگذاری این نوع دادهها فرآیند سادهای دارد. اما زمانی که با آرایه ها یا رشته ها (strings) کار میکنیم، به دلیل اندازه متغیر دادهها، فرآیند رمزگذاری پیچیدهتر میشود.
فرض کنیم تابعی داریم که یک آرایه از uint
ها و یک address
به عنوان ورودی دریافت میکند. در اینجا جزئیات پیادهسازی تابع برای ما اهمیتی ندارد؛ فقط امضای تابع مهم است:
1 |
transfer(uint256[], address) |
بیایید فرض کنیم داده زیر را به تابع ارسال میکنیم:
1 |
transfer([5769, 14894, 7854], 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1) |
هدف ما این است که calldata
مربوط به این فراخوانی را بررسی کنیم و ببینیم چگونه هر بخش از آن رمزگذاری میشود.
در قدم اول باید مقدار offset مربوط به آرگومان آرایه uint256[]
را رمزگذاری کنیم. اما offset دقیقاً چیست؟
Offset چیست؟
ما در رمزگذاری ABI از offset استفاده میکنیم تا دقیقاً مشخص کنیم دادههای پویا (مثل آرایه یا رشته) از کجای calldata شروع میشوند. این مقدار به تابع کمک میکند تا محتوای واقعی داده را از مکان درست بازیابی کند.
در مثال ما، یک نوع داده پویا uint256[]
و یک نوع داده ثابت address
وجود دارد. مقدار offset مربوط به uint256[]
در calldata بالا برابر است با 0x40 در مبنای هگزادسیمال (یا 64 در مبنای دسیمال) و این مقدار در قالب یک کلمه ۳۲ بایتی رمزگذاری میشود.
از آنجایی که آرایه پویا اولین آرگومان تابع محسوب میشود، offset آن در اولین کلمه ۳۲ بایتی از calldata قرار میگیرد:
برای توضیح بیشتر درباره نحوه عملکرد offset، تصویر بالا دقیقاً محل قرارگیری offset آرایه در داخل calldata را مشخص کرده است. هر کلمه ۳۲ بایتی به صورت زیر شمارهگذاری میشود:
- 0 تا 31 (ردیف اول شامل ۳۲ بایت)
- 32 تا 63 (ردیف دوم شامل ۳۲ بایت)
- 64 تا 95 (ردیف سوم شامل ۳۲ بایت)
- و به همین ترتیب ادامه دارد.
بنابراین مقدار 64 (یا 0x40 در مبنای هگزادسیمال) دقیقاً برابر با اولین بایت (جفت کاراکتر هگزادسیمال سمت چپ) در ردیف سوم است؛ همانجایی که هایلایت سبز به پایان میرسد. این موقعیت همان نقطهای است که offset به آن اشاره میکند.
در این مثال، offset فاصلهای را نشان میدهد که از ابتدای اولین بایت بعد از شناسه تابع شروع میشود تا جایی که داده پویا (یعنی آرایه) آغاز میشود. البته در ادامه خواهیم دید که offset همیشه به معنی “فاصله از اولین بایت بعد از شناسه تابع” نیست.
رمزگذاری داده ثابت — آدرس
در خط بعدی، سیستم آدرس را رمزگذاری میکند. این مقدار را به صورت یک کلمه ۳۲ بایتی نمایش میدهد و با صفرهای ابتدایی پُر میشود. این همان آدرسی است که پیشتر به تابع دادهایم، زیرا آدرس از قبل در قالب هگزادسیمال قرار دارد.
رمزگذاری طول یک داده پویا — آرایه
در خط بعدی، سیستم طول آرایه را رمزگذاری میکند؛ یعنی تعداد آیتمهایی که درون آرایه قرار دارند. همانطور که مشاهده میکنید، این آرایه شامل سه مقدار است: [5769, 14894, 7854]. بنابراین، سیستم عدد ۳ را بهعنوان طول آرایه در نظر میگیرد و در تصویر زیر همین مقدار را نمایش میدهد:
رمزگذاری عناصر آرایه به صورت هگزادسیمال
در مراحل قبل، ما نوع داده ثابت، مقدار offset و طول آرایه را رمزگذاری کردیم. حالا باید عناصر آرایه را رمزگذاری کنیم. هر عنصر آرایه را به عددی هگزادسیمال تبدیل میکنیم، مانند آنچه در تصویر بعدی مشاهده میشود:
ما هر یک از اعداد صحیح موجود در آرایه را به معادل هگزادسیمال آن تبدیل میکنیم و سپس با صفرهای ابتدایی آنها را تا ۳۲ بایت کامل میکنیم. در نتیجه، آیتمهای آرایه به صورت زیر رمزگذاری میشوند:
با این بخش، بررسی ما درباره calldata
مربوط به رمزگذاری ABI برای تابع transfer(uint256[], address)
کامل میشود.
رمزگذاری آرگومان از نوع رشته (string) در ABI
رمزگذاری یک مقدار از نوع string
فرآیند سادهای دارد و فقط شامل موارد زیر است:
-
تعیین offset
-
مشخص کردن طول رشته
-
رمزگذاری محتوای رشته با فرمت UTF-8
در ادامه یک مثال با تابعی میبینید که دارای آرگومان string
است:
1 |
play(string) |
1 |
play("Eze") |
calldata
مربوط به این فراخوانی به صورت زیر خواهد بود:
1 2 3 4 5 |
0x 718e6302 0000000000000000000000000000000000000000000000000000000000000020 0000000000000000000000000000000000000000000000000000000000000003 457a650000000000000000000000000000000000000000000000000000000000 |
مقدار offset به صورت 0x20
(مبنای هگزادسیمال) نمایش داده شده است، زیرا محل قرارگیری رمزگذاری رشته، دقیقاً ۳۲ بایت بعد از ابتدای calldata
(پس از شناسه تابع) قرار دارد. عدد ۳۲ در مبنای دهدهی برابر با ۲۰ در مبنای هگزادسیمال است.
همچنین مشاهده میکنیم که طول رشته "Eze"
برابر با ۳ است، چون این رشته شامل ۳ کاراکتر میباشد و هر کاراکتر معادل ۱ بایت است (هر بایت معادل ۲ کاراکتر هگزادسیمال است).
رشته "Eze"
فقط شامل نویسههای ASCII است که هر کدام دقیقاً ۱ بایت فضا اشغال میکنند، به همین دلیل طول رشته ۳ بایت محاسبه میشود. اما در مقابل، نویسههایی که در قالب Unicode هستند — مانند "好"
— معمولاً چند بایتی هستند. این کاراکتر خاص در UTF-8 به صورت ۳ بایت رمزگذاری میشود. در واقع، هر نویسه UTF-8 میتواند حداکثر تا ۴ بایت فضا بگیرد. برای مثال، رشته "你好"
که شامل دو نویسه چینی است، در UTF-8 به صورت ۶ بایت (۳ بایت برای هر نویسه) رمزگذاری میشود.
رمزگذاری struct
و tuple
در calldata
در رمزگذاری ABI، tuple
ها و struct
ها به یک شکل رمزگذاری میشوند، زیرا کامپایلر سالیدیتی نوع داده struct
را به صورت tuple
در ABI تفسیر میکند.
طبق مشخصات رسمی رمزگذاری ABI در سالیدیتی، رمزگذاری یک struct
از بههمپیوستن رمزگذاری تکتک اعضای آن تشکیل میشود؛ در این فرآیند، نوع دادههایی که اندازه ثابتی دارند، با صفرهای ابتدایی طوری پُر میشوند که به طول ۳۲ بایت برسند.
فرض کنیم قرارداد زیر را داریم:
1 2 3 4 5 6 7 8 9 10 11 |
contract C { struct Point { uint256 x; uint256 y; } function foo(Point memory point) external pure { //... } } |
foo
به صورت زیر خواهد بود:
1 |
foo((uint256,uint256)) |
این ساختار هیچ تفاوتی با حالتی ندارد که تابع به جای struct
، مستقیماً یک tuple
به عنوان ورودی دریافت کند.
اگر تابع، آرایهای پویا از ساختارهای مشابه را دریافت کند، امضای آن به صورت زیر خواهد بود:
1 |
foo((uint256,uint256)[]) |
اگر تمام فیلدهای یک struct
از نوع دادههای با اندازه ثابت باشند، ما کل ساختار را به عنوان یک نوع دادهی ثابت رمزگذاری میکنیم و در این صورت نیازی به offset نخواهد بود.
اما اگر حتی یکی از فیلدهای struct
از نوع داده متغیر (مثل string
یا bytes
) باشد، ساختار آن به عنوان دادهی پویا در نظر گرفته میشود و بنابراین نحوهی رمزگذاری آن تغییر خواهد کرد.
برای مثال، ساختاری مانند نمونه زیر را در نظر بگیرید:
1 2 3 4 5 |
RareToken { uint256 n; } send(RareToken,address) |
1 |
send(RareToken(1), 0x1b7e1b7ea98232c77f9efc75c4a7c7ea2c4d79f1) |
ساختار RareToken(1)
به عنوان نوع داده ثابت (static type) رمزگذاری خواهد شد؛ زیرا تمام اعضای این ساختار اندازه ثابتی دارند.
در تصویر زیر، میتوانید نحوه دقیق رمزگذاری این داده ها را مشاهده کنید:
اما اگر یکی از فیلدهای struct
از نوع داده پویا باشد، باید کل ساختار را به عنوان نوع داده پویا (dynamic type) رمزگذاری کنیم. بیایید ساختار زیر را به عنوان مثال در نظر بگیریم:
1 2 3 4 5 6 |
RareToken { uint256; string; } send(RareToken) |
1 |
send(RareToken(50, "Eze")) |
رمزگذاری این فراخوانی با کدی مطابقت دارد که در تصویر زیر نمایش داده شده است. در این ساختار، RareToken دو فیلد دارد: یکی از نوع داده ثابت (uint256 با مقدار 50) و دیگری از نوع داده پویا (string با مقدار “Eze”). این ساختار ترکیبی از یک نوع پویا و یک نوع ثابت را در بر میگیرد، بنابراین آن را بهعنوان یک نوع داده پویا رمزگذاری میکنیم.
رمزگذاری ABI برای چندین آرگومان از نوع struct
حال فرض کنید تابع send()
به جای یک ساختار، سه struct
به عنوان ورودی دریافت کند. در این صورت، calldata
تابع مطابق با ساختار زیر رمزگذاری خواهد شد:
سه کلمه ۳۲ بایتی اول در calldata
همگی offset هستند، چون تابع مورد نظر ما سه آرگومان دریافت میکند و هر کدام از این آرگومان ها، یک نوع داده پویا محسوب میشوند (یعنی struct
هایی که حداقل یک فیلد با نوع داده پویا دارند).
ستونی که در سمت چپ شامل مقادیری مانند 0x00
، 0x20
، …، 0x1c0
است، نشان میدهد که هر offset دقیقاً به کدام بخش از calldata
اشاره میکند. دقت کنید که مقدار offset از ابتدای اولین offset شروع میشود، نه از موقعیتی که خود offset در آن قرار دارد. در ادامه و هنگام بررسی داده های پویا تو در تو (nested dynamic data)، بهصورت دقیقتری نحوه کارکرد offset را تحلیل میکنیم.
نحوه رمزگذاری آرایههای با اندازه و نوع داده ثابت
رمزگذاری آرایه های با اندازه ثابت به محتوای آنها بستگی دارد. اگر آرایه تنها از نوع دادههای ثابت تشکیل شود، آن را بهعنوان یک نوع داده ثابت در نظر میگیریم و بر همین اساس رمزگذاری را انجام میدهیم. اما اگر آرایه شامل نوع داده پویا باشد—در حالتی که طول ثابتی داشته باشد—با آن مثل یک آرایه پویا برخورد میکنیم و رمزگذاری را مطابق قوانین مربوط به داده های پویا انجام میدهیم. این رویکرد همان منطق رمزگذاری struct هایی را دنبال میکند که دارای نوع داده پویا هستند؛ موضوعی که در بخش قبلی به آن پرداختیم.
بیایید با یک آرایه با اندازه ثابت که فقط نوع دادههای ثابت دارد شروع کنیم. طول این آرایه برابر با ۳ است:
1 |
play(uint256[3]) |
1 |
play([1,2,3]) |
همانطور که میبینید، در calldata
هیچ offset وجود ندارد. فقط مقادیر عناصر آرایه به ترتیب و در قالب کلمههای ۳۲ بایتی رمزگذاری شدهاند.
نحوه رمزگذاری آرایه fixed size دارای نوع داده پویا
بیایید حالتی را بررسی کنیم که در آن، آرایه fixed size فقط داده هایی از نوع پویا در خود داشته باشد. در مثال زیر، تابعی داریم که یک آرایه از دو رشته (string[2]
) را به عنوان ورودی دریافت میکند:
1 |
plays(string[2]) |
1 |
play(["Eze","Sunday"]) |
در زمان رمزگذاری، calldata
به صورت زیر تولید خواهد شد:
چون این آرایه fixed size شامل نوع داده پویا است، رمزگذاری را به گونهای انجام میدهیم که کل آرایه مانند یک آرایه پویا عمل کند. تنها تفاوت اینجاست که طول آرایه را رمزگذاری نمیکنیم، چون امضای تابع از ابتدا تعداد اعضای آرایه (۲ عضو) را مشخص کرده است.
اگر همین تابع را به گونهای تعریف کنیم که آرایه دارای طول پویا باشد:
1 |
plays(string[]) |
calldata
رمزگذاری شده است.
آرگومانهای چند آرایه ای و آرایه های تو در تو در calldata
کار با آرایه های متعدد و آرایه های تو در تو در calldata
ممکن است کمی پیچیده و فریبدهنده به نظر برسد. با این حال، الگوی کلی رمزگذاری همچنان ثابت باقی میماند. در این بخش یاد میگیریم که چگونه آرایه های تو در تو را رمزگذاری و رمزگشایی کنیم و درک دقیقتری از نحوه عملکرد offset پیدا کنیم.
از امضای تابع زیر به عنوان مثال استفاده میکنیم:
1 |
transfer(uint256[][],address[]) |
1 |
transfer([[123, 456], [789]], [0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, 0x7b38da6a701c568545dcfcb03fcb875f56bedfb3]) |
calldata
مربوط به این تابع و آرگومان ها به صورت هگزادسیمال خواهد بود:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
0x7a63729a 0000000000000000000000000000000000000000000000000000000000000040 0000000000000000000000000000000000000000000000000000000000000140 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000000000000000000000000000000000000000000040 00000000000000000000000000000000000000000000000000000000000000a0 0000000000000000000000000000000000000000000000000000000000000002 000000000000000000000000000000000000000000000000000000000000007b 000000000000000000000000000000000000000000000000000000000000007b 0000000000000000000000000000000000000000000000000000000000000001 000000000000000000000000000000000000000000000000000000000000007b 0000000000000000000000000000000000000000000000000000000000000002 0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4 0000000000000000000000007b38da6a701c568545dcfcb03fcb875f56bedfb3 |
ویژگی جدیدی که در تابع transfer()
داریم این است که دو آرایه به عنوان ورودی دریافت میکند و یکی از این آرایه ها، خودش شامل دو زیرآرایه است.
در ادامه، ساختار کلی رمزگذاری calldata
برای آرایه های تو در تو و چند آرگومانی را مرور میکنیم:
- Offsets: رمزگذاری با تعریف offsetها آغاز میشود؛ یعنی مشخص میکنیم هر آرایه در چه موقعیتی از
calldata
قرار دارد. اگر چند آرایه به عنوان ورودی داشته باشیم، ابتدا offset تمام آنها رمزگذاری میشود.
در مثال ما، دو offset خواهیم داشت:-
offset مربوط به آرایه اول (
uint256[][]
) -
offset مربوط به آرایه دوم (
address[]
) -
و در صورت وجود آرایه های بیشتر، offsetهای بعدی به همین ترتیب ادامه خواهند داشت
-
- طول آرایه اول (که در مثال ما برابر ۲ است:
[[123, 456], [789]]
) در مرحله بعدی قرار میگیرد و دقیقاً در موقعیتی نوشته میشود که offset اول به آن اشاره کرده است. این مقدار، طول کل آرایه را مشخص میکند. پیش از آنکه رمزگذاری هر زیرآرایه را شروع کنید، ابتدا باید طول آن آرایه را مشخص کنید. بنابراین، در رمزگذاری ABI، برای هر زیرآرایه نیز ابتدا طول آن تعریف و رمزگذاری میشود. این کار باعث میشود هنگام خواندن دادهها، مشخص باشد که هر بخش ازcalldata
چه تعداد مقدار در بر دارد. - در مرحله بعد، نوبت به رمزگذاری زیرآرایه های آرگومان اول میرسد:
-
offset مربوط به زیرآرایه اول:
[123, 456]
- offset مربوط به زیرآرایه دوم:
[789]
- طول زیرآرایه اول (در مثال ما برابر ۲ است)
-
مقدار اول زیرآرایه اول:
123
-
مقدار دوم زیرآرایه اول:
456
-
-
طول زیرآرایه دوم (در مثال ما برابر ۱ است)
-
مقدار اول زیرآرایه دوم:
789
-
- طول زیرآرایه اول (در مثال ما برابر ۲ است)
-
- پس از اینکه تمام مقادیر مربوط به آرایه اول را رمزگذاری کردید، باید رمزگذاری آرگومان بعدی یعنی آرایه دوم را آغاز کنید.
-
ابتدا طول آرایه دوم را مشخص کنید (در مثال ما شامل ۲ آدرس است)
-
سپس، مقادیر آرایه دوم را رمزگذاری میکنیم (در اینجا، آدرسها). اگر آرایه دوم شامل زیرآرایه باشد، همان الگوی قبلی را تکرار میکنیم: ابتدا offset هر زیرآرایه را مشخص میکنیم، سپس طول آن را مینویسیم و در نهایت مقادیر آن را رمزگذاری میکنیم.
-
اکنون، برای درک بهتر ساختار calldata
، بیایید آن را بهصورت تصویری بررسی کنیم.
ابتدا، برای خوانایی بهتر، رشته calldata
را به خطوطی تقسیم میکنیم که هر کدام ۳۲ بایت (معادل ۶۴ کاراکتر هگزادسیمال) داشته باشند. پیشوند 0x
و شناسه تابع (function selector) را جدا در ابتدای داده قرار میدهیم. امضای تابع و رشته کامل calldata
در بالای تصویر نمایش داده شده است:
offset آرگومان اول از نوع آرایه
اولین کلمه ۳۲ بایتی در رشته calldata
نشاندهنده offset است؛ یعنی موقعیتی که داده های مربوط به آرگومان اول (که یک آرایه تو در تو است) از آنجا شروع میشوند. در تصویر زیر، میتوانید محل دقیق offset مربوط به آرگومان uint256[][]
را درون calldata
مشاهده کنید:
offset آرگومان دوم از نوع آرایه
در گام بعد، باید offset مربوط به آرگومان دوم یعنی آرایه آدرس ها را رمزگذاری کنیم.
همانطور که در مثال structهای پویا توضیح دادیم، هنگام محاسبه offset، شمارش را از محل فعلی offset شروع نمیکنیم. در عوض، شمارش را از اولین offset آغاز میکنیم که آن سطح از ساختار داده (مثل آرایه بیرونی یا struct اصلی) را تعریف کرده است.
بهطور کلی، در ساختارهای تو در تو، هر offset مسیر خود را نسبت به اولین موقعیت سطح مربوط به خودش مشخص میکند. وقتی به رمزگذاری زیرآرایهها برسیم، این منطق کاملاً روشن خواهد شد.
در تصویر زیر، میبینید که offset مربوط به آرگومان دوم به دادههای آرایه آدرسها اشاره دارد. چون این آرگومان شامل دو آدرس است، offset به عدد 2
ارجاع میدهد.
همانطور که میدانیم، موقعیت کلمههای ۳۲ بایتی به شکل زیر شمارهگذاری میشود:
-
بایت 0 تا 31 → ردیف اول
-
بایت 32 تا 63 → ردیف دوم
-
بایت 64 تا 95 → ردیف سوم
-
…
در این مثال، مقدار offset برابر 320 بایت (یا 0x140
هگزادسیمال) است. این مقدار، به اولین بایت سمت چپ در ردیفی اشاره میکند که هایلایت پایان مییابد.
طول آرایه اول
در مرحله بعد، باید طول آرایه اول را رمزگذاری کنیم. این آرایه شامل دو زیرآرایه است: [123, 456]
و [789]
که با هم ساختار [[123, 456], [789]]
را تشکیل میدهند. چون این آرایه ۲ زیرآرایه دارد، طول آن برابر با ۲ خواهد بود. این مقدار را به صورت یک عدد ۳۲ بایتی در calldata
قرار میدهیم و آن را دقیقاً در موقعیتی رمزگذاری میکنیم که offset اول به آن اشاره میکند. تصویر زیر محل قرارگیری طول آرگومان اول را در داخل calldata
نشان میدهد.
offsetهای زیرآرایه ها در آرگومان اول از نوع آرایه
بعد از طول آرایه، offsetهایی قرار دارند که نشان میدهند محتوای این آرایه ها در کجای calldata ذخیره شدهاند. دو offset وجود دارد، چون دو زیرآرایه داریم: [123, 456]
و [789]
.
offset زیرآرایه اول
offset مربوط به زیرآرایه اول ([123, 456]
) مقدار 0x40
است و کادر سمت راست آن را نشان میدهد. در اینجا، شمارش را از اولین کلمه پس از مقدار طول آرایه (جایی که آرایه تعریف میشود) آغاز میکنیم. همچنین باید دقت کنید که این offset به کلمهای اشاره میکند که مقدار 2
دارد، چون زیرآرایه [123, 456]
شامل دو مقدار است.
offset زیرآرایه دوم
offset مربوط به زیرآرایه دوم ([789]
) از ابتدای calldata
شروع نمیشود. این offset در موقعیت 0xa0
قرار دارد (که در تصویر زیر با هایلایت قرمز مشخص شده) و فاصله آن از محل تعریف offset زیرآرایه اول، دقیقاً ۱۶۰ بایت (در مبنای دسیمال) است.
یادآوری میکنیم که offsetها بهطور کلی شمارش را از محل فعلی خود شروع نمیکنند، بلکه از اولین offset مربوط به همان سطح از ساختار تو در تو آغاز میکنند. در این مثال، ما اکنون در یک سطح داخلی از آرایه تو در تو هستیم؛ بنابراین اولین offset در این سطح مقدار 0x40
است (که در تصویر با هایلایت بنفش مشخص شده). offset دوم نیز شمارش را از همین نقطه (یعنی 0x40
) آغاز میکند، نه از مکان فعلی خودش:
طول زیرآرایه اول
در مرحله بعد، طول زیرآرایه اول قرار میگیرد. این زیرآرایه شامل ۲ مقدار است، بنابراین طول آن برابر با 2
خواهد بود. این مقدار به صورت یک کلمه ۳۲ بایتی در calldata
رمزگذاری میشود و در تصویر زیر با هایلایت زرد مشخص شده است:
مقادیر زیرآرایه اول
دو کلمه بعدی در calldata
، نمایش هگزادسیمال دو مقدار موجود در زیرآرایه اول هستند. این مقادیر همان 123
و 456
هستند که بهصورت جداگانه و با فرمت ۳۲ بایتی رمزگذاری شدهاند، همانطور که در تصویر زیر مشاهده میشود:
طول زیرآرایه دوم
سپس به رمزگذاری طول زیرآرایه دوم میرسیم که فقط شامل یک مقدار است. بنابراین، مقدار طول آن برابر با 1
خواهد بود:
مقدار زیرآرایه دوم
زیرآرایه دوم تنها یک مقدار دارد، همانطور که از طول آن مشخص است. در ادامه، مقدار همین عنصر (یعنی 789
) در قالب هگزادسیمال (0x315
) در یک کلمه ۳۲ بایتی رمزگذاری میشود، همانطور که در تصویر زیر مشاهده میکنید:
طول آرایه دوم
در نهایت، به رمزگذاری طول آرگومان دوم یعنی آرایه آدرسها میرسیم. این آرایه شامل ۲ آدرس است، بنابراین طول آن برابر با 2
خواهد بود. این مقدار به صورت یک کلمه ۳۲ بایتی در calldata
رمزگذاری میشود و در تصویر زیر با هایلایت مشخص شده است:
مقادیر آدرس در آرایه دوم
در این مرحله، آدرسهای موجود در آرایه دوم را رمزگذاری میکنیم. هر آدرس را در قالب ۲۰ بایت نمایش میدهیم و با اضافه کردن صفرهای ابتدایی، آن را با ساختار ۳۲ بایتی ABI هماهنگ میکنیم.
و به این ترتیب، تابع:
1 |
transfer([[123, 123], [123]], [0x5b38da6a701c568545dcfcb03fcb875f56beddc4, 0x7b38da6a701c568545dcfcb03fcb875f56bedfb3]) |
به معادل هگزادسیمال قابل فهم برای ماشین مجازی اتریوم (EVM) تبدیل میشود.
این ویدیو خلاصهای از calldata
این مثال را نمایش میدهد.
انیمیشن آرایه سه بعدی تو در تو
در این ویدیو نشان میدهد چگونه یک آرایه سه بعدی uint
با امضای تابع f(uint[][][] memory data) به روش ABI رمزگذاری میشود:
طول calldata و هزینه گس در سالیدیتی
به عنوان یک توسعهدهنده سالیدیتی، یکی از دغدغههای اصلی شما باید صرفهجویی در مصرف گس باشد. بهویژه زمانی که با calldata
کار میکنید، چون هر بایت از calldata هزینه گس دارد.
برای محاسبه هزینه calldata
، ابتدا باید طول آن را بر حسب بایت مشخص کنیم. برای این کار، کافی است تعداد بایتها را بشماریم. بیایید از رشته calldata
مثال قبلیمان به عنوان مطالعه موردی استفاده کنیم:
1 |
0xa9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000 |
0x
را حذف میکنیم، زیرا این فقط یک علامت برای مشخص کردن هگزادسیمال بودن داده در اتریوم است و در محاسبه طول نقشی ندارد. حالا فقط این مقدار برای ما باقی میماند:
1 |
a9059cbb0000000000000000000000003f5047bdb647dc39c88625e17bdbffee905a9f4400000000000000000000000000000000000000000000011c9a62d04ed0c80000 |
این رشته ۱۳۶ رقم هگزادسیمال دارد، یعنی طول آن معادل ۶۸ بایت است. چون هر بایت در رشته calldata
با دو رقم هگزادسیمال نمایش داده میشود، با تقسیم ۱۳۶ بر ۲ میتوان طول را بهدست آورد: ۶۸ = ۲ ÷ ۱۳۶
هر بایت غیر صفر در calldata معادل ۱۶ گس هزینه دارد، در حالی که بایتهای صفر ۴ گس هزینه دارند. بنابراین، برای ادامه محاسبه باید آنها را از هم جدا کنیم.
ما داریم:
32 non-zero-bytes = 32 × 16 = 512 گس
36 zero-bytes = 36 × 4 = 144 گس
کل هزینه گس برای calldata = 512 گس + 144 گس = 656 گس.
از آنجایی که بایتهای صفر ارزانتر هستند، برخی توسعهدهندگان اقدام به تولید (mine) آدرسهایی میکنند — چه برای حسابها و چه برای قراردادهای هوشمند — که چندین بایت صفر در ابتدای خود دارند. این کار باعث میشود هزینه گس برای ارسال آن آدرس به عنوان آرگومان کاهش یابد.
جمع بندی
در طول این راهنما، با مفاهیم پایهای رمزگذاری ABI برای فراخوانی توابع آشنا شدیم، اجزای کلیدی یک فراخوانی تابع به روش ABI را بررسی کردیم، و درک دقیقتری از ساختار calldata
به دست آوردیم. همچنین، یاد گرفتیم چگونه هزینه گس مربوط به calldata
را محاسبه کنیم، و در ادامه با مثالهایی پیچیدهتر، به رمزگشایی و رمزگذاری پیشرفتهتر نیز پرداختیم تا دانش بهدستآمده را تثبیت کنیم. امیدوارم این مطلب برایتان مفید بوده باشد.
تمرین
۱. در فراخوانی تابع foo(uint16 x)
، چند بایت در calldata
وجود دارد؟
۲. رمزگذاری ABI برای foo(uint256 x, uint256[])
در صورتی که (2, [5, 9])
به آن ارسال شود، چیست؟
۳. رمزگذاری ABI برای foo(S[] memory s)
چیست؟ (که S
یک ساختار با فیلدهای uint256 x; uint256[] a;
میباشد)
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۴ تیر ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++C
- ADO.NET
- Adobe Flash
- Ajax
- AngularJS
- apache
- ARM
- Asp.Net
- ASP.NET MVC
- AVR
- Bootstrap
- CCNA
- CCNP
- CMD
- CSS
- Dreameaver
- EntityFramework
- HTML
- IOS
- jquery
- Linq
- Mysql
- Oracle
- PHP
- PHPMyAdmin
- Rational Rose
- silver light
- SQL Server
- Stimulsoft Reports
- Telerik
- UML
- VB.NET&VB6
- WPF
- Xml
- آموزش های پروژه محور
- اتوکد
- الگوریتم تقریبی
- امنیت
- اندروید
- اندروید استودیو
- بک ترک
- بیسیک فور اندروید
- پایتون
- جاوا
- جاوا اسکریپت
- جوملا
- دلفی
- دوره آموزش Go
- دوره های رایگان پیشنهادی
- زامارین
- سئو
- ساخت CMS
- سی شارپ
- شبکه و مجازی سازی
- طراحی الگوریتم
- طراحی بازی
- طراحی وب
- فتوشاپ
- فریم ورک codeigniter
- فلاتر
- کانستراکت
- کریستال ریپورت
- لاراول
- معماری کامپیوتر
- مهندسی اینترنت
- هوش مصنوعی
- یونیتی
- کتاب های آموزشی
- Android
- ASP.NET
- AVR
- LINQ
- php
- Workflow
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس