Pesan L1-L2
Fitur penting dari Layer 2 adalah kemampuannya untuk berinteraksi dengan Layer 1.
Starknet memiliki sistem L1-L2
Messaging sendiri, yang berbeda dari mekanisme konsensusnya dan pengiriman pembaruan status di L1. Messaging adalah cara bagi smart-contract di L1 untuk berinteraksi dengan smart-contract di L2 (atau sebaliknya), memungkinkan kita melakukan transaksi "cross-chain". Sebagai contoh, kita dapat melakukan beberapa komputasi pada suatu rantai dan menggunakan hasil komputasi ini pada rantai lain.
Semua jembatan di Starknet menggunakan L1-L2
messaging. Katakanlah Anda ingin menjembatani token dari Ethereum ke Starknet. Anda hanya perlu mendepositkan token Anda di kontrak jembatan L1, yang secara otomatis akan memicu pencetakan token yang sama di L2. Penggunaan kasus lain yang bagus untuk L1-L2
messaging adalah pengelolaan DeFi.
Di Starknet, penting untuk dicatat bahwa sistem messaging ini asynchronous dan asymmetric.
- Asynchronous: ini berarti bahwa dalam kode kontrak Anda (baik itu solidity atau cairo), Anda tidak dapat menunggu hasil dari pesan yang dikirim di rantai lain dalam eksekusi kode kontrak Anda.
- Asymmetric: mengirim pesan dari Ethereum ke Starknet (
L1->L2
) sepenuhnya diotomatisasi oleh sequencer Starknet, yang berarti pesan tersebut secara otomatis dikirim ke kontrak target di L2. Namun, saat mengirim pesan dari Starknet ke Ethereum (L2->L1
), hanya hash dari pesan yang dikirim di L1 oleh sequencer Starknet. Anda kemudian harus mengonsumsi pesan tersebut secara manual melalui transaksi di L1.
Mari kita telusuri detailnya.
Kontrak StarknetMessaging
Komponen penting dari sistem L1-L2
Messaging adalah kontrak StarknetCore
. Ini adalah kumpulan kontrak Solidity yang didistribusikan di Ethereum yang memungkinkan Starknet untuk berfungsi dengan baik. Salah satu kontrak dari StarknetCore
disebut StarknetMessaging
dan merupakan kontrak yang bertanggung jawab atas pengiriman pesan antara Starknet dan Ethereum. StarknetMessaging
mengikuti antarmuka dengan fungsi-fungsi yang memungkinkan pengiriman pesan ke L2, menerima pesan di L1 dari L2, dan membatalkan pesan.
interface IStarknetMessaging is IStarknetMessagingEvents {
function sendMessageToL2(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload
) external returns (bytes32);
function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload)
external
returns (bytes32);
function startL1ToL2MessageCancellation(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload,
uint256 nonce
) external;
function cancelL1ToL2Message(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload,
uint256 nonce
) external;
}
Antarmuka kontrak messaging Starknet
Dalam kasus pesan L1->L2
, sequencer Starknet terus-menerus mendengarkan log yang dihasilkan oleh kontrak StarknetMessaging
di Ethereum. Begitu sebuah pesan terdeteksi dalam log, sequencer menyiapkan dan mengeksekusi L1HandlerTransaction
untuk memanggil fungsi di kontrak L2 yang dituju. Proses ini memakan waktu 1-2 menit (beberapa detik untuk blok Ethereum ditambang, dan kemudian sequencer harus membangun dan mengeksekusi transaksi).
Pesan L2->L1
disiapkan oleh eksekusi kontrak di L2 dan merupakan bagian dari blok yang dihasilkan. Ketika sequencer menghasilkan blok, ia mengirim hash setiap pesan yang disiapkan oleh eksekusi kontrak
ke kontrak StarknetCore
di L1, di mana pesan tersebut kemudian dapat dikonsumsi setelah blok yang mereka miliki terbukti dan diverifikasi di Ethereum (saat ini sekitar 3-4 jam).
Mengirim pesan dari Ethereum ke Starknet
Jika Anda ingin mengirim pesan dari Ethereum ke Starknet, kontrak Solidity Anda harus memanggil fungsi sendMessageToL2
dari kontrak StarknetMessaging
. Untuk menerima pesan-pesan ini di Starknet, Anda perlu menandai fungsi-fungsi yang dapat dipanggil dari L1 dengan atribut #[l1_handler]
.
Mari kita lihat sebuah kontrak sederhana yang diambil dari tutorial ini di mana kita ingin mengirim pesan ke Starknet.
_snMessaging
adalah variabel state yang sudah diinisialisasi dengan Address kontrak StarknetMessaging
. Anda dapat memeriksa Address-Address tersebut di sini.
// Mengirim pesan di Starknet dengan satu nilai felt.
function sendMessageFelt(
uint256 contractAddress,
uint256 selector,
uint256 myFelt
)
external
payable
{
// Kami "serialize" nilai felt ke dalam payload, yang merupakan array uint256.
uint256[] memory payload = new uint256[](1);
payload[0] = myFelt;
// msg.value harus selalu >= 20_000 wei.
_snMessaging.sendMessageToL2{value: msg.value}(
contractAddress,
selector,
payload
);
}
Fungsi ini mengirim pesan dengan satu nilai felt ke kontrak StarknetMessaging
.
Harap dicatat bahwa jika Anda ingin mengirim data yang lebih kompleks, Anda dapat melakukannya. Hanya perhatikan bahwa kontrak cairo Anda hanya akan memahami tipe data felt252
. Jadi Anda harus memastikan bahwa serialisasi data Anda ke dalam array uint256
mengikuti skema serialisasi cairo.
Penting untuk dicatat bahwa kita memiliki {value: msg.value}
. Faktanya, nilai minimum yang harus kami kirim di sini adalah 20k wei
, karena kontrak StarknetMessaging
akan mendaftarkan
hash pesan kita dalam penyimpanan Ethereum.
Selain dari 20k wei
itu, karena L1HandlerTransaction
yang akan dieksekusi oleh sequencer tidak terikat pada akun mana pun (pesan berasal dari L1), Anda juga harus memastikan
bahwa Anda membayar cukup biaya di L1 agar pesan Anda dapat didekripsi dan diproses di L2.
Biaya dari L1HandlerTransaction
dihitung secara regular seperti halnya untuk transaksi Invoke
. Untuk ini, Anda dapat memperkirakan konsumsi gas menggunakan starkli
atau snforge
untuk mengetahui biaya eksekusi pesan Anda.
Tanda tangan dari sendMessageToL2
adalah:
function sendMessageToL2(
uint256 toAddress,
uint256 selector,
uint256[] calldata payload
) external override returns (bytes32);
Parameter-parameter tersebut adalah sebagai berikut:
toAddress
: Address kontrak di L2 yang akan dipanggil.selector
: Selektor dari fungsi kontrak ini ditoAddress
. Selektor (fungsi) ini harus memiliki atribut#[l1_handler]
agar dapat dipanggil.payload
: Payload selalu berupa arrayfelt252
(yang direpresentasikan olehuint256
dalam Solidity). Untuk alasan ini, kami telah memasukkan inputmyFelt
ke dalam array. Inilah sebabnya mengapa kita perlu memasukkan data input ke dalam sebuah array.
Di sisi Starknet, untuk menerima pesan ini, kita memiliki:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_04_L1_L2_messaging/src/lib.cairo:felt_msg_handler}}
Kita perlu menambahkan atribut #[l1_handler]
ke dalam fungsi kita. Handler L1 adalah fungsi khusus yang hanya dapat dieksekusi oleh L1HandlerTransaction
. Tidak ada yang perlu dilakukan secara khusus untuk menerima transaksi dari L1, karena pesan tersebut secara otomatis diteruskan oleh sequencer. Pada fungsi #[l1_handler]
Anda, penting untuk memverifikasi pengirim pesan L1 untuk memastikan bahwa kontrak kita hanya dapat menerima pesan dari kontrak L1 yang terpercaya.
Mengirim pesan dari Starknet ke Ethereum
Ketika mengirim pesan dari Starknet ke Ethereum, Anda harus menggunakan syscall send_message_to_l1
dalam kontrak Cairo Anda. Syscall ini memungkinkan Anda untuk mengirim pesan ke kontrak StarknetMessaging
di L1. Berbeda dengan pesan L1->L2
, pesan L2->L1
harus dikonsumsi secara manual, yang berarti bahwa Anda perlu kontrak Solidity Anda untuk memanggil fungsi consumeMessageFromL2
dari kontrak StarknetMessaging
secara eksplisit untuk mengonsumsi pesan tersebut.
Untuk mengirim pesan dari L2 ke L1, apa yang akan kita lakukan di Starknet adalah:
{{#include ../listings/ch99-starknet-smart-contracts/listing_99_04_L1_L2_messaging/src/lib.cairo:felt_msg_send}}
Kami hanya membangun payload dan meneruskannya, bersama dengan Address kontrak L1, ke fungsi syscall.
Pada L1, bagian penting adalah untuk membangun payload yang sama seperti pada L2. Kemudian Anda memanggil consumeMessageFromL2
dengan melewati Address kontrak L2 dan payload.
Harap diperhatikan bahwa Address kontrak L2 yang diharapkan oleh consumeMessageFromL2
adalah Address kontrak dari akun yang mengirim transaksi di L2,
dan bukan Address kontrak yang menjalankan send_message_to_l1_syscall
.
function consumeMessageFelt(
uint256 fromAddress,
uint256[] calldata payload
)
external
{
let messageHash = _snMessaging.consumeMessageFromL2(fromAddress, payload);
// Anda dapat menggunakan hash pesan di sini jika diinginkan.
// Kami mengharapkan payload hanya berisi nilai felt252 (yang merupakan uint256 dalam Solidity).
require(payload.length == 1, "Payload tidak valid");
uint256 my_felt = payload[0];
// Dari sini, Anda dapat dengan aman menggunakan `my_felt` karena pesan telah diverifikasi oleh StarknetMessaging.
require(my_felt > 0, "Nilai tidak valid");
}
Seperti yang terlihat, dalam konteks ini, kita tidak perlu memverifikasi kontrak mana dari L2 yang mengirim pesan. Namun, kita sebenarnya menggunakan consumeMessageFromL2
untuk memvalidasi input (Address pengirim di L2 dan payload) untuk memastikan kita hanya mengonsumsi pesan yang valid.
Penting untuk diingat bahwa di L1 kita mengirim payload uint256
, namun tipe data dasar di Starknet adalah felt252
; namun, felt252
sekitar 4 bit lebih kecil dari uint256
. Jadi kita harus memperhatikan nilai yang terkandung dalam payload dari pesan yang kita kirim. Jika, di L1, kita membangun pesan dengan nilai di atas maksimum felt252
, pesan tersebut akan terjebak dan tidak pernah dikonsumsi di L2.
Cairo Serde
Sebelum mengirim pesan antara L1 dan L2, Anda harus ingat bahwa kontrak Starknet, yang ditulis dalam Cairo, hanya dapat memahami data yang telah diserialkan. Dan data yang diserialkan selalu berupa array felt252
.
Pada Solidity, kita memiliki tipe uint256
, dan felt252
sekitar 4 bit lebih kecil dari uint256
. Jadi kita harus memperhatikan nilai-nilai yang terkandung dalam payload pesan yang kita kirim.
Jika, di L1, kita membangun pesan dengan nilai di atas maksimum felt252
, pesan tersebut akan terjebak dan tidak pernah dikonsumsi di L2.
Sebagai contoh, nilai uint256
sebenarnya dalam Cairo direpresentasikan oleh sebuah struktur seperti:
struct u256 {
rendah: u128,
tinggi: u128,
}
yang akan diserialkan sebagai DUA felt, satu untuk rendah
dan satu untuk tinggi
. Ini berarti untuk mengirim hanya satu u256
ke Cairo, Anda harus mengirim payload dari L1 dengan DUA nilai.
uint256[] memory payload = new uint256[](2);
// Mari kirim nilai 1 sebagai u256 di Cairo: rendah = 1, tinggi = 0.
payload[0] = 1;
payload[1] = 0;
Jika Anda ingin mempelajari lebih lanjut tentang mekanisme pengiriman pesan, Anda dapat mengunjungi dokumentasi Starknet.
Anda juga dapat menemukan panduan terperinci di sini untuk menguji pengiriman pesan secara lokal.