L1Opcode جدید را برای ذخیرهسازی کلید امن و مقیاسپذیر SLOAD کنید

توابع انتزاع حساب متقابل زنجیره ای به لطف Keystores امکان پذیر خواهد بود. کاربران قادر خواهند بود چندین کیف پول قرارداد هوشمند را در چندین زنجیره و با یک کلید کنترل کنند. این می تواند تجربه کاربری خوب مورد انتظار را برای کاربران نهایی در مجموعه های اتریوم به ارمغان بیاورد.
برای اینکه این اتفاق بیفتد، باید بتوانیم دادههای L1 را از مجموعههای L2 بخوانیم، که در حال حاضر فرآیند بسیار گرانی است. به همین دلیل اسکرول اخیراً پیش کامپایل را معرفی کرده است L1SLOAD
که قادر است وضعیت L1 را سریع و ارزان بخواند. کیف پول ایمن یک نسخه ی نمایشی ارائه شده در Safecon Berlin 2024 ایجاد کرده است. فکر می کنم این فقط شروع است، این می تواند برنامه های زنجیره ای متقابل را در DeFi، بازی ها، شبکه های اجتماعی و بسیاری موارد دیگر بهبود بخشد.
بیایید اکنون با مثالهای عملی، مفاهیم اساسی این بدوی جدید را بیاموزیم که راه جدیدی را برای تعامل با اتریوم باز میکند.
1. کیف پول Scroll Devnet خود را وصل کنید
درحال حاضر، L1SLOAD
فقط در Scroll Devnet موجود است. توجه داشته باشید و آن را با Scroll Sepolia Testnet اشتباه نگیرید. اگرچه هر دو در Sepolia Testnet مستقر هستند، اما زنجیره های جداگانه ای هستند.
ما با اتصال کیف پول خود به Scroll Devnet شروع می کنیم:
- نام:
Scroll Devnet
- RPC:
https://l1sload-rpc.scroll.io
- شناسه زنجیره:
222222
- سمبل:
Sepolia ETH
- کاوشگر:
https://l1sload-blockscout.scroll.io
2. در Scroll Devnet بودجه دریافت کنید
دو روش برای به دست آوردن وجوه در Scroll Devnet وجود دارد. یکی را که ترجیح می دهید انتخاب کنید.
ربات شیر در تلگرام (توصیه می شود)
به این گروه تلگرامی بپیوندید و بنویسید /drop TUADDRESS
(به عنوان مثال /drop 0xd8da6bf26964af9d7eed9e03e53415d37aa96045
) برای دریافت وجوه به طور مستقیم به حساب خود.
پل د سپولیا
می توانید از طریق پل وجوه از Sepolia به Scroll Devnet ارسال کنید. دو راه برای رسیدن به این هدف وجود دارد اما در این مورد ما از Remix استفاده خواهیم کرد.
بیایید اکنون کیف پول شما را با Sepolia ETH به Sepolia Testnet متصل کنیم. به یاد داشته باشید که می توانید Sepolia ETH را به صورت رایگان در یک شیر آب دریافت کنید.
حال رابط زیر را کامپایل کنید.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
interface ScrollMessanger {
function sendMessage(address to, uint value, bytes memory message, uint gasLimit) external payable;
}
سپس، در تب “Deploy & Run”، قرارداد زیر را وصل کنید: 0x561894a813b7632B9880BB127199750e93aD2cd5
.
اکنون می توانید ETH را با فراخوانی تابع ارسال کنید sendMessage
به شرح زیر
- به: آدرس حساب EOA شما. آدرسی که در L2 وجوه دریافت می کند.
- مقدار: مقدار اتری که می خواهید در L2 در قالب wei دریافت کنید. مثلا اگر بفرستید
0.01
ETH شما باید به عنوان یک پارامتر ارسال کنید10000000000000000
- پیام: خالی بگذارید، فقط ارسال کنید
0x00
- محدودیت گاز:
1000000
باید کافی باشه
همچنین به یاد داشته باشید که مقداری ارزش را به تراکنش خود منتقل کنید. و مقداری ETH اضافی برای پرداخت هزینه های L2 اضافه کنید، 0.001
باید بیش از حد کافی باشد بنابراین اگر برای مثال ارسال کردید 0.01
ETH روی پل، یک تراکنش با 0.011
ETH برای پوشش هزینه ها.
همچنین به یاد داشته باشید که یک هزینه اضافی خرج کنید value
در معامله شما یعنی برای پرداخت هزینه های L2 کمی اتریوم اضافی اضافه کنید. 0.001
باید بیش از حد کافی باشد بنابراین، برای مثال، اگر 0.01 ETH روی پل ارسال کردید، یک تراکنش با آن ارسال کنید 0.011
ETH برای پوشش کمیسیون
روی دکمه کلیک کنید transact
و وجوه شما باید در حدود 15 دقیقه برسد.
2. قرارداد خود را در L2 راه اندازی کنید
همانطور که قبلا ذکر کردیم، L1SLOAD
وضعیت قراردادها را در L1 از L2 می خواند. بیایید اکنون یک قرارداد ساده در L1 راه اندازی کنیم که سپس مقدار متغیر را می خوانیم number
از L2.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;
/**
* @title Storage
* @dev Store & retrieve value in a variable
*/
contract L1Storage {
uint256 public number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}
حالا با store(uint256 num)
تابع و ارسال یک مقدار جدید. مثلا بگذریم 42
.
3. یک اسلات از L2 بدست آورید
قرارداد زیر را در L2 با عبور آدرس قراردادی که به تازگی در L1 راه اندازی کرده ایم به عنوان پارامتر در سازنده راه اندازی می کنیم.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;
interface IL1Blocks {
function latestBlockNumber() external view returns (uint256);
}
contract L2Storage {
address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
uint256 constant NUMBER_SLOT = 0;
address immutable l1StorageAddr;
uint public l1Number;
constructor(address _l1Storage) {
l1StorageAddr = _l1Storage;
}
function latestL1BlockNumber() public view returns (uint256) {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
return l1BlockNum;
}
function retrieveFromL1() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
bytes memory input = abi.encodePacked(l1BlockNum, l1StorageAddr, NUMBER_SLOT);
bool success;
bytes memory ret;
(success, ret) = L1_SLOAD_ADDRESS.call(input);
if (success) {
(l1Number) = abi.decode(ret, (uint256));
} else {
revert("L1SLOAD failed");
}
}
}
توجه داشته باشید که این قرارداد ابتدا تماس می گیرد latestL1BlockNumber()
برای دریافت آخرین بلوک در L1 که برای خواندن در L2 در دسترس است. بعد زنگ میزنیم L1SLOAD
(opcode 0x101
) ارسال آدرس قرارداد در L1 به عنوان پارامتر و شکاف 9، جایی که متغیر number
در داخل آن قرارداد قرار دارد.
حالا میتونیم زنگ بزنیم retrieveFromL1()
برای بدست آوردن مقدار ذخیره شده قبلی
مثال شماره 2: خواندن انواع دیگر متغیرها
Solidity اسلات های متغیر را به همان ترتیبی که اعلام شده اند ذخیره می کند. این برای ما کاملاً راحت است. به عنوان مثال، در قرارداد زیر، account
در اسلات شماره 0 ذخیره می شود، number
در شماره 1 و text
در 2.
// SPDX-License-Identifier: MIT
pragma solidity >=0.7.0 <0.9.0;
contract AdvancedL1Storage {
address public account;
uint public number;
string public text;
}
ما می توانیم ببینیم که چگونه می توانیم مقادیر انواع مختلف را بدست آوریم: uint256، آدرس و غیره… رشته ها به دلیل ماهیت متغیر اندازه آنها کمی متفاوت هستند.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;
interface IL1Blocks {
function latestBlockNumber() external view returns (uint256);
}
contract L2Storage {
address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
address immutable l1ContractAddress;
address public account;
uint public number;
string public test;
constructor(address _l1ContractAddress) { //0x5555158Ea3aB5537Aa0012AdB93B055584355aF3
l1ContractAddress = _l1ContractAddress;
}
// Internal functions
function latestL1BlockNumber() internal view returns (uint256) {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
return l1BlockNum;
}
function retrieveSlotFromL1(uint blockNumber, address l1StorageAddress, uint slot) internal returns (bytes memory) {
bool success;
bytes memory returnValue;
(success, returnValue) = L1_SLOAD_ADDRESS.call(abi.encodePacked(blockNumber, l1StorageAddress, slot));
if(!success)
{
revert("L1SLOAD failed");
}
return returnValue;
}
function decodeStringSlot(bytes memory encodedString) internal pure returns (string memory) {
uint length = 0;
while (length < encodedString.length && encodedString[length] != 0x00) {
length++;
}
bytes memory data = new bytes(length);
for (uint i = 0; i < length; i++) {
data[i] = encodedString[i];
}
return string(data);
}
// Public functions
function retrieveAddress() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
account = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 0), (address));
}
function retrieveNumber() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
number = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 1), (uint));
}
function retrieveString() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
test = decodeStringSlot(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 2));
}
function retrieveAll() public {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
account = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 0), (address));
number = abi.decode(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 1), (uint));
test = decodeStringSlot(retrieveSlotFromL1(l1BlockNum, l1ContractAddress, 2));
}
}
مثال شماره 3: موجودی توکن ERC20 را در L1 بخوانید
ما با راه اندازی یک توکن نسبتا ساده ERC20 شروع کردیم.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract SimpleToken is ERC20 {
constructor(
string memory name,
string memory symbol,
uint256 initialSupply
) ERC20(name, symbol) {
_mint(msg.sender, initialSupply * 1 ether);
}
}
سپس، قرارداد زیر را در L2 راهاندازی میکنیم و به عنوان پارامتر آدرس توکنی را که به تازگی در L1 راهاندازی کردهایم، ارسال میکنیم.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.20;
interface IL1Blocks {
function latestBlockNumber() external view returns (uint256);
}
contract L2Storage {
address constant L1_BLOCKS_ADDRESS = 0x5300000000000000000000000000000000000001;
address constant L1_SLOAD_ADDRESS = 0x0000000000000000000000000000000000000101;
address immutable l1ContractAddress;
uint public l1Balance;
constructor(address _l1ContractAddress) {
l1ContractAddress = _l1ContractAddress;
}
// Internal functions
function latestL1BlockNumber() public view returns (uint256) {
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
return l1BlockNum;
}
function retrieveSlotFromL1(uint blockNumber, address l1StorageAddress, uint slot) internal returns (bytes memory) {
bool success;
bytes memory returnValue;
(success, returnValue) = L1_SLOAD_ADDRESS.call(abi.encodePacked(blockNumber, l1StorageAddress, slot));
if(!success)
{
revert("L1SLOAD failed");
}
return returnValue;
}
// Public functions
function retrieveL1Balance(address account) public {
uint slotNumber = 0;
uint256 l1BlockNum = IL1Blocks(L1_BLOCKS_ADDRESS).latestBlockNumber();
l1Balance = abi.decode(retrieveSlotFromL1(
l1BlockNum,
l1ContractAddress,
uint(keccak256(
abi.encodePacked(uint160(account),slotNumber)
)
)
), (uint));
}
}
قراردادهای OpenZeppelin به راحتی نقشه تعادل توکن را در شکاف 0 قرار می دهند. بنابراین می توانید تماس بگیرید retrieveL1Balance()
ارسال آدرس دارنده به عنوان پارامتر و موجودی توکن در متغیر ذخیره می شود l1Balance
. همانطور که در کد می بینید، فرآیند به این صورت است که ابتدا آدرس را به uint160 تبدیل می کنیم و سپس آن را با شکاف نگاشت که 0 است، هش می کنیم. این به این دلیل است که Solidity نگاشت ها را به این صورت پیاده سازی می کند.
با تشکر از خواندن این راهنما!
من را در dev.to و YouTube برای همه چیزهایی که به توسعه بلاک چین به زبان اسپانیایی مرتبط است دنبال کنید.