一文了解 Aleo 智能合約特點與安全

封面: Aleo

9 月 15 日,可程式隱私網路 Aleo 主線上線。作為第一個將零知識證明 (ZKP) 技術深度融入 Layer 1 的明星項目,其核心特點是採用零知識證明(ZKP)技術,使用戶能夠在不暴露具體數據的前提下進行交易和互動。 Aleo 也引進了創新的混合架構,將鏈上儲存與鏈下運算結合起來,提升了網路的擴展性和處理效率。所有交易的計算過程在鏈下進行,鏈上只儲存必要的證明數據,這大幅減少了區塊鏈的負擔,並優化了效能。

本文旨在為 Aleo 的合約開發人員和愛好者提供一份關於 Aleo 智能合約(Aleo 中也稱為程序)安全實踐與審計要點,幫助開發人員構建強大且安全的鏈上項目。

 Leo 語言特性

Leo 是 Aleo 區塊鏈設計的程式語言,專注於隱私保護應用的開發。它內建零知識證明(ZKP)功能,讓開發者可以建立能夠保護資料隱私的智慧合約,適用於需要隱私保障的去中心化應用程式(dApps)。透過使用零知識證明,Leo 支援驗證計算的正確性,而無需公開底層資料。

Leo 是一種靜態型別語言,它的語法結構類似 Rust ,主要特性如下:

隱私性:Leo 設計的一個核心目標是支援隱私保護,透過零知識證明(ZKP)技術,開發者可以創建能夠驗證交易而不洩露敏感資訊的智慧合約。這使得用戶在進行交易時,能夠保護其身分和資料隱私。

安全性:Leo 內建了多種安全特性,幫助開發者避免常見的智慧合約漏洞,例如重入攻擊和溢位錯誤等。它的編譯器會在合約編譯時捕捉潛在的錯誤,從而提高合約的安全性。

簡潔性:Leo 合約的程式碼通常比較簡潔,這是因為它可利用零知識證明將計算負載轉移到鏈下,只有最終結果(例如,計算的雜湊或證明)會被上傳到鏈上。

Record

Record 是 Aleo 中的一種核心資料結構,用於記錄使用者資產和程式狀態,其可見性可以是 constant、public 或 private,預設情況是 private,表示該 record 的內容是私密的,只有擁有者或授權方才能查看和訪問。 Record 的建立和使用與比特幣的 UTXO 類似,但需要呼叫 Transition 函數來建立和使用 record,當程式建立 Record 時,該 Record 隨後可由相同程式作為函數的輸入使用。一旦將記錄用作輸入,即視為已花費,不能再次使用。

在下面的程式碼中,定義了一個包含代幣所有者和數量的 token record。 mint_private 函數創建一個新的 token record,而 transfer_private 函數將以一個 token record 作為數輸入被消耗掉,同時創建兩個新的 token record,分別記錄代幣發送和接收者的代幣記錄。

record token {        // The token owner.        owner: address,        // The token amount.        amount: u64,}
// The function `mint_private` initializes a new record with the specified amount of tokens for the receiver.transition mint_private(receiver: address, amount: u64) -> token {        return token {            owner: receiver,            amount: amount,        };}
// The function `transfer_private` sends the specified token amount to the token receiver from the specified token record.transition transfer_private(sender: token, receiver: address, amount: u64) -> (token, token) {        let difference: u64 = sender.amount - amount;        // Produce a token record with the change amount for the sender.        let remaining: token = token {            owner: sender.owner,            amount: difference,        };        // Produce a token record for the specified receiver.        let transferred: token = token {            owner: receiver,            amount: amount,        };        // Output the sender's change record and the receiver's record.        return (remaining, transferred);}

在 Aleo 上,一筆交易可以包含多個 Transition(最多 32 個),每個 Transition 都可以創建和花費 record。

資料來源:https://developer.aleo.org/concepts/beginner/records

  氣體

在 Aleo 中,部署和調用合約同樣會消耗 Gas,但與以太坊等傳統區塊鏈的 Gas 計算方式不同,由於 Aleo 依賴於零知識證明(ZKP)技術,其 Gas 費用的計算更多地考慮了計算複雜度、證明產生和驗證的開銷,而不僅僅是合約執行時的指令消耗。所以對於 Leo 合約的 Gas 消耗計算,需要轉換為零知識電路,電路規模越大,Gas 費用越高。每個操作(如加法、乘法、雜湊函數等)都會增加電路的閘數,導致更多的 Gas 消耗。特別是涉及加密運算(如雜湊、簽章驗證)時,電路閘數顯著增加。

因此,Leo 合約的 Gas 費用更著重於計算複雜性和證明效率,開發者需要優化合約邏輯以降低電路複雜度,從而減少 Gas 費用。

 Aleo 智能合約安全注意事項

Aleo 提供了強大的原語,使開發者能夠輕鬆建立私人應用程式。儘管如此,不謹慎的編碼可能導致不直觀的行為和安全漏洞。因此,了解 Aleo 程序的編碼模式及其潛在風險至關重要。在本節中,我們將探討 Aleo 智能合約的編碼模式和常見的安全隱患,以幫助開發者提升應用程式的安全性。

 1. Record 模型的呼叫者檢查

Aleo 的 record 模型類似於 UTXO 模型,但是 record 的所有權透過 owner 欄位標識。因此,在與 record 相關的函數邏輯中,一般無需驗證呼叫者身份,因為鏈層會自動校驗發起交易的人是否為 record 的實際所有者。以下為 join 函數的程式碼範例,該函數用於將 owner 持有的兩個 record 合併為一個,此處無需驗證它們的擁有者是否一致。

以下是鏈上 credits.aleo 程式中 join 函數的 Aleo 電路範例,同樣也不需要校驗兩個 record 的 owner 是否一致。

record credits:    owner as address.private;    microcredits as u64.private;function join:    input r0 as credits.record;    input r1 as credits.record;    add r0.microcredits r1.microcredits into r2;    cast r0.owner r2 into r3 as credits.record;    output r3 as credits.record;

 2. Aleo 中的遞迴調用

對於 Ethereum 上的智慧合約,限定了呼叫堆疊深度最大為 1024,但一般合約很少達到此限制。這主要是因為 EVM 的執行主要受 Gas 費用的限制,每次交易執行都需支付一定的 Gas,以控制計算資源的消耗。當 Gas 消耗完畢,交易將被中止。因此,遞歸呼叫在 Solidity 中會顯著增加 Gas 的消耗,一旦超出交易設定的 Gas 上限,遞歸呼叫就會失敗。

而 Aleo 使用零知識證明(ZK-SNARKs)來驗證交易和程式執行的正確性,因此其程式執行會被編譯為零知識電路。每個程式執行都需在電路中定義明確的計算路徑,這意味著所有可能的計算步驟必須預先設定。這帶來了一個關鍵限制:遞歸呼叫在零知識電路中難以處理,因為電路的深度和複雜性必須是固定的,否則編譯時無法產生有效的證明。遞歸呼叫會導致電路結構不確定,特別是在遞歸深度未知時,電路編譯器無法推導出適當的電路大小和限制。因此,如果 Aleo 程式碼中出現遞歸調用,編譯器將拋出錯誤,提示電路無法產生

下面僅給出一個 leo 語言的程式碼範例,該程式碼無法透過編譯產生 aleo 電路。因為程式碼中 transition test3 會調用 inline 函數 test4,而 test4 會調用另一個 inline 函數 test5,test5 又會回調 test4,形成一個完整的遞歸調用,但是測試結果是無法通過編譯,原因是編譯的電路由於存在環路而無法建造。

3. Aleo 中的溢出

溢位是所有智能合約都需要重點關注的話題,而在 Aleo 中對於整數類型(例如 i32 和 u32), Leo 在運行時將始終會捕捉下溢和溢位。在 transition 中,如果發生下溢或溢出,證明者將無法建立證明。在 function 中,如果發生這種情況,整個交易將被還原。

對於 field 類型,由於它是模運算,因此不會出現下溢和溢出,這是因為與標準整數類型(如 i32 或 u32)不同,field 類型中的所有運算都使用模數運算來確保結果始終在有限域的範圍內。這意味著無論執行多少次加減乘除運算,數值都不會超出有限域的界限,也不會出現下溢或溢位。

 4. Aleo 中的非同步呼叫安全性

在 Aleo 中,async 指令取代了先前用於在函數體中 output 語句後執行操作的 finalize 指令, async 的引入允許在 Aleo 智能合約中執行非阻塞操作。這意味著當函數遇到 async 操作時,它可以啟動該進程,並繼續執行其他程式碼,而不需要等待非同步任務立即完成。

在先前的 finalize 模式中,函數的輸出完成後才可以執行一些收尾操作,而這種執行方式是線性的,無法並發處理, async 則提供了更高的靈活性和效率。但這也大大增大了部分審計業務場景的難度

例如下面的質押合約中,轉帳和 finalize_stake 涉及的帳本更新分別在兩個不同的非同步函數中,在非同步環境中轉帳是可能失敗的,因此需要使用 await 操作等到轉帳成功執行之後,再進行質押帳本的更新

// Claim unbonding microcreditsasync transition stake(public amount: u64) -> Future {        let f1: Future = credits.aleo/transfer_public(Staking.aleo, amount);        return finalize_stake(self.caller, amount, f1, f2);    }    async function finalize_stake(public caller: address, public amount: u64, public f1: Future) {        f1.await();        // Update states        let pre_state: StakerState = state.get(true);        state.set(true, StakerState {            total_pushed: pre_state.total_staked + amount,            total_reward: pre_state.total_reward,        });    }

而下面程式碼範例中,由於 transition 不存在關鍵狀態變數修改,所以 finalize_set_admin 非同步函數中無需 await。

// Add or remove an adminasync transition set_admin(public admin: address, public flag: bool) -> Future {        // Prevent all admins from getting lost        assert_neq(self.caller, admin);        return finalize_set_admin(self.caller, admin, flag);    }
async function finalize_set_admin(public caller: address, public admin: address, public flag: bool) { // Only admins can perform this action assert_eq(admins.get(caller), true); admins.set(admin, flag); }

 5. Aleo 中的初始化函數

合約的構造函數通常用於在智能合約部署時初始化一些狀態變數或執行一次性設置,因為 Aleo 中的智能合約透過 transition 函數來更新狀態,而不是在部署時初始化,所以沒有構造函數。因此對於部分項目來說,需要額外的安全校驗限制初始化函數的呼叫順序以及次數,否則可能存在合約重複初始化等安全性問題

// Initialize the programasync transition init() -> Future {        assert_eq(self.caller, DEFAULT_ADMIN);        return finalize_init();    }
async function finalize_init() { // Can only be initialized once. assert_eq(admins.contains(DEFAULT_ADMIN), false); admins.set(DEFAULT_ADMIN, true); operators.set(DEFAULT_ADMIN, true); state.set(true, StakerState { total_pulled: 0u64, total_pushed: 0u64, total_reward: 0u64, });    }

 6. Aleo 中的三元運算符

在 Leo 語言中,三元運算子會同時計算條件兩邊的表達式。這表示無論條件為 true 或 false,Leo 都會執行 value_if_true 和 value_if_false 兩側的程式碼,而不是僅根據條件選擇一邊執行。這可能是因為在零知識證明中,驗證方需要對整個電路進行驗證(無論實際的條件如何)。因此,使用三元運算子時,Leo 在產生證明的過程中需要同時評估 value_if_true 和 value_if_false,以確保電路涵蓋所有可能分支,以便於正確產生和驗證證明。以下是一個具體的範例:

7. Aleo 中程序的唯一性

Aleo 對於身分和資源的識別方式與傳統區塊鏈網路有所不同,在以太坊中,主要透過地址來識別用戶、合約和帳戶的唯一性。而 Aleo 使用合約名稱來識別唯一性,這樣雖然降低了開發者在引用其他合約時的技術難度,但是卻帶來了許多新的安全問題。

開發者需要特別注意自己合約中引用的 Aleo 程式是否已經部署,否則可能導致邏輯被劫持或是專案直接無法運作的情況。

// Claim unbonding microcreditsasync transition stake(public amount: u64) -> Future {        let f1: Future = credits.aleo/transfer_public(Staking.aleo, amount);        return finalize_stake(self.caller, amount, f1, f2);    }    async function finalize_stake(public caller: address, public amount: u64, public f1: Future) {        f1.await();        // Update states        let state: State = state.get(true);        state.set(true, StakerState {            total_pushed: state.total_staked + amount,            total_reward: state.total_reward,        });    }

綜上所述, Aleo 透過 Leo 抽象低階密碼學並使零知識邏輯表達變得容易,從而簡化了零知識程式設計。開發人員可以編寫簡潔的程式碼,這些程式碼可以編譯為零知識電路並進行驗證,而無需任何密碼學專業知識或經驗。此外, Leo 透過安全模型增強零知識應用程式的安全性,而無需任何明確加密或隱藏。同時,開發者在進行開發 Aleo 智能合約開發時需要了解並注意 Leo 的一些獨特功能和特性,提升其智能合約的安全性,減少潛在的安全風險。

免責聲明:作為區塊鏈資訊平台,本站所發布文章僅代表作者及來賓個人觀點,與 Web3Caff 立場無關。文章內的資訊僅供參考,均不構成任何投資建議及要約,並請您遵守所在國家或地區的相關法律法規。