Skip to content

Commit

Permalink
Add L1-L2 communication docs guide
Browse files Browse the repository at this point in the history
  • Loading branch information
kkawula committed Aug 1, 2024
1 parent 5126491 commit 26ae59d
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ Guide
guide/using_existing_contracts
guide/resolving_proxy_contracts
guide/deploying_contracts
guide/mocking_interaction_with_l1
guide/serialization
guide/signing
64 changes: 64 additions & 0 deletions docs/guide/mocking_interaction_with_l1.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
Mocking interaction with L1
===========================

Abstract
--------

In order to test interaction with L1 contracts, devnet client provides a way to mock the L1 interaction.
Before taking a look at the examples, please get faimiliar with the `devnet postman docs <https://0xspaceshard.github.io/starknet-devnet-rs/docs/postman>`_ and messaging mechanism:

- `Writing messaging contracts <https://book.cairo-lang.org/ch16-04-L1-L2-messaging.html>`_
- `Mechanism overview <https://docs.starknet.io/architecture-and-concepts/network-architecture/messaging-mechanism/>`_
- `StarkGate example <https://docs.starknet.io/architecture-and-concepts/network-architecture/messaging-mechanism/>`_

L1 network setup
----------------

First of all you should deploy `messaging contract <https://github.com/0xSpaceShard/starknet-devnet-rs/blob/138120b355c44ae60269167b326d1a267f7af0a8/contracts/l1-l2-messaging/solidity/src/MockStarknetMessaging.sol>`_ on ethereum network.

.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_postman_load.py
:language: python
:dedent: 4


L2 -> L1
--------

Deploying L2 interaction contract
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Interaction with L1 is done by sending a message using `send_message_to_l1_syscall` function.
So in order to test this functionality, you need to deploy a contract that has this functionality.
Example contract: `l1_l2.cairo <https://github.com/0xSpaceShard/starknet-devnet-js/blob/5069ec3397f31a408d3df2734ae40d93b42a0f7f/test/data/l1_l2.cairo>`_

.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_postman_load.py
:language: python
:dedent: 4
:start-after: docs: messaging-contract-start
:end-before: docs: messaging-contract-end


Consuming message
^^^^^^^^^^^^^^^^^

After deploying the contract, you need to flush the messages to the L1 network.
And then you can consume the message on the L1 network.

.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_postman_load.py
:language: python
:dedent: 4
:start-after: docs: flush-1-start
:end-before: docs: flush-1-end

L1 -> L2
--------

Sending mock transactions from L1 to L2 does not require L1 node to be running.

.. codesnippet:: ../../starknet_py/tests/e2e/docs/guide/test_postman_load.py
:language: python
:dedent: 4
:start-after: docs: send-l2-start
:end-before: docs: send-l2-end


18 changes: 18 additions & 0 deletions starknet_py/tests/e2e/client_devnet/fixtures/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,21 @@ async def deploy_string_contract(
contract_name="MyString",
class_hash=f_string_contract_class_hash,
)


@pytest_asyncio.fixture(scope="package", name="l1_l2_contract_class_hash")
async def declare_l1_l2_contract(account) -> int:
contract = load_contract("l1_l2")
class_hash, _ = await declare_cairo1_contract(
account, contract["sierra"], contract["casm"]
)
return class_hash


@pytest_asyncio.fixture(scope="package", name="l1_l2_contract")
async def deploy_l1_l2_contract(account, l1_l2_contract_class_hash) -> Contract:
return await deploy_v1_contract(
account=account,
contract_name="l1_l2",
class_hash=l1_l2_contract_class_hash,
)
83 changes: 83 additions & 0 deletions starknet_py/tests/e2e/docs/guide/test_postman_load.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import pytest

from starknet_py.hash.selector import get_selector_from_name


@pytest.mark.asyncio
async def test_postman_load(devnet_client, l1_l2_contract, account):
# pylint: disable=import-outside-toplevel

eth_account_address = 1390849295786071768276380950238675083608645509734

# docs: start
from starknet_py.devnet_utils.devnet_client import DevnetClient

client = DevnetClient(node_url="http://127.0.0.1:5050")

# docs: end
client: DevnetClient = devnet_client
# docs: start
# Load the messaging contract on ETH network
# e.g. anvil eth devnet https://github.com/foundry-rs/foundry/tree/master/crates/anvil

await client.postman_load(network_url="http://127.0.0.1:8545")
# docs: end

# docs: messaging-contract-start
from starknet_py.contract import Contract

# Address of your contract that is emitting messages
contract_address = "0x12345"

# docs: messaging-contract-end
contract_address = l1_l2_contract.address

# docs: messaging-contract-start
contract = await Contract.from_address(address=contract_address, provider=account)

await contract.functions["increase_balance"].invoke_v1(
user=account.address, amount=100, max_fee=int(1e16)
)

# docs: messaging-contract-end
assert await contract.functions["get_balance"].call(user=account.address) == (100,)

# docs: messaging-contract-start
# Invoking function that is emitting message
await contract.functions["withdraw"].invoke_v1(
user=account.address,
amount=100,
l1_address=eth_account_address,
max_fee=int(1e16),
)
# docs: messaging-contract-end
assert await contract.functions["get_balance"].call(user=account.address) == (0,)

# docs: flush-1-start
# Sending messages from L2 to L1.
flush_response = await client.postman_flush()

message = flush_response.messages_to_l1[0]

message_hash = await client.consume_message_from_l2(
from_address=message.from_address,
to_address=message.to_address,
payload=message.payload,
)
# docs: flush-1-end

# docs: send-l2-start
await client.send_message_to_l2(
l2_contract_address=contract_address,
entry_point_selector=get_selector_from_name("deposit"),
l1_contract_address="0xa000000000000000000000000000000000000001",
payload=[account.address, 420],
nonce="0x0",
paid_fee_on_l1="0xfffffffffff",
)

# Sending messages from L1 to L2.
flush_response = await client.postman_flush()
# docs: send-l2-end

assert await contract.functions["get_balance"].call(user=account.address) == (420,)
110 changes: 110 additions & 0 deletions starknet_py/tests/e2e/mock/contracts_v2/src/l1_l2.cairo
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! L1 L2 messaging demo contract.
//! Rewrite in Cairo 1 of the contract from previous Devnet version:
//! https://github.com/0xSpaceShard/starknet-devnet/blob/e477aa1bbe2348ba92af2a69c32d2eef2579d863/test/contracts/cairo/l1l2.cairo
//!
//! This contract does not use interface to keep the code as simple as possible.
//!

#[starknet::contract]
mod l1_l2 {
const MESSAGE_WITHDRAW: felt252 = 0;

#[storage]
struct Storage {
// Balances (users) -> (amount).
balances: LegacyMap<felt252, felt252>,
}

#[event]
#[derive(Drop, starknet::Event)]
enum Event {
DepositFromL1: DepositFromL1,
}

#[derive(Drop, starknet::Event)]
struct DepositFromL1 {
#[key]
user: felt252,
#[key]
amount: felt252,
}

/// Gets the balance of the `user`.
#[external(v0)]
fn get_balance(self: @ContractState, user: felt252) -> felt252 {
self.balances.read(user)
}

/// Increases the balance of the `user` for the `amount`.
#[external(v0)]
fn increase_balance(ref self: ContractState, user: felt252, amount: felt252) {
let balance = self.balances.read(user);
self.balances.write(user, balance + amount);
}

/// Withdraws the `amount` for the `user` and sends a message to `l1_address` to
/// send the funds.
#[external(v0)]
fn withdraw(ref self: ContractState, user: felt252, amount: felt252, l1_address: felt252) {
assert(amount.is_non_zero(), 'Amount must be positive');

let balance = self.balances.read(user);
assert(balance.is_non_zero(), 'Balance is already 0');

// We need u256 to make comparisons.
let balance_u: u256 = balance.into();
let amount_u: u256 = amount.into();
assert(balance_u >= amount_u, 'Balance will be negative');

let new_balance = balance - amount;

self.balances.write(user, new_balance);

let payload = array![MESSAGE_WITHDRAW, user, amount,];

starknet::send_message_to_l1_syscall(l1_address, payload.span()).unwrap();
}

/// Withdraws the `amount` for the `user` and sends a message to `l1_address` to
/// send the funds.
#[external(v0)]
fn withdraw_from_lib(
ref self: ContractState, user: felt252, amount: felt252, l1_address: felt252, message_sender_class_hash: starknet::ClassHash,
) {
assert(amount.is_non_zero(), 'Amount must be positive');

let balance = self.balances.read(user);
assert(balance.is_non_zero(), 'Balance is already 0');

// We need u256 to make comparisons.
let balance_u: u256 = balance.into();
let amount_u: u256 = amount.into();
assert(balance_u >= amount_u, 'Balance will be negative');

let new_balance = balance - amount;

self.balances.write(user, new_balance);

let calldata = array![user, amount, l1_address];

starknet::SyscallResultTrait::unwrap_syscall(
starknet::library_call_syscall(
message_sender_class_hash,
selector!("send_withdraw_message"),
calldata.span(),
)
);
}

/// Deposits the `amount` for the `user`. Can only be called by the sequencer itself,
/// after having fetched some messages from the L1.
#[l1_handler]
fn deposit(ref self: ContractState, from_address: felt252, user: felt252, amount: felt252) {
// In a real case scenario, here we would assert from_address value

let balance = self.balances.read(user);
self.balances.write(user, balance + amount);

self.emit(DepositFromL1 { user, amount });
}
}
1 change: 1 addition & 0 deletions starknet_py/tests/e2e/mock/contracts_v2/src/lib.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ mod test_contract;
mod test_enum;
mod test_option;
mod token_bridge;
mod l1_l2;

0 comments on commit 26ae59d

Please sign in to comment.