作者:Shew & Faust,極客web3
摘要
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字節碼部署到以太坊鏈上。
(圖片來源:not-satoshi.com)
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。
(圖片來源:Starknet官網)
之後,如果你要要採用該資產合約裏定義的函數功能,可以通過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,就可以完成業務邏輯升級,不需要把資產狀態遷移到新去處,這種合約升級形式比以太坊的更徹底、更原生。
而以太坊合約要變更業務邏輯,往往就要把業務邏輯“外包”給代理合約,通過變更依賴的代理合約,來實現主合約業務邏輯的變更,但這種方式不夠簡潔,也“不原生”。
(圖片來源:wtf Academy)
在某些場景下,如果舊的以太坊合約被整個棄用,裏面的資產狀態就無法直接遷移到新去處,非常麻煩;而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節點收到了你發起的交易後,會調用你在鏈上账戶中自定義的一系列交易處理邏輯。這樣顯然要更靈活。
而在以太坊的設計中,身份驗證(數字籤名)等邏輯是寫死在節點客戶端代碼裏的,不能原生支持账戶功能的自定義。
(Starknet架構師指明的原生AA方案示意圖,交易驗證和gas費資格驗證都被轉移到鏈上合約去處理,鏈的底層虛擬機可以調用用戶自定義或指定的這些函數)
按照zkSyncEra和Starknet官方人員的說法,這套账戶功能模塊化的思路,借鑑了EIP-4337。但不同的是,zkSync和Starknet從一开始就把账戶類型合並了,統一了交易類型,並且用統一入口接收處理所有交易,而以太坊因爲存在歷史包袱,且基金會希望盡可能避免硬分叉等粗暴的迭代方案,所以支持了EIP-4337這種“曲线救國”的方案,但這樣的效果是,EOA账戶和4337方案各自採用獨立的交易處理流程,顯得別扭而且臃腫,不像原生AA那么靈便。
(圖片來源:ArgentWallet)
但目前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生態做出了重要的探索。
鄭重聲明:本文版權歸原作者所有,轉載文章僅為傳播信息之目的,不構成任何投資建議,如有侵權行為,請第一時間聯絡我們修改或刪除,多謝。