# Cairo 基礎介紹
###### tags: `Cairo` `StarkNet`
---
:::warning
(Last edited on 2022.10.14)很抱歉,Cairo 迭代速度太快,這份文件的內容已經過期。請參考官方網站:
- https://www.cairo-lang.org/docs/hello_starknet/index.html#
:::
---
➡ 看完基礎介紹後如果你對 Cairo 還想再跟深入學習的話,你可以透過文中連結的 Hello Cairo 官方教學文件或是我整理出來的一些[重點](https://hackmd.io/2xdTRB1jT62gbHDOG_i5Qg)
**警告:深入學習的水深和這篇基礎介紹的水深差很多**
___
[TOC]
___
## Introduction
STARK 所使用的編程語言
- **"圖靈完備"**
- 近似於低階語言的用法
- 比使用 Solidity 這種高階語言複雜許多
- 甚至比其他 SNARK 的編程語言像是 `circom` 或是 `ZoKrates` 還麻煩
- **"高效率"**
- 能使用的功能和套件庫還很少
- pedersen, ecdsa
- 使用 Cairo 打造的項目: DeversiFi, dydx and immutableX
- 都是基於 Starkware 打造的 StarEx
- 針對 Solidity contracts 有經過多方審計
- 但還沒有看到針對 Cairo 代碼的審計
- STARK Prover 目前是使用 Polaris License 授權
- 可用於非商業使用, 或是
- 使用於商業用途但是產生的 proof 必須要送到 Polaris Verifier 去驗證
- 這是一組由 Starkware 官方維護的 Verifier 的名單
- [SHARP](https://www.cairo-lang.org/docs/sharp.html)(Shared Prover) service
- 目前唯一產生 STARK proof 的方式,Start Prover 還未開源
- 由 Starkware 維護
- 搜集多方執行 Cairo 程序後的結果,合併產生一個 proof
- 平攤、共享 proof 的成本
### Documentations and codes
- 開發者文件
- [Hello Cairo](https://www.cairo-lang.org/docs/hello_cairo/index.html): 用範例來介紹 Cairo
- [How Cairo works](https://www.cairo-lang.org/docs/how_cairo_works/index.html): 深入 Cairo 細節
建議從 Hello Cairo 開始
### Playground
- [Cairo playground](https://cairo-lang.org/playground/)
- 像是 Remix,你可以在上面編寫、編譯 Cairo 程式,甚至產生 proof
- 上面也有一系列的範例和練習題
### Playground <-> local env
你也可以在本地安裝 Cairo 開發環境(下方 [Setting up environment](#Setting-up-environment) 會介紹)
在本地端開發有幾項相比於 playground 的優勢:
- 你可以為程序提供 [input](https://hackmd.io/aqZ708enRfq4mK1YixYhQA#program-input)
- 在 playground 裡,你只能寫死你的程序 input
- 在編譯和執行你的程式時,有更多選擇:
- 在執行程式(`cairo-run`)時,你可以透過設置 `--step` 及 `--no-end` 參數來指定程序從執行到停止的步數
- 這可以幫你檢視程序執行到一半的狀態、找出程序可能在哪一步驟出錯
- 另外你也可以設置 `--tracer` 參數
- 它會在 `localhost:8100` 跑起一個後台,在頁面上還原執行的過程
- playground 也有一樣的功能
___
## Setting up environment
https://www.cairo-lang.org/docs/quickstart.html
### 1. Python
- 官方文件使用 python3.7 及 python virtual env
### 2. Install preliminary packages
- Ubuntu: `sudo apt install -y libgmp3-dev`
- Mac: `brew install gmp`
- python packages: `pip3 install ecdsa fastecdsa sympy`
- 如果是使用 python3.6: `pip3 install contextvars`
### 3. Install Cairo
**撰文當前最新版是 0.2.0 版**
- [download cairo@0.2.0](https://github.com/starkware-libs/cairo-lang/releases/tag/v0.2.0)
- install: `pip3 install cairo-lang-0.2.0.zip`
### 4. Quick example
假設我想要寫一個小程式,證明我知道某個 hash 值的 pre-image:
```python=
%builtins output pedersen
from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.cairo.common.hash import hash2
# Implicit arguments: addresses of the output and pedersen
# builtins.
func main{output_ptr, pedersen_ptr : HashBuiltin*}():
alloc_locals
local preimage
%{ ids.preimage = program_input['preimage'] %}
let (digest) = hash2{hash_ptr=pedersen_ptr}(preimage, 0)
assert [output_ptr] = digest
let output_ptr = output_ptr + 1
return ()
end
```
將這個檔案存為 `test.cairo`
並將只有你知道的 pre-image 值存到 `input.json` 檔,例如:
```json=
{
preimage: 33
}
```
### 5. Compile and run
- 編譯
- `cairo-compile test.cairo --output test_compiled.json`
- 執行
- `cairo-run --program=test_compiled.json --program_input=input.json --print_output --layout=small --tracer`
- 可以到 [http://localhost:8100/](http://localhost:8100/) 去檢視程序執行過程
- `--program_input` 指定 input 檔
- `--print_output` 會印出程序的輸出
- `--layout=small` 會指定要使用哪一組預設功能(但目前也沒有太多預設功能可以選擇
你會看到程式輸出
```
Program output:
10895499158002645496215369097676997787459265175555863332772759280702396009108
```
108 開頭這個數字即是 33 這個值經過 pedersen hash function 得到的輸出
**注意: program output 不只是方便你 debug 用的,它還會影響你程式執行所產生的 proof。怎麼影響?後面會講**
### 6. prove
- 接著產生 proof 的部分就要使用 SHARP 了
- 在本地端和 playground 都可以送請求給 SHARP
- 本地端執行 `cairo-sharp submit --source test.cairo`
- 它會編譯和執行程式,並把程序的 metadata 及 execution trace 傳送給 SHARP
- 你會得到一個 **Job key** 和一個 **Fact**
- **Job key** 用於 SHARP,你可以透過它向 SHARP 查詢 proof 的處理狀態
- **Fact** 就像是你 proof 的 id(像是以太交易的 tx hash)
- 所以如果你的程式代碼一樣,執行時的 program input 也是一樣的話,那你就會**拿到一樣的 Fact**
- SHARP 把 proof 送到鏈上合約並驗證後,合約會在 fact registry 合約去註冊你程序的 Fact
- 這時候你就可以透過[合約](https://ropsten.etherscan.io/address/0x4Cd99A1FC780d874a34008fdC6da2961d540fE64#readProxyContract)去查詢你的 Fact
- `isValid(FACT_OF_YOUR_PROOF)`
___
## 接下來先介紹一些 Cairo 的架構和語法基礎
### ❥ numbers are field elements and they operate in prime field
- `p = 3618502788666131213697322783095070105623107215331596699973092056135872020481`
- `x = 100 (% p)`, `y = 3 (% p)`, `x + y = 100 + 3 (% p)`
- `felt` 表示 field element
- 在執行 **division** 或是 **checking even number** 時要小心
- division
- `x / y = 1206167596222043737899107594365023368541035738443865566657697352045290673527`
- 你可以看到結果不是你平常預期的 `100 / 3 = 33.33...` 或是 `33`
- 它只確保 `1206167596222043737899107594365023368541035738443865566657697352045290673527 * y = x`
- 建議不要直接使用除法運算符號,而是使用提供的數學運算套件
- `from starkware.cairo.common.math import ...`
### ❥ basic components
1. memory cells
- 下圖中每一個 row 都是一個 memory cell
- 第二行由上往下 1, 2, 3, ... 就是 memory cell 的編號(或說地址)
- 第三行是該 memory cell 的內容
- memory cells 是 **read only**, 也就是說如果你對同一個 memory cell 去賦值,會失敗

2. instructions(紅圈處)
- memory cell 開頭會寫入該程式的所有指令(也就是 memory cell 的開頭會保留給你的程式代碼,後面才是留給你程式執行過程中會寫入的值)
3. pointers(藍圈及黃色底線處)有三種
- 在程序執行的過程中,這些 pointer 的值會一直改變
- program counter(**pc**, 黃色底線處)
- 指向當前的 instruction
- application pointer(**ap**)
- 指向一個未使用的 memory cell
- ap 的值會隨著執行一直改變
- frame pointer(**fp**)
- 指向當前的 function frame
- 不同於 ap,在同一個函式執行的期間,fp 的值是固定的
#### Cairo 語法範例
```python=
ap = 123 # NO, YOU CAN NOT DO THIS
[ap] = 123 # YES, YOU CAN DO THIS
# And it assigns 123 to the memory cell ap points to
[ap] = [ap - 1] * [fp]; ap++ # See (a ) below.
[fp - 1] = [ap - 2] + [fp + 4]
[ap - 1] = [fp + 10] * [ap]; ap++
[ap - 1] = [fp + 10] + 12345; ap++ # See (b ) below.
[fp + 2] = [ap + 5]
[fp + 2] = 12345
[ap + 2] = [[ap + 5]] # See (c ) below.
[ap] = [fp - 3] - [ap + 4] # See (d ) below.
[ap] = [fp - 3] / [ap + 4] # See (d ) below.
```
:::spoiler (a )
這個 instruction 聲明等號右邊的兩個 memory cell (`[ap - 1]` and `[fp]`) 的乘積必須等於下一個 un-used memory cell (也就是等號左邊的 `[ap]`).
我們把這看作是:把這兩個值的乘積寫進 `[ap]` 裡。最後面的 `ap++` 指示 cairo 在前面的動作執行完後,把 `ap` 的值加一(也就是指到下一個 un-used memroy cell)
要更改 `ap` 值的方法,除了 `ap++`,就是使用 `ap += ...`
注意:`ap++` **不是一個獨立的指令** - 它和 `;` 符號是一起使用的。`;` 在這不可以當作分開 instruction 的功能 (不像 C++ 等語言的用法)
:::
:::spoiler (b )
在一個 instruction 裡,會出現兩種整數值
1. immediates:
- 像是 `[fp + 2] = 12345` 或是 `[ap - 1] = [fp + 10] + 12345` 裡的 `12345`
2. offsets:
- 像是 `[fp - 1] = [ap - 2] + [fp + 4]` 裡的 `-1`, `-2` 和 `4`
- offsets 的值需在一個範圍內: [-2^15^, 2^15^]
:::
:::spoiler (c )
這是一個 double dereference 的 instruction,取得 `[ap + 5]` 的值並再把它當作某個 memory cell 的地址,再一次去取值
:::
:::spoiler (d )
這些是語法糖 instruction – 它們會被編譯器分別用 `[fp - 3] = [ap] + [ap + 4]` 及 `[fp - 3] = [ap] * [ap + 4]` 取代
:::
### ❥ immutable memory
```python=
func main():
[ap] = 100
[ap] = 101
ret
end
```
你會看到以下 error,因為 memory 只能寫一次,再一次對同一個 `ap` 寫入會噴錯
:::danger
An ASSERT_EQ instruction failed: 100 != 101
:::
#### ⭐️exercise: 算出 𝑥^3^+23𝑥^2^+45𝑥+67 這個多項式在 x=100 時候的值
```python=
func main():
[ap] = 100; ap++
# << Your code here >>
ret
end
```
- 要求:在程序結束時,取值的結果必須在 `ap - 1` 這個 memory cell 裡
- 要求:你的代碼不需依賴於 `x` 的值
:::spoiler Answer:
```python=
func main():
[ap] = 100; ap++ # x
[ap] = [ap - 1] * [ap - 1]; ap++ # x^2
[ap] = [ap - 1] * [ap - 2]; ap++ # x^3
[ap] = [ap - 3] * 45; ap++ # 45x
[ap] = [ap - 3] * 23; ap++ # 23x^2
[ap] = [ap - 3] + [ap - 1]; ap++ # x^3 + 23x^2
[ap] = [ap - 3] + [ap - 1]; ap++ # x^3 + 23x^2 + 45x
[ap] = [ap - 1] + 67; ap++ # x^3 + 23x^2 + 45x + 67
ret
end
```
**你可以看到,要紀錄 ap 的變化是很困難的**
另一方面我們知道 x^3^+23x^2^+45x+67 可以寫成 x(x(x + 23) + 45) + 67
這可以讓我們減少代碼數量:
```python=
func main():
[ap] = 100; ap++ # x
[ap] = [ap - 1] + 23; ap++ # x + 23
[ap] = [ap - 1] * [ap - 2]; ap++ # x(x + 23)
[ap] = [ap - 1] + 45; ap++ # x(x + 23) + 45
[ap] = [ap - 1] * [ap - 4]; ap++ # x(x(x + 23) + 45)
[ap] = [ap - 1] + 67; ap++ # x(x(x + 23) + 45) + 67
ret
end
```
:::
___
## Wrap up
到這邊,你對最基本的 Cairo 架構及語法已經有一些了解了:
- memory cell 只能寫一次,寫完要改變 `ap` pointer 的值,讓它指向下一個沒被寫過的 memory cell
- 要紀錄這些 pointer 很困難,但別擔心,會有工具減輕這個負擔(例如 Reference 的使用),但在這不會去介紹 Reference
然後你也看過證明你知道 pre-image 的簡短例子了,知道怎麼 compile 和產生 proof。這邊要 highlight 的是例子中出現的兩段程式碼:
1.
```
%{ ids.preimage = program_input['preimage'] %}
```
2.
```
assert [output_ptr] = digest
```
第一段程式碼在做的事情是從 `input.json` 裡讀進只有你知道的 `preimage` 值
- `program_input` 是 Cairo 特殊用法,功能是讀進你在 `cairo-run` 透過參數 `program_input=...` 指定的 input json 檔
- `%{` 和 `%}` 這兩個符號組合中間的值是一個叫 hint 的 Cairo 功能,它是給 Prover 的工具(例如讀進 program input),對 Verifier 或其他沒有執行這個 Cairo 程式的人來說,hint 裡的代碼是無意義的
- 在這也不會去更詳細地介紹 hint
第二段代碼在做的是把 `digest` 的值,輸出到 program output。如同上面所說,program output 不只是給你 debug 用的,它還會影響你程式執行所產生的 proof!記得上面提到的 fact 嗎?這個 fact 是你 proof 的 id,它被註冊進 fact registry 代表你的 proof 成功被驗證
- fact 是由程式本身代碼的 hash 值再加上程式所輸出的 program output,組合起來再拿去 hash 所得到的值。
- 大概像這樣:`fact = hash(hash(program) | program outputs...)`
- 所以只要你 program input 不變(或甚至你的程式連 program input 都沒有),program output 就會一樣,而 program ouput 一樣,fact 就會一樣。
- 就像是你 pre-image 給 33,那 program output 一定會是 180 開頭那個數字
- 如果你 pre-image 給 34,那就會得到不一樣的 program output,也就會得到不一樣的 fact
- 那 dapp 要怎麼使用 fact ?
- 首先你要等到 SHARP 把 proof 送到鏈上去驗證,並註冊了你的 fact
- 再來你要去戳 dapp,告訴它你的 program output,讓它用 program output 加上 program 的 hash 值算出 fact,然後去問 fact registry
- 如果它確認這個 fact 真的存在 fact registry,那他就能確認你真的知道 pre-image
- 你可能會疑問它怎麼知道 program 的 hash 值?這裡假設 dapp 已經透過某種方式註冊這個 program 的 hash 值,所以你拿你自己寫的作弊 Cairo program 想要去騙 dapp 是沒辦法的,因為你的 program hash 和 dapp 裡註冊的 program hash 不一樣
- 我知道這個 Cairo program 例子很廢,但我只是想要藉由這個例子讓你知道 dapp 要怎麼整合 Cairo program
- 它和你可能習以為常的 dapp 使用方式很不一樣:
- 用 hint 讀取 program input
- 還要把你要證明的值 output 出來
- 然後你要等 SHARP 上傳 proof、fact 被註冊後,才能戳 dapp
- 最後你要提供 program output,讓 dapp 算出 fact