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。
Gas
在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 microcredits
async 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 admin
async 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 program
async 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 microcredits
async 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的一些独特功能和特性,提升其智能合约的安全性,减少潜在的安全风险。
Beosin此前已对Aleo生态的去中心化交易所AlphaSwap和流动性质押项目Beta Staking进行了详尽的安全审计。审计内容涵盖了智能合约代码的安全性、 业务实现逻辑的正确性、合约代码gas优化、潜在漏洞的发现和漏洞修复等多个方面。
声明:本网站所有相关资料如有侵权请联系站长删除,资料仅供用户学习及研究之用,不构成任何投资建议!