作者:Shew & Faust,極客web3
顧問:CryptoNerdCn,Starknet生態核心開發者,瀏覽器端Cairo開發平台WASM Cairo創辦人
摘要:
- Starknet最主要的幾大技術特性,包括利於ZK證明產生的Cairo語言、原生層級的AA 、業務邏輯與狀態儲存相獨立的智慧合約模型。
- Cairo是一種通用的ZK語言,既可以在Starknet上實現智能合約,也可以用於開發偏傳統的應用,其編譯流程中引入Sierra作為中間語言,使得Cairo可以頻繁迭代,但又不必變更最底層的字節碼,只需要把變化傳導至中間語言身上;在Cairo的標準庫內,也納入了帳戶抽象所需的許多基本資料結構。
- Starknet智能合約將業務邏輯與狀態資料分開來存儲,不同於EVM鏈,Cairo合約部署包含「編譯、聲明、部署」三階段,業務邏輯被聲明在Contract class中,包含狀態資料的Contract實例可以與class建立關聯,並呼叫後者所包含的程式碼;
- Starknet的上述智慧合約模型利於程式碼重複使用、合約狀態重複使用、儲存分層、偵測垃圾合約,也利於儲存租賃制和交易並行化的實作。雖然後兩者目前暫未落地,但Cairo智能合約的架構,還是為其創造了「必要條件」。
- Starknet鏈上只有智慧合約帳戶,沒有EOA帳戶,從一開始就支援原生層級的AA帳戶抽象化。其AA方案一定程度吸收了ERC-4337的思路,讓用戶選擇高度客製化的交易處理方案。為了防止潛在的攻擊場景,Starknet做出了許多反制措施,為AA生態做出了重要的探索。
正文:繼Starknet發行代幣之後,STRK逐漸成為以太坊觀察者眼中不可或缺的要素之一。這個向來以「特立獨行」「不重視使用者體驗」而聞名的以太坊Layer2明星,就像一個與世無爭的隱士,在EVM兼容大行其道的Layer2生態裡默默的開闢自己的一畝三分地。
由於太過忽視用戶,甚至公開在Discord開設「電子乞丐」頻道,Starknet一度遭到擼毛黨的抨擊,在遭噴「不近人情」的同時,技術上的深厚造詣瞬間變得「一文不值”,似乎只有UX和造富效應才是一切。 《金閣寺》中那句話“不被人理解成了我唯一的自豪”,簡直就是Starknet的自我寫照。
但拋開這些江湖瑣事,單純從程式碼極客們的「技術品味」出發,作為ZK Rollup先驅之一的Starknet和StarkEx,幾乎就是Cairo愛好者眼中的瑰寶,在某些全鏈遊戲開發者心中, Starknet和Cairo簡直就是web3的一切,無論是Solidity或Move都無法與之相提並論。現如今橫亙在「科技極客」與「使用者」之間的最大代溝,其實更歸因於人們對Starknet的認知欠缺。
抱著對區塊鏈技術的興趣與探索欲,以及對Starknet的價值發現,本文作者從Starknet的智能合約模型與原生AA出發,為大家簡單梳理其技術方案與機制設計,在為更多人展示Starknet技術特性的同時,也希望讓人們了解這個「不被人所理解的獨行俠」。
Cairo語言極簡科普
下文中我們將重點討論Starknet的智慧合約模型與原生帳號抽象,說明Starknet是如何實現原生AA的。讀完此文,大家也可以理解為什麼Starknet中不同錢包的助記詞不能混用。
但在介紹原生帳號抽象之前,讓我們先了解下Starknet獨創的Cairo語言。在Cairo的發展歷程中,出現了名為Cairo0的早期版本,以及後來的現代版本。 Cairo的現代版本整體語法類似於Rust,實際上是一門通用的ZK語言,除了可以在Starknet上編寫智能合約,也可以用於通用應用的開發。
例如我們可以用Cairo語言開發ZK驗證系統,這段程式可以在自己搭建的伺服器上運行,不必依賴StarkNet網路。可以說,任何需要可驗證計算屬性的程式都可以用Cairo語言來實作。而Cairo可能是目前最利於生成ZK證明的程式語言。
從編譯流程來看,Cairo使用了基於中間語言的編譯方法,如下圖所示。圖中的Sierra是Cairo語言編譯過程中的一道中間形態(IR),而Sierra會再被編譯為更底層的二進位程式碼形式,名為CASM,在Starknet節點設備上直接運作。
引入Sierra作為中間形態,方便Cairo語言增加新特性,許多時候只要在Sierra這道中間語言上做手腳,不必直接變更底層的CASM程式碼,這就省去了很多麻煩事, Starknet的節點客戶端就不必頻繁更新。這樣就可以在不變更StarkNet底層邏輯的情況下,實作Cairo語言的頻繁迭代。而在Cairo的標準函式庫內,也納入了帳戶抽象化所需的許多基本資料結構。
Cairo的其他創新,包括一種被稱為Cairo Native的理論方案,該方案計劃將Cairo編譯為能適配不同硬體設備的底層機器代碼,Starknet節點在運行智能合約時,將不必依賴CairoVM虛擬機,這樣可以大幅提升程式碼執行速度【目前還處於理論階段,未落地】。
Starknet智能合約模型:程式碼邏輯與狀態儲存的剝離
與EVM相容鏈不同,Starknet在智慧合約系統的設計上,有著突破性的創新,這些創新很大程度上是為原生AA以及未來上線的平行交易功能準備的。在這裡,我們要知道,以太坊等傳統公鏈上,智能合約的部署往往遵循「編譯後部署」的方式,以ETH智能合約舉例:
1.開發者在本地編寫好智能合約後,透過編輯器將Solidity程式編譯為EVM的字節碼,這樣就可以被EVM直接理解並處理;
2.開發者發起一筆部署智能合約的交易請求,把編譯好的EVM字節碼部署到以太坊鏈上。
Starknet的智能合約雖然也遵循「先編譯後部署」的思路,智能合約以CairoVM支援的CASM字節碼形式部署在鏈上,但在智能合約的呼叫方式與狀態存儲模式上,Starknet與EVM相容鏈有著巨大差異。
準確的說,以太坊智能合約=業務邏輯+狀態信息,比如USDT的合約中不光實現了Transfer、Approval等常用的函數功能,還存放著所有USDT持有者的資產狀態,代碼和狀態被耦合在了一起,這帶來了許多麻煩,首先不利於DAPP合約升級與狀態遷移,也不利於交易的並行處理,是一種沉重的技術包袱。
對此,Starknet對狀態的儲存方式進行了改良,在其智能合約實現方案中,DAPP的業務邏輯與資產狀態完全解耦,分別存放在不同地方,這樣做的好處很明顯,首先可以讓系統更快速的分辨出,是否有重複或多餘的程式碼部署。這裡的原理是這樣:
以太坊的智能合約=業務邏輯+狀態數據,假如有幾個合約的業務邏輯部分完全一致,但狀態數據不同,則這幾個合約的hash也不同,此時系統難以分辨出這些合約是否冗餘,是否有「垃圾合約」存在。
而在Starknet的方案中,程式碼部分和狀態資料直接分開,系統根據程式碼部分的hash,更容易分辨出是否有相同的程式碼被多次部署,因為他們的hash是相同的。這樣方便制止重複的程式碼部署行為,節省Starknet節點的儲存空間。
在Starknet的智慧合約系統中,合約的部署與使用,分為「編譯、聲明、部署」三個階段。資產發行者如果要部署Cairo合約,第一步要在自己的設備本地,把寫好的Cairo程式碼,編譯為Sierra 以及底層字節碼CASM形式。
然後,合約部署者要發布聲明「declare」交易,把合約的CASM 字節碼和Sierra 中間代碼部署到鏈上,名為Contract Class。
之後,如果你要採用該資產合約裡定義的函數功能,可以透過DAPP前端發起「deploy"交易,部署一個和Contract Class相關聯的Contract實例,這個實例裡面會存放資產狀態。之後,使用者可以呼叫Contract Class裡的函數功能,變更Contract實例的狀態。
其實,但凡了解物件導向程式設計的人,都應該能很容易的理解Starknet這裡的Class和Instance各自代表啥。開發者聲明的Contract Class,只包含智能合約的業務邏輯,是一段誰都可以調用的函數功能,但沒有實際的資產狀態,也就沒有直接實現“資產實體”,只有“靈魂”沒有“肉體” 。
而當使用者部署具體的Contract實例後,資產就完成了「實體化」。如果你要對資產「實體」的狀態進行變更,例如把自己的token轉移給別人,可以直接呼叫Contract Class裡寫好的函數功能。上述過程就和傳統物件導向程式語言裡的「實例化」有些類似(但不完全一致)。
智慧合約被分離為Class和實例後,業務邏輯與狀態資料解耦合,為Starknet帶來了以下特性:
1.利於儲存分層及「儲存租賃制」的實現
所謂的儲存分層,就是開發者可以依照自己的需求,將資料放在自訂的位置,例如Starknet鏈下。 StarkNet準備相容於Celestia等DA層,DAPP開發者可以將資料存放在這些第三方DA層。例如一個遊戲可以將最重要的資產資料存放在Starknet主網上,而將其他資料儲存在Celestia等鏈下DA層。這種依照安全需求客製化選擇DA層的方案,被Starknet命名為"Volition"。
而所謂的儲存租賃制,是指每個人要持續的為自己佔用的儲存空間付費。你佔用的鏈上空間有多少,理論上就該持續的支付租金。
在以太坊智能合約模型中,合約的所有權不明確,難以分辨出一個ERC-20合約應該由部署者還是資產持有者支付“租金”,遲遲沒有上線存儲租賃功能,只在合約部署時向部署者收取一筆費用,這種儲存費用模型並不合理。
而在Starknet和Sui以及CKB、Solana的智能合約模型下,智能合約的所有權劃分更明確,便於收取儲存資金【目前Starknet沒有直接上線儲存租賃制,但未來會實現】
2.實現真正的程式碼復用,減少垃圾合約的部署
我們可以宣告一個通用的代幣合約是作為class儲存到鏈上,然後所有人都可以呼叫這個class裡的函數,來部署屬於自己的代幣實例。而且合約也可以直接呼叫class內的程式碼,這就實現了類似Solidity中的Library函式庫的效果。
同時,Starknet的這種智慧合約模型,有助於分辨「垃圾合約」。前面對此有所解釋。在支援程式碼重複使用與垃圾合約偵測後,Starknet可以大幅減少上鍊的資料量,盡可能減輕節點的儲存壓力。
3.真正的合約「狀態」復用
區塊鏈上的合約升級主要涉及業務邏輯的變更,在Starknet的場景下,智慧合約的業務邏輯與資產狀態天生就是分離的,合約實例變更了關聯的合約類型class,就可以完成業務邏輯升級,不需要把資產狀態遷移到新去處,這種合約升級形式比以太坊的更徹底、更原生。
而以太坊合約要變更業務邏輯,往往就要把業務邏輯“外包”給代理合約,透過變更依賴的代理合約,來實現主合約業務邏輯的變更,但這種方式不夠簡潔,也“不原生” 。
在某些場景下,如果舊的以太坊合約被整個棄用,裡面的資產狀態就無法直接遷移到新去處,非常麻煩;而Cairo合約就不需要把狀態遷移走,可以直接「復用」舊的狀態。
4.利於交易並行化處理
要盡可能提升不同交易指令的可並行度,必要一環是把不同人的資產狀態分散開存儲,這在比特幣、CKB和Sui身上可見一斑。而上述目標的先決條件,就是把智能合約的業務邏輯和資產狀態資料剝離開。雖然Starknet還沒有針對交易並行進行深度的技術實現,但未來將把平行交易作為一個重要目標。
Starknet的原生AA與帳戶合約部署
其實,所謂的帳戶抽象與AA,是以太坊社群發明出來的獨特概念,在許多新公鏈中,並沒有EOA帳戶和智慧合約帳戶的分野,從一開始就避開了以太坊式帳戶體系的坑。例如在以太坊的設定下,EOA帳戶控制者必須在鏈上有ETH才能發起交易,沒有辦法直接選用多樣性的身份驗證方式,要添加一些客製化的支付邏輯也極為麻煩。甚至有人認為,以太坊的這種帳戶設計簡直就是反人類的。
如果我們去觀察Starknet或zkSyncEra等主打「原生AA」的鏈,可以觀察到明顯的不同:首先,Starknet和zkSyncEra統一了帳戶類型,鏈上只有智能合約帳戶,從一開始就沒有EOA帳戶這種東西(zkSync Era會在使用者新建立的帳戶上,預設部署一套合約程式碼,模擬出以太坊EOA帳戶的特徵,這樣就便於相容於Metamask)。
而Starknet沒有考慮直接相容Metamask等以太坊週邊設施,用戶在初次使用Starknet錢包時,會自動部署專用的合約帳戶,說白了就是部署前面提到的合約實例,這個合約實例會和錢包項目方事先部署的合約class相關聯,可以直接呼叫class裡面寫好的一些功能。
下面我們將談及一個有趣的話題:在領取STRK空投時,很多人發現Argent與Braavos錢包彼此不能兼容,將Argent的助記詞導入Braavos後,無法導出對應的賬戶,這其實是因為Argent和Braavos採用了不同的帳戶產生計算方式,導致相同助記詞產生的帳戶位址不同。
具體而言,在Starknet中,新部署的合約位址可以透過確定性的演算法得出,具體使用以下公式:
上述公式中的pedersen(),是一種易於在ZK系統中使用的雜湊演算法,產生帳戶的過程,其實就是給pedersen函數輸入幾個特殊參數,產生對應的hash,這個hash就是產生的帳戶位址。
上面的圖片中顯示了Starknet產生「新的合約位址」時所用到的幾個參數,deployer_address代表「合約部署者」的位址,這個參數可以為空,即便你事先沒有Starknet合約帳戶,也可以部署新的合約。
salt為計算合約地址的鹽值,簡單來說,就是一個隨機數,該變數實際上是為了避免合約地址重複引入的。 class_hash就是前面介紹過的,合約實例對應的class的雜湊值。而constructor_calldata_hash,代表合約初始化參數的哈希。
基於上述公式,使用者可以在合約部署至鏈上之前,就預先計算出生成的合約地址。 Starknet允許用戶在事先沒有Starknet帳戶的情況下,直接部署合約,流程如下:
1. 使用者先確定自己要部署的合約實例,要關聯哪個合約class,把該class的hash當作初始化參數之一,並算出salt,得知自己產生的合約位址;
2. 使用者知道自己將會把合約部署在哪後,先向該地址轉入一定量的ETH,作為合約部署費用。一般來說,這部分ETH要透過跨鏈橋從L1跨到Starknet網路;
3. 用戶發起合約部署的交易請求。
其實,所有的Starknet帳戶都是透過上述流程部署的,但大部分錢包屏蔽了這裡面的細節,用戶根本感知不到裡面的過程,就好像自己轉入ETH後合約帳戶就部署完了。
上述方案帶來了一些相容性問題,因為不同的錢包在產生帳戶位址時,產生的結果並不一致,只有滿足以下條件的錢包才可以混用:
- 錢包使用的私鑰派生公鑰與簽署演算法相同;
- 錢包的salt計算流程相同;
- 錢包的智能合約class在實作細節上沒有根本性不同;
在先前談到的案例中,Argent與Braavos都使用了ECDSA簽章演算法,但雙方的salt計算方法不同,相同的助記詞在兩款錢包中產生的帳戶位址會不一致。
我們再回到帳戶抽象的話題。 Starknet和zkSync Era把交易處理流程中涉及的一系列流程,如身份驗證(驗證數位簽章)、Gas費支付等核心邏輯,全部挪到「鏈底層」之外去實現。使用者可以在自己的帳戶中,自訂上述邏輯的實作細節.
例如你可以在自己的Starknet智慧合約帳戶裡,部署專用的數位簽章驗證函數,當Starknet節點收到了你發起的交易後,就會呼叫你在鏈上帳戶中自訂的一系列交易處理邏輯。這樣顯然要更靈活。
而在以太坊的設計中,身份驗證(數位簽章)等邏輯是寫死在節點客戶端程式碼裡的,不能原生支援帳戶功能的自訂。
根據zkSyncEra和Starknet官方人員的說法,這套帳戶功能模組化的思路,借鑒了EIP-4337。但不同的是,zkSync和Starknet從一開始就把帳戶類型合併了,統一了交易類型,並且用統一入口接收處理所有交易,而以太坊因為存在歷史包袱,且基金會希望盡可能避免硬分叉等粗暴的迭代方案,所以支持了EIP-4337這種「曲線救國」的方案,但這樣的效果是,EOA帳戶和4337方案各自採用獨立的交易處理流程,顯得彆扭而且臃腫,不像原生AA那麼靈便。
但目前Starknet的原生帳戶抽象還沒有達到完全的成熟,從實踐進度來看,Starknet的AA帳戶實現了簽名驗證演算法的自定義,但對於手續費支付的自定義,目前Starknet實際上僅支援ETH和STRK繳交gas費,還沒有支援第三方代繳gas。所以Starknet在原生AA上的進度,可以說是「理論方案基本上已經成熟,實踐方案還在推進」。
由於Starknet內只有智慧合約帳戶,所以其交易的整個流程都考慮了帳戶智能合約的影響。首先,一筆交易被Starknet節點的記憶體池(Mempool)接收後,要進行校驗,驗證步驟包括:
- 交易的數位簽章是否正確,此時會呼叫交易發起者帳戶中,自訂的驗簽函數;
- 交易發起人的帳戶餘額能否支付得起gas費;
這裡要注意,使用帳戶智能合約中自訂的簽章驗證函數,就表示有攻擊場景。因為記憶體池在對新來的交易進行簽名驗證時,不會收取gas費(如果直接收取gas費,會帶來更嚴重的攻擊場景)。惡意使用者可以先在自己的帳戶合約中自訂超級複雜的驗簽函數,再發起大量交易,讓這些交易被驗簽時,都去調用自訂的複雜驗簽函數,這樣可以直接耗盡節點的計算資源。
為了避免這種情況的發生,StarkNet對交易進行了以下限制:
- 單一使用者在單位時間內,可發起的交易筆數有上限;
- Starknet帳戶合約中自訂的簽章驗證函數,有複雜度上的限制,過於複雜的驗簽函數不會被執行。 Starknet限制了驗簽函數的gas消耗上限,如果驗簽函數消耗的gas量過高,則直接拒絕此交易。同時,也不允許帳戶合約內的驗簽函數呼叫其他合約。
Starknet交易的流程圖如下:
值得注意的是,為了進一步加速交易校驗流程,Starknet節點客戶端中直接實作了Braavos和Argent錢包的簽章驗證演算法,節點發現交易產生自這兩大主流Starknet錢包時,會呼叫客戶端裡自帶的Braavos/Argent簽章演算法,透過這種類似快取的思想,Starknet可以縮短交易驗證時間。
交易資料再通過排序器的驗證後(排序器的驗證步驟比記憶體池驗證會深入很多),排序器會將來自記憶體池的交易打包處理,並遞交給ZK證明產生者。進入此環節的交易即使失敗,也會被收取gas。
但如果讀者了解Starknet的歷史,會發現早期的Starknet對執行失敗的交易不收取手續費,最常見的交易失敗情況是,用戶僅有1ETH 的資金,但是對外轉出10ETH,這種交易顯然有邏輯錯誤,最終必然失敗,但在具體執行前誰也不知道結果是啥。
但StarkNet過去不會對這種失敗交易收取手續費。這種無成本的錯誤交易會浪費Starknet節點的運算資源,會衍生出ddos攻擊場景。表面上看,對錯誤交易收取手續費似乎很好實現,實際上卻相當複雜。 Starknet推出新版的Cairo1語言,很大程度就是為了解決失敗交易的gas收取問題。
我們都知道,ZK Proof是一種有效性證明,而執行失敗的交易,其結果是無效的,無法在鏈上留下輸出結果。試著用有效性證明,來證明某條指令執行無效,不能產生輸出結果,聽起來就相當奇怪,實際上也不可行。所以過去的Starknet在產生證明時,直接把不能產生輸出結果的失敗交易都刨除了出去。
Starknet團隊後來採用了更聰明的解決方案,建立了一門新的合約語言Cairo1,使得「所有交易指令都能產生輸出結果並onchain」。乍一看,所有交易都能產生輸出,就表示從不出現邏輯錯誤,而大多數時候交易失敗,是因為遇到一些bug,導致指令執行中斷了。
讓交易永不中斷並成功產生輸出,很難實現,但實際上有一個很簡單的替代方案,就是在交易遇到邏輯錯誤導致中斷時,也讓他產生輸出結果,只不過這時候會回傳一個False值,讓大家知道這筆交易的執行不順利。
但要注意,回傳False值,也就回傳了輸出結果,也就是說, Cairo1裡面,不管指令有沒有遇到邏輯錯誤,有沒有暫時中斷,都能夠產生輸出結果並onchain。這個輸出結果可以是正確的,也可以是False報錯訊息。
For Example,假如存在以下程式碼段
這裡的_balances::read(from) - amount可能因為向下溢出而報錯,這個時候就會導致相應的交易指令中斷並停止執行,不會在鏈上留下交易結果;而如果將其改寫為以下形式,在交易失敗時仍然返回一個輸出結果,留存在鏈上,單純從觀感上來看,這就好像所有的交易都能順利的在鏈上留下交易輸出,統一收取手續費就顯得特別合理。
StarknetAA合約概述
考慮到本文有部分讀者可能存在程式設計背景,所以此處簡單展示了一下Starknet中的帳戶抽象合約的介面:
上述介面中的__validate_declare__,用於使用者發起的declare交易的驗證,而__validate__則用於一般交易的驗證,主要驗證使用者的簽章是否正確,而__execute__則用於交易的執行。我們可以看到Starknet合約帳戶預設支援multicall即多重呼叫。多重呼叫可以實現一些很有趣的功能,例如在進行某些DeFi互動時打包以下三筆交易:
- 第一筆交易將代幣授權給DeFi合約
- 第二筆交易觸發DeFi合約邏輯
- 第三筆交易清空DeFi合約的授權
當然,由於多重呼叫是具有原子性的,所以存在一些更複雜的用法,例如執行某些套利交易。
總結
- Starknet最主要的幾大技術特性,包括利於ZK證明產生的Cairo語言、原生層級的AA、業務邏輯與狀態儲存相獨立的智慧合約模型。
- Cairo是一種通用的ZK語言,既可以在Starknet上實現智能合約,也可以用於開發偏傳統的應用,其編譯流程中引入Sierra作為中間語言,使得Cairo可以頻繁迭代,但又不必變更最底層的字節碼,只需要把變化傳導至中間語言身上;在Cairo的標準庫內,也納入了帳戶抽象所需的許多基本資料結構。
- Starknet智能合約將業務邏輯與狀態資料分開來存儲,不同於EVM鏈,Cairo合約部署包含「編譯、聲明、部署」三階段,業務邏輯被聲明在Contract class中,包含狀態資料的Contract實例可以與class建立關聯,並呼叫後者包含的程式碼;
- Starknet的上述智慧合約模型利於程式碼重複使用、合約狀態重複使用、儲存分層、偵測垃圾合約,也有利於儲存租賃制和交易並行化的實作。雖然後兩者目前暫未落地,但Cairo智能合約的架構,還是為其創造了「必要條件」。
- Starknet鏈上只有智慧合約帳戶,沒有EOA帳戶,從一開始就支援原生層級的AA帳戶抽象化。其AA方案一定程度吸收了ERC-4337的思路,讓用戶選擇高度客製化的交易處理方案。為了防止潛在的攻擊場景,Starknet做出了許多反制措施,為AA生態做出了重要的探索。