Joel's dev blog

Decoding calldata on Starknet

January 26, 2024

3 min read

This is a brief summary of the anatomy of Starknet’s calldata and how to decode it. The Internet needs more information about how Starknet works.

Basically, Starknet has two versions of execution encoding: legacy and new. You can check starknet-rs:

/// How calldata for the `__execute__` entrypoint is encoded.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ExecutionEncoding {
    /// Encode `__execute__` calldata in Cairo 0 style, where calldata from all calls are concated
    /// and appended at the end.
    Legacy,
    /// Encode `__execute__` calldata in Cairo (1) style, where each call is self-contained.
    New,
}

But unfortunately starknet-rs only supports encoding calldata. This means one needs to work backwards and write a code to decode calldata, which is not super difficult but still needs to be done.

So without any further due, if you just want to use the code (Rust), go to https://github.com/9oelM/decode-starknet-calldata/ and use it right away.

If you wanna learn about the structure of calldata, keep reading.

Legacy format

Let’s take an example of Transaction of hash 0x06b8627ba886d457d32cc5a2ef0cc99741fc67b1142ce3f180a29b817b6f5f33, which is a transaction sent to zkLend, that has 3 calls: namely approve, deposit and enable_collateral.

legacy.png

The legacy format follows this signature: __execute__(call_array_len, call_array, calldata_len, calldata). The order of the calldata follows the order of arguments of __execute__.

So the raw data is:

0	0x3
1	0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
2	0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c
3	0x0
4	0x3
5	0x4c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
6	0xc73f681176fc7b3f9693986fd7b14581e8d540519e27400e88b8713932be01
7	0x3
8	0x2
9	0x4c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
10	0x271680756697a04d1447ad4c21d53bdf15966bdc5b78bd52d4fc2153aa76bda
11	0x5
12	0x1
13	0x6
14	0x4c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
15	0x738a4f05910
16	0x0
17	0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
18	0x738a4f05910
19	0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7

and this is what you get from it:

legacy_structured.png

Observe that the real calldata is always appended at the last part.

New format

The new format is relatively easier than the legacy format. Each call is just self-contained, and are stored in an array of calls.

Let’s take an example of the same calls (approve, deposit, and enable_collateral) at a different transaction of hash 0x06a031ca9916acc3a3723f2f15c2a0c32e756c887b33271d914b1309f57be0f0:

new.png

This is what you get as raw data:

0	0x3
1	0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
2	0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c
3	0x3
4	0x4c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
5	0x1dccd9ffaff50
6	0x0
7	0x4c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
8	0xc73f681176fc7b3f9693986fd7b14581e8d540519e27400e88b8713932be01
9	0x2
10	0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7
11	0x1dccd9ffaff50
12	0x4c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
13	0x271680756697a04d1447ad4c21d53bdf15966bdc5b78bd52d4fc2153aa76bda
14	0x1
15	0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7

And these are the corresopnding calls:

new_structured.png

The raw data is in the same order as the values in the array in the picture above. The only difference is that there are numbers in between to indicate the length of the forthcoming call.

For example, 0x3 is the field element in the data. The first field element in the raw data must always be the length of all calls, which is 0x3 = 3.

Then 0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7 and 0x219209e083275171774dab1df80982e9df2096516f06319c5c6d71ae0a8480c are just to and selector of the first call. Then we have a calldata of the first call. But before that, we have 0x3, which again indicates that the forthcoming calldata is an array of length 3.

Inside the calldata of the first array are:

4	0x4c0a5193d58f74fbace4b74dcf65481e734ed1714121bdc571da345540efa05
5	0x1dccd9ffaff50
6	0x0

Then, we jump to to of the second call, and the rest works the same.

That’s it! Thanks.


Written by Joel Mun. Joel likes Typescript, React, Node.js, GoLang, Python, Wasm and more. He also loves to enlarge the boundaries of his knowledge, mainly by reading books and watching lectures on Youtube. Guitar and piano are necessities at his home.

© Joel Mun 2024