Skip to content

Commit d35a04a

Browse files
Add method to get L1 message hash (#1205)
* Add method to get L1 message hash * Fix code formatting * Add assert for nonce to fix typecheck error * Add explicit not none check in nonce assert * Add unit tests for hash utils * Add bytes length parameter to encode_uint function --------- Co-authored-by: Tomasz Miśkowicz <tomasz.miskowicz@swmansion.com>
1 parent 6a63c74 commit d35a04a

File tree

5 files changed

+129
-2
lines changed

5 files changed

+129
-2
lines changed

starknet_py/hash/utils.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def _starknet_keccak(data: bytes) -> int:
2525
return int_from_bytes(k.digest()) & MASK_250
2626

2727

28+
def keccak256(data: bytes) -> int:
29+
k = keccak.new(digest_bits=256)
30+
k.update(data)
31+
return int_from_bytes(k.digest())
32+
33+
2834
def pedersen_hash(left: int, right: int) -> int:
2935
"""
3036
One of two hash functions (along with _starknet_keccak) used throughout Starknet.
@@ -70,3 +76,11 @@ def private_to_stark_key(priv_key: int) -> int:
7076
Deduces the public key given a private key.
7177
"""
7278
return cpp_get_public_key(priv_key)
79+
80+
81+
def encode_uint(value: int, bytes_length: int = 32) -> bytes:
82+
return value.to_bytes(bytes_length, byteorder="big")
83+
84+
85+
def encode_uint_list(data: List[int]) -> bytes:
86+
return b"".join(encode_uint(x) for x in data)

starknet_py/hash/utils_test.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@
22
# fmt: off
33
import pytest
44

5-
from starknet_py.hash.utils import compute_hash_on_elements, pedersen_hash
5+
from starknet_py.hash.utils import (
6+
compute_hash_on_elements,
7+
encode_uint,
8+
encode_uint_list,
9+
keccak256,
10+
pedersen_hash,
11+
)
612

713

814
@pytest.mark.parametrize(
@@ -33,3 +39,49 @@ def test_compute_hash_on_elements(data, calculated_hash):
3339
)
3440
def test_pedersen_hash(first, second, hash_):
3541
assert pedersen_hash(first, second) == hash_
42+
43+
44+
@pytest.mark.parametrize(
45+
"value, expected_encoded",
46+
[
47+
(0, b"\x00" * 32),
48+
(1, b"\x00" * 31 + b"\x01"),
49+
(123456789, b"\x00" * 28 + b"\x07\x5b\xcd\x15")
50+
]
51+
)
52+
def test_encode_uint(value, expected_encoded):
53+
assert encode_uint(value) == expected_encoded
54+
55+
56+
@pytest.mark.parametrize(
57+
"value, expected_encoded",
58+
[
59+
([], b""),
60+
([1, 2, 3], b"\x00" * 31 + b"\x01" + b"\x00" * 31 + b"\x02" + b"\x00" * 31 + b"\x03"),
61+
]
62+
)
63+
def test_encode_uint_list(value, expected_encoded):
64+
assert encode_uint_list(value) == expected_encoded
65+
66+
67+
@pytest.mark.parametrize(
68+
"string, expected_hash",
69+
[
70+
("", 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470),
71+
("test", 0x9c22ff5f21f0b81b113e63f7db6da94fedef11b2119b4088b89664fb9a3cb658),
72+
("longer test string", 0x47bed17bfbbc08d6b5a0f603eff1b3e932c37c10b865847a7bc73d55b260f32a)
73+
]
74+
)
75+
def test_keccak256_strings(string, expected_hash):
76+
assert keccak256(string.encode("utf-8")) == expected_hash
77+
78+
79+
@pytest.mark.parametrize(
80+
"value, expected_hash",
81+
[
82+
(4, 0x8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b),
83+
(5, 0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0)
84+
]
85+
)
86+
def test_keccak256_ints(value, expected_hash):
87+
assert keccak256(encode_uint(value)) == expected_hash

starknet_py/net/client_utils.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from typing_extensions import get_args
44

5-
from starknet_py.net.client_models import Hash, Tag
5+
from starknet_py.hash.utils import encode_uint, encode_uint_list
6+
from starknet_py.net.client_models import Hash, L1HandlerTransaction, Tag
67

78

89
def hash_to_felt(value: Hash) -> str:
@@ -17,3 +18,21 @@ def hash_to_felt(value: Hash) -> str:
1718

1819
def is_block_identifier(value: Union[int, Hash, Tag]) -> bool:
1920
return isinstance(value, str) and value in get_args(Tag)
21+
22+
23+
def encode_l1_message(tx: L1HandlerTransaction) -> bytes:
24+
# TODO (#1047): remove this assert once GatewayClient is deprecated and nonce is always required
25+
assert tx.nonce is not None
26+
27+
from_address = tx.calldata[0]
28+
# Pop first element to have in calldata the actual payload
29+
tx.calldata.pop(0)
30+
31+
return (
32+
encode_uint(from_address)
33+
+ encode_uint(tx.contract_address)
34+
+ encode_uint(tx.nonce)
35+
+ encode_uint(tx.entry_point_selector)
36+
+ encode_uint(len(tx.calldata))
37+
+ encode_uint_list(tx.calldata)
38+
)

starknet_py/net/full_node_client.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from marshmallow import EXCLUDE
77

88
from starknet_py.constants import RPC_CONTRACT_ERROR
9+
from starknet_py.hash.utils import keccak256
910
from starknet_py.net.client import Client
1011
from starknet_py.net.client_errors import ClientError
1112
from starknet_py.net.client_models import (
@@ -19,6 +20,7 @@
1920
EstimatedFee,
2021
EventsChunk,
2122
Hash,
23+
L1HandlerTransaction,
2224
PendingBlockStateUpdate,
2325
PendingStarknetBlock,
2426
PendingStarknetBlockWithTxHashes,
@@ -36,6 +38,7 @@
3638
TransactionTrace,
3739
TransactionType,
3840
)
41+
from starknet_py.net.client_utils import encode_l1_message
3942
from starknet_py.net.http_client import RpcHttpClient
4043
from starknet_py.net.models.transaction import (
4144
AccountTransaction,
@@ -322,6 +325,16 @@ async def get_transaction(
322325
raise TransactionNotReceivedError() from ex
323326
return cast(Transaction, TypesOfTransactionsSchema().load(res, unknown=EXCLUDE))
324327

328+
async def get_l1_message_hash(self, tx_hash: Hash) -> Hash:
329+
tx = await self.get_transaction(tx_hash)
330+
if not isinstance(tx, L1HandlerTransaction):
331+
raise TypeError(
332+
f"Transaction {tx_hash} is not a result of L1->L2 interaction."
333+
)
334+
335+
encoded_message = encode_l1_message(tx)
336+
return keccak256(encoded_message)
337+
325338
async def get_transaction_receipt(self, tx_hash: Hash) -> TransactionReceipt:
326339
res = await self._client.call(
327340
method_name="getTransactionReceipt",

starknet_py/tests/e2e/tests_on_networks/client_test.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,35 @@ async def test_get_block(full_node_client_integration):
402402
assert tx.hash is not None
403403

404404

405+
@pytest.mark.skipif(
406+
condition="--client=gateway" in sys.argv,
407+
reason="Method get_l1_message_hash not implemented for Gateway client.",
408+
)
409+
@pytest.mark.asyncio
410+
async def test_get_l1_message_hash(full_node_client_integration):
411+
tx_hash = "0x0060bd50c38082211e6aedb21838fe7402a67216d559d9a4848e6c5e9670c90e"
412+
l1_message_hash = await full_node_client_integration.get_l1_message_hash(tx_hash)
413+
assert (
414+
hex(l1_message_hash)
415+
== "0x140185c79e5a04c7c3fae513001f358beb66653dcee75be38f05bd30adba85dd"
416+
)
417+
418+
419+
@pytest.mark.skipif(
420+
condition="--client=gateway" in sys.argv,
421+
reason="Method get_l1_message_hash not implemented for Gateway client.",
422+
)
423+
@pytest.mark.asyncio
424+
async def test_get_l1_message_hash_raises_on_incorrect_transaction_type(
425+
full_node_client_integration,
426+
):
427+
tx_hash = "0x06d11fa74255c1f86aace54cbf382ab8c89e2b90fb0801f751834ca52bf2a2a2"
428+
with pytest.raises(
429+
TypeError, match=f"Transaction {tx_hash} is not a result of L1->L2 interaction."
430+
):
431+
await full_node_client_integration.get_l1_message_hash(tx_hash)
432+
433+
405434
@pytest.mark.asyncio
406435
async def test_get_public_key(gateway_client_integration):
407436
current_public_key = (

0 commit comments

Comments
 (0)