تراکنش های فهرست دسترسی در اتریوم (Access List Transactions) به کاربران کمک میکنند هنگام اجرای فراخوانی های بین قراردادها، مصرف گس را کاهش دهند. برای این کار، کاربر از قبل اعلام میکند کدام قراردادها و اسلات های ذخیره سازی قرار است در طول تراکنش مورد استفاده قرار بگیرند. با این روش، هر اسلاتی که از قبل در فهرست آمده باشد، میتواند تا 100 واحد گس صرفهجویی ایجاد کند.
توسعه دهندگان EIP-2930 را معرفی کردند تا تأثیر منفی تغییرات EIP-2929 را کاهش دهند. آن تغییرات هزینه دسترسی به حافظه سرد را افزایش داده بودند.
در EIP-2929، تیم توسعه تصمیم گرفت قیمتگذاری اشتباه عملیات های حافظه را اصلاح کند؛ زیرا این قیمت های پایین، خطر حملات انکار سرویس (DoS) را افزایش میداد. با اینکه این تصمیم امنیت شبکه را بالا برد، اما باعث شد برخی قراردادهای هوشمند دیگر بهدرستی کار نکنند. برای حل این مشکل، توسعه دهندگان از EIP-2930 استفاده کردند تا فهرست های دسترسی اختیاری را وارد شبکه کنند.
EIP-2930 به کاربران اجازه میدهد اسلات های مورد نیاز را پیش از اجرای تراکنش، گرم کنند (pre-warm). در نتیجه این اسلات ها دیگر حافظه سرد محسوب نمیشوند و هزینه کمتری برای دسترسی به آنها پرداخت میشود. توالی شمارههای EIP-2929 و EIP-2930 نیز کاملاً هدفمند بوده است؛ زیرا اولی مشکل را ایجاد کرد و دومی راهحل آن را ارائه داد. این مفهوم، بهویژه برای افرادی که بهتازگی وارد حوزه برنامه نویسی و توسعه قراردادهای هوشمند شدهاند، کاربردی و مهم است؛ زیرا میتواند در بهینه سازی عملکرد و هزینه تراکنش ها نقش کلیدی داشته باشد.
نحوه عملکرد EIP-2930
تراکنش هایی که بر اساس EIP-2930 اجرا میشوند، دقیقاً مانند سایر تراکنش های اتریوم عمل میکنند، با این تفاوت که هزینه دسترسی به حافظه سرد (cold storage) پیش از اجرای عملیات SLOAD و با تخفیف پرداخت میشود، نه در حین اجرای آن.
برای استفاده از این قابلیت، نیازی نیست تغییری در کد سالیدیتی ایجاد شود؛ این ویژگی بهطور کامل در سمت کلاینت (node client) مشخص میشود و از طریق رابط کاربری یا API تراکنش اعمال میگردد.
با پرداخت این هزینه از پیش، دسترسی سرد به اسلات ذخیره سازی پیشپرداخت میشود. بنابراین، زمانی که تراکنش در حال اجرا است، فقط هزینه دسترسی گرم (warm access fee) از کاربر کسر میشود. اگر کلاینت گره اتریوم از قبل کلیدهای ذخیره سازی موردنیاز را بداند، میتواند مقادیر حافظه را زودتر بارگذاری کند. این کار امکان اجرای موازی برخی عملیات ها بین محاسبات و دسترسی به ذخیره سازی را فراهم میکند.
EIP-2930 دسترسی به حافظهای که در فهرست دسترسی وجود ندارد را محدود نمیکند. افزودن یک ترکیب «آدرس و اسلات حافظه» به فهرست دسترسی، هیچ تعهدی برای استفاده از آن ایجاد نمیکند. اما اگر آن ترکیب واقعاً در زمان اجرای تراکنش استفاده نشود، کاربر بدون هیچ منفعتی هزینه دسترسی سرد را از قبل پرداخت کرده است.
کاهش هزینه دسترسی
بر اساس EIP-2930، هاردفورک Berlin هزینه دسترسی “سرد” به اپکدهای مربوط به حسابها (مانند BALANCE، تمام فراخوانیهای CALL و EXT*) را به 2600 گس افزایش داد. همچنین هزینه دسترسی سرد به وضعیت ذخیره سازی (اپکد SLOAD) نیز از 800 به 2100 گس افزایش پیدا کرد. در مقابل، هزینه دسترسی “گرم” برای هر دو نوع عملیات به 100 گس کاهش یافت.
با این حال، EIP-2930 یک مزیت اضافی هم دارد: این استاندارد به هر تراکنش شامل فهرست دسترسی، تخفیف 200 گس اختصاص میدهد. این تخفیف باعث میشود هزینه نهایی تراکنش کمتر شود.
در نتیجه، به جای پرداخت 2600 گس برای CALL و 2100 گس برای SLOAD در حالت سرد، کاربر فقط 2400 گس برای CALL و 1900 گس برای SLOAD میپردازد. در ادامه، اگر همان دسترسیها مجدداً انجام شوند، چون در وضعیت “گرم” قرار دارند، فقط 100 گس مصرف خواهند کرد.
پیاده سازی یک تراکنش با فهرست دسترسی
در این بخش، یک فهرست دسترسی (Access List) را پیاده سازی میکنیم، سپس آن را با یک تراکنش معمولی مقایسه میکنیم و در نهایت، چند معیار عددی از نظر مصرف گس ارائه میدهیم.
برای شروع، بیایید نگاهی بیندازیم به قراردادی که قصد داریم آن را فراخوانی کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 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(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
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; }); |
قسمت type
که مقدار آن 1 است و درست بالای accessList
قرار دارد، مشخص میکند که این تراکنش از نوع تراکنش فهرست دسترسی (Access List Transaction) است.
مقدار accessList
یک آرایه از آبجکت ها است که هرکدام شامل یک آدرس و مجموعهای از اسلات های ذخیره سازی هستند که تراکنش قصد دارد به آنها دسترسی پیدا کند.
مقادیر storage slots
(یا همان storageKeys
که در کد تعریف شدهاند) باید دقیقاً ۳۲ بایت باشند. به همین دلیل است که در مقداردهی آنها، صفرهای زیادی در ابتدای رشته ها مشاهده میکنیم.
در این مثال، ما دو کلید ذخیره سازی ۳۲ بایتی داریم که مقادیرشان متناظر با صفر و یک هستند؛ چون تابع getSum
که از طریق قرارداد Caller
فراخوانی میشود، دقیقاً به همین اسلات ها در قرارداد Calculator
دسترسی پیدا میکند. بهطور خاص، متغیر x
در اسلات شماره صفر و متغیر y
در اسلات شماره یک قرار دارد.
نتایج
خروجی اجرای کد به صورت زیر نمایش داده شده:
1 2 3 4 5 6 7 |
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 واحد گس صرفهجویی کنیم (این نتیجه بدون توجه به تنظیمات بهینهساز (optimizer) همچنان برقرار است).
در این تراکنش، فراخوانی قرارداد خارجی بهتنهایی 200 گس صرفهجویی ایجاد کرده، و هر یک از دو دسترسی به اسلات های ذخیره سازی نیز 200 گس تخفیف به همراه داشتهاند؛ یعنی در مجموع، پتانسیل صرفهجویی تا 600 گس وجود داشته است.
با این حال، باید هزینه دسترسی گرم (warm access) را هم در نظر بگیریم؛ چون هر سه عملیات (فراخوانی خارجی و دو دسترسی به حافظه) همچنان به میزان 100 گس برای هر کدام نیاز دارند. بنابراین، از 600 گس صرفهجویی بالقوه، فقط 300 گس صرفهجویی خالص بهدست آمده است.
محاسبه دقیق:
اگر از فهرست دسترسی استفاده نمیکردیم، هزینه های مربوط به دسترسی سرد به صورت زیر محاسبه میشد:
2600 (برای CALL) + 2100 × 2 (برای SLOAD) = 6800 گس
اما با فهرست دسترسی، این مقادیر از پیش پرداخت شده و به شکل زیر کاهش یافتند:
2400 (برای CALL) + 1900 × 2 (برای SLOAD) = 6200 گس
سپس باید هزینه دسترسی گرم را نیز اضافه کنیم:
100 (برای CALL) + 100 × 2 (برای دو SLOAD) = 300 گس
در مجموع، در این حالت 6500 گس پرداخت کردیم، در حالی که بدون فهرست دسترسی باید 6800 گس پرداخت میکردیم. بنابراین، صرفهجویی خالص دقیقاً 300 گس بوده است.
بهدست آوردن اسلات های ذخیره سازی در یک تراکنش فهرست دسترسی
کلاینت Go-Ethereum (معروف به geth) متدی با نام eth_createAccessList
در اختیار دارد که از طریق RPC به راحتی میتوان اسلات های ذخیره سازی موردنیاز برای یک تراکنش را مشخص کرد. (برای مثال میتوان به مستندات API مربوط به web3.js مراجعه کرد.)
با استفاده از این متد RPC، کلاینت بهطور خودکار اسلات های ذخیره سازی مورد استفاده در تراکنش را شناسایی میکند و سپس فهرست دسترسی (access list) متناظر را برمیگرداند.
علاوه بر این، در ابزار Foundry نیز میتوان از این قابلیت بهرهبرداری کرد. با اجرای دستور cast access-list
در Foundry، متد eth_createAccessList
در پسزمینه اجرا میشود و در نهایت فهرست دسترسی تولید میشود.
در این مثال، میخواهیم با قرارداد UniswapV2 Factory در شبکه آزمایشی Göerli تعامل داشته باشیم. هدف این است که تابع allPairs
را فراخوانی کنیم. این تابع بر اساس ایندکس ورودی، آدرس یکی از قراردادهای جفتارز (pair contract) را از یک آرایه بازمیگرداند.
برای این کار، دستور زیر را در یک نسخه فورک شده از شبکه Göerli اجرا میکنیم:
1 |
cast access-list 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f "allPairs(uint256)" 0 |
1 2 3 4 5 6 7 |
gas used: 27983 // مقدار گس مصرفشده توسط تراکنش access-list: - address: 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f // آدرس قرارداد UniswapV2 Factory keys: 0xc2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85b // اسلات مربوط به آدرس جفتارز 0x0000000000000000000000000000000000000000000000000000000000000003 // اسلات مربوط به طول آرایه |
نمونهای از هدر رفت گس هنگام استفاده نادرست از فهرست دسترسی
اگر اسلات ذخیره سازی بهاشتباه محاسبه شود، تراکنش همچنان هزینه مربوط به فهرست دسترسی را پرداخت میکند، اما هیچ سودی از آن نمیبرد. در چنین شرایطی، صرفهجویی در مصرف گس اتفاق نمیافتد و بخشی از هزینه بهصورت کاملاً بیفایده مصرف میشود.
برای روشنتر شدن موضوع، یک تراکنش فهرست دسترسی را بررسی میکنیم که به دلیل اشتباه در محاسبه اسلات ذخیره سازی، بهینه عمل نمیکند و در نهایت باعث هدر رفت گس میشود.
در این بنچمارک خاص، هزینه از قبل برای اسلات شماره ۱ پرداخت شده، در حالی که تابع موردنظر در واقع فقط به اسلات شماره ۰ دسترسی دارد. این اختلاف باعث میشود فهرست دسترسی نهتنها کمکی به کاهش هزینه نکند، بلکه بخشی از گس هم بدون نتیجه مصرف شود.
1 2 3 4 5 6 7 8 9 10 |
// 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 مورد استفاده قرار میگیرد:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
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; }); |
1 2 3 |
Slot contract deployed to 0x5FbDB2315678afecb367f032d93F642f64180aa3 gas cost for incorrect access list: 27610 gas cost for tx without access list: 23310 |
تراکنش با وجود استفاده از اسلات ذخیره سازی اشتباه، با موفقیت انجام شد؛ اما اگر هیچ فهرست دسترسی تعریف نمیکردیم، هزینه کمتری پرداخت میکردیم.
در واقع، استفاده از فهرست دسترسی که بهدرستی محاسبه نشده، نه تنها مزیتی ایجاد نمیکند، بلکه باعث افزایش غیرضروری هزینه گس نیز میشود. این مثال بهخوبی نشان میدهد که دقت در تعیین صحیح اسلات های حافظه برای استفاده از EIP-2930 کاملاً حیاتی است.
از فهرست دسترسی در شرایط غیرقابل پیش بینی استفاده نکنید
بر اساس آنچه در بخش قبل بررسی شد، میتوان نتیجه گرفت که در شرایطی که اسلات های ذخیره سازی بهصورت قطعی (Deterministic) قابل محاسبه نیستند، استفاده از فهرست دسترسی توصیه نمیشود.
بهعنوان نمونه، اگر شماره اسلات ذخیره سازی بر اساس شماره یک بلاک خاص محاسبه شود، این مقدار ماهیت غیرقطعی دارد و نمیتوان آن را بهصورت دقیق پیشبینی و از قبل در فهرست وارد کرد.
مثال دیگر، اسلات هایی هستند که وابسته به زمان اجرای تراکنشاند. در برخی پیادهسازیهای استاندارد ERC-721، آدرس مالک توکن بهصورت پویا به یک آرایه اضافه میشود و سیستم از ایندکس آرایه برای تعیین مالکیت NFT استفاده میکند. در این ساختار، موقعیت ذخیره سازی هر توکن وابسته به ترتیب مینت شدن توسط کاربران است و معمولاً امکان پیشبینی آن از قبل وجود ندارد.
به همین دلیل، استفاده از فهرست دسترسی در چنین شرایطی نهتنها مزیتی ایجاد نمیکند، بلکه حتی ممکن است باعث افزایش غیرضروری هزینه گس نیز بشود.
به همین دلیل، توسعه دهندگان باید از بهکارگیری Access List در تراکنشهایی با الگوهای دسترسی غیرقطعی اجتناب کنند.
در چه مواقعی فهرست دسترسی باعث صرفهجویی در گس میشود؟
هر زمان که قرار است فراخوانی بین قراردادها (cross-contract call) انجام دهید، بهتر است استفاده از تراکنش با فهرست دسترسی (Access List Transaction) را مدنظر قرار دهید.
در حالت عادی، اجرای یک فراخوانی بین دو قرارداد باعث مصرف 2600 گس اضافی برای دسترسی سرد به قرارداد مقصد میشود. اما اگر از فهرست دسترسی استفاده کنید، تنها 2400 گس برای دسترسی اولیه پرداخت میکنید و از آنجا که قرارداد مقصد به حالت “گرم” (prewarmed) منتقل شده، در ادامه تنها 100 گس دیگر بابت استفاده از آن دریافت میشود. در نتیجه، هزینه کل از 2600 به 2500 گس کاهش پیدا میکند.
همین منطق در مورد دسترسی به متغیرهای ذخیره شده در قرارداد دیگر نیز صادق است. دسترسی سرد به چنین متغیرهایی بهطور معمول 2100 گس هزینه دارد. اما زمانی که از فهرست دسترسی استفاده میکنید، سیستم تنها 1900 گس برای گرم کردن اسلات حافظه دریافت میکند و سپس فقط 100 گس دیگر برای استفاده واقعی از آن، که در مجموع به 100 گس صرفهجویی خالص منجر میشود.
در مخزن (repository) مربوطه، چند نمونه از استفادههای رایج فهرست دسترسی در فراخوانیهای بین قراردادها ارائه شدهاند، از جمله:
-
دسترسی به قیمت در اوراکل Chainlink
-
فراخوانی
delegatecall
توسط یک قرارداد پراکسی به قرارداد پیادهسازی -
انتقال توکن ERC-20 از طریق فراخوانی بین دو قرارداد
چه زمانی نباید از تراکنش فهرست دسترسی استفاده کرد؟
زمانی که تراکنش فقط به یک قرارداد هوشمند خاص دسترسی دارد، استفاده از فهرست دسترسی هیچ مزیتی ایجاد نمیکند. دلیل آن این است که فراخوانی مستقیم یک قرارداد، هزینه اضافی جداگانهای ندارد و هزینه آن در همان ۲۱٬۰۰۰ گس پایه تراکنش گنجانده شده است. بنابراین، افزودن فهرست دسترسی در چنین مواردی تنها پیچیدگی ایجاد میکند، بدون اینکه موجب صرفهجویی در مصرف گس شود.
جمعبندی
تراکنش های فهرست دسترسی در اتریوم (EIP-2930) ابزار مناسبی برای صرفه جویی در مصرف گس هستند، بهویژه زمانی که آدرس قرارداد و اسلات ذخیره سازی مورد نظر در یک فراخوانی بین قراردادها قابل پیشبینی باشد. در این شرایط، میتوان برای هر اسلات ذخیره سازی تا ۲۰۰ گس صرفهجویی ایجاد کرد.
اما اگر تراکنش فقط به یک قرارداد دسترسی دارد یا اگر ترکیب آدرس و اسلات ذخیره سازی قابل پیشبینی نیست، استفاده از فهرست دسترسی توصیه نمیشود؛ چرا که نه تنها کمکی نمیکند، بلکه ممکن است هزینه کلی را نیز افزایش دهد.
راستی! برای دریافت مطالب جدید در کانال تلگرام یا پیج اینستاگرام سورس باران عضو شوید.
- انتشار: ۳۱ تیر ۱۴۰۴
دسته بندی موضوعات
- آموزش ارز دیجیتال
- آموزش برنامه نویسی
- آموزش متنی برنامه نویسی
- اطلاعیه و سایر مطالب
- پروژه برنامه نویسی
- دوره های تخصصی برنامه نویسی
- رپورتاژ
- فیلم های آموزشی
- ++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
- اچ تی ام ال
- بانک اطلاعاتی
- برنامه نویسی سوکت
- برنامه نویسی موبایل
- پاسکال
- پایان نامه
- پایتون
- جاوا
- جاوا اسکریپت
- جی کوئری
- داده کاوی
- دلفی
- رباتیک
- سئو
- سایر کتاب ها
- سخت افزار
- سی اس اس
- سی پلاس پلاس
- سی شارپ
- طراحی الگوریتم
- فتوشاپ
- مقاله
- مهندسی نرم افزار
- هک و امنیت
- هوش مصنوعی
- ویژوال بیسیک
- نرم افزار و ابزار برنامه نویسی
- وردپرس