مقایسه فراخوانی سطح پایین و سطح بالا در سالیدیتی

در زبان برنامه نویسی سالیدیتی، قراردادها می توانند به دو روش با یکدیگر ارتباط برقرار کنند. روش اول استفاده از رابط قرارداد (interface) است که به آن فراخوانی سطح بالا (High-Level Call) می گویند. روش دوم استفاده از تابع call است که یک فراخوانی سطح پایین (Low-Level Call) محسوب می شود. در این مقاله، تفاوت فراخوانی سطح پایین و سطح بالا در سالیدیتی را بررسی می کنیم تا درک بهتری از رفتار این دو روش در زمان اجرا و مواجهه با خطا به دست آوریم.

هرچند هر دو روش در نهایت از دستور اجرایی CALL در ماشین مجازی اتریوم استفاده می کنند، اما سالیدیتی آن‌ها را به شکل متفاوتی مدیریت می کند.

در این مقاله، خواهیم دید که چرا فراخوانی سطح پایین هیچ وقت به طور خودکار باعث برگشت نمی شود، اما فراخوانی سطح بالا ممکن است عملیات را متوقف کند. همچنین بررسی می کنیم که چرا فراخوانی سطح پایین با یک آدرس خالی موفق محسوب می شود، ولی فراخوانی سطح بالا با یک قرارداد ناموجود باعث خطا می شود.

چرا فراخوانی سطح پایین (یا delegatecall) باعث برگشت نمی شود، اما فراخوانی از طریق رابط قرارداد ممکن است برگشت داشته باشد؟

پیش از آنکه علت را توضیح بدهیم، بهتر است بخشی از مستندات رسمی سالیدیتی را نقل کنیم که مستقیماً به این موضوع اشاره دارد:

زمانی که یک استثناء (Exception) در فراخوانی یک زیرقرارداد رخ می دهد، به صورت خودکار “به سطح بالا منتقل می شود” (یعنی خطا دوباره پرتاب می شود)، مگر اینکه در یک بلوک try/catch مدیریت شده باشد.
اما یک استثناء در توابع سطح پایین مانند call، delegatecall و staticcall این قاعده را دنبال نمی کند. این توابع به جای پرتاب خطا، مقدار false را به عنوان اولین خروجی برمی گردانند.

برای درک بهتر این تفاوت، در ادامه رفتار دو نوع فراخوانی را با هم مقایسه می کنیم: یکی از طریق رابط قرارداد (فراخوانی سطح بالا) و دیگری از طریق تابع call (فراخوانی سطح پایین). در این مثال، از متد call استفاده می کنیم، اما همین اصول در مورد delegatecall نیز صدق می کند.

قرارداد Caller می تواند تابع ops() را از قرارداد Called به دو روش مختلف صدا بزند. دقت کنید که تابع ops() همیشه عملیات را با خطا متوقف می کند (revert):

با وجود اینکه هر دو روش برای فراخوانی یک تابع مشخص به کار می روند و هر دو از دستور اجرایی CALL در سطح ماشین مجازی اتریوم (EVM) استفاده می کنند، کامپایلر سالیدیتی در زمان تولید بایت‌ کد، نحوه مدیریت خطاها را به شیوه‌ متفاوتی پیاده سازی می کند.

اگر هر دو تابع callByInterface و callByCall را در قرارداد Caller اجرا کنیم، متوجه می شویم که اجرای callByInterface باعث برگشت (revert) می شود، در حالی که callByCall بدون برگشت عمل می کند.

در سطح ماشین مجازی اتریوم، دستور CALL تنها یک مقدار بولی (true یا false) باز می گرداند که موفقیت یا شکست فراخوانی را مشخص می کند و این مقدار را روی پشته قرار می دهد. خود این دستور، باعث برگشت عملیات نمی شود.

زمانی که فراخوانی از طریق رابط قرارداد انجام می شود، سالیدیتی به صورت خودکار مقدار برگشتی را بررسی می کند. اگر مقدار برگشتی false باشد، زبان سالیدیتی یک برگشت صریح (revert) ایجاد می کند؛ مگر اینکه فراخوانی داخل یک بلوک try/catch انجام شده باشد. این رفتار یکی از نکات کلیدی در برنامه نویسی قراردادهای هوشمند است، زیرا درک درست از نحوه مدیریت خطاها می‌تواند از بسیاری از باگ‌های رایج جلوگیری کند.

اما در فراخوانی های سطح پایین، مدیریت این مقدار به عهده برنامه نویس است. در این حالت، ما باید مقدار بولی بازگشتی را به صورت دستی بررسی کنیم و در صورت نیاز، خودمان دستور revert را اجرا کنیم.

این تفاوت میان فراخوانی سطح بالا و فراخوانی سطح پایین در نمودار زیر به خوبی نمایش داده شده است:

مدیریت فراخوانی سطح پایین برای بازگشت در مقابل مدیریت فراخوانی سطح بالا برای بازگشت

تفاوت فراخوانی مستقیم (call) و فراخوانی از طریق رابط (interface) هنگام فراخوانی با یک آدرس خالی

در سالیدیتی، متد سطح پایین call پیش از اجرای عملیات، بررسی نمی کند که آیا آدرس مورد نظر واقعاً به یک قرارداد هوشمند اشاره دارد یا خیر. البته در صورت نیاز، خود قرارداد می تواند با استفاده از دستور EXTCODESIZE (که پشت صحنه ویژگی address.code.length قرار دارد) بررسی کند که آیا در آن آدرس، کدی مستقر شده یا نه. اگر اندازه کد برابر صفر باشد، مشخص می شود که در آن آدرس هیچ قرارداد هوشمندی مستقر نیست.

اما متد call این بررسی را انجام نمی دهد و صرف نظر از اینکه آدرس مقصد معتبر باشد یا نه، مستقیماً دستور CALL را اجرا می کند.

در مقابل، زمانی که از رابط قرارداد برای فراخوانی استفاده می کنیم، سالیدیتی قبل از اجرای CALL اندازه کد آدرس مقصد را بررسی می کند. در بایت‌ کدی که برای تابع callByInterface تولید می شود، ابتدا دستور EXTCODESIZE روی آدرس مورد نظر اجرا می شود. اگر این دستور مقدار صفر برگرداند (یعنی هیچ قراردادی در آن آدرس وجود ندارد)، عملیات پیش از اجرای CALL متوقف می شود و تابع به صورت خودکار revert می دهد.

به همین دلیل است که وقتی تابع callByInterface با یک آدرس ناموجود اجرا می شود، عملیات با خطا مواجه می شود. اما در همان شرایط، اجرای callByCall به ظاهر موفق انجام می شود، چون بررسی وجود قرارداد در آن انجام نمی شود.

در تصویر زیر، تفاوت نحوه برخورد هر دو روش هنگام فراخوانی با یک آدرس خالی به صورت شماتیک نمایش داده شده است:

فراخوانی سطح پایین برای فراخوانی یک قرارداد خالی در مقابل فراخوانی سطح بالا برای فراخوانی یک قرارداد خالی

در اصل، ماشین مجازی اتریوم (EVM) یک عملیات را زمانی متوقف می کند (revert)، که یکی از شرایط زیر رخ بدهد:

  • اجرای دستور REVERT

  • تمام شدن گاز (Gas)

  • انجام عملیاتی که ممنوع است، مانند تقسیم عددی بر صفر

اما وقتی که تماس به یک آدرس خالی انجام می شود، هیچ‌کدام از این شرایط اتفاق نمی افتند. چون در آن آدرس، هیچ کدی برای اجرا وجود ندارد، دستوری مانند REVERT اجرا نمی شود، مصرف گاز به حد بحرانی نمی‌رسد و هیچ عملیات محاسباتی ممنوعه‌ای رخ نمیدهد.

به همین دلیل است که تماس سطح پایین (call) به یک آدرس خالی، حتی اگر هیچ اثر واقعی نداشته باشد، از نظر ماشین مجازی اتریوم «موفق» تلقی می شود و برگشت نمی دهد.

5/5 - (1 امتیاز)

راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.

دوره آموزش طراحی وب سایت مدرسه با PHP و MySql
  • انتشار: ۶ تیر ۱۴۰۴

دسته بندی موضوعات

آخرین محصولات فروشگاه

مشاهده همه

نظرات

بازخوردهای خود را برای ما ارسال کنید