探索Leo:Aleo程序安全入门 - ZKSECURITY

本文介绍了Aleo区块链平台及其上的Leo编程语言,Leo专注于开发具有强大隐私性的应用程序。文章详细阐述了Leo语言的基础知识,包括其语法、记录模型以及与链上状态的交互方式。此外,文章还探讨了Aleo程序中可能存在的潜在漏洞,例如整数溢出、未防护的程序初始化、Record的消费限制以及函数中信息泄露,并提供了避免这些漏洞的最佳实践。

Aleo 是一个区块链平台,它利用零知识密码学来实现私有且可扩展的去中心化应用。Leo 是 Aleo 的核心,是一种专为开发私有应用程序量身定制的高级编程语言。Leo 允许开发人员专注于创建具有强大隐私性的应用程序,而无需考虑零知识证明的复杂性。

理解和利用 Leo 的独特功能对于旨在构建强大且安全解决方案的开发人员至关重要。本文简要介绍了 Leo,重点介绍了它的安全特性和开发人员的实用技巧。它旨在帮助开发人员了解如何利用 Leo 的功能在 Aleo 上编写更安全的程序。本文中的所有程序均以 Leo 2.1.0 版本编写。

Leo 语言基础

Leo 是一种静态类型语言,其语法与 Rust 类似。以下是一个示例程序,可让你对 Leo 语言有一个初步的印象:

program example_program1.aleo {

    mapping data_map: address => u8;

    // transition 在链下执行
    async transition save_sum(private a: u8, private b: u8) -> (u8, Future) {
        let sum: u8 = a + b;
        return (sum, finalize_save_sum(self.caller, sum));
    }

    // finalize 在链上执行
    async function finalize_save_sum(caller: address, sum: u8) {
        data_map.set(caller, sum);
    }
}

Leo 的语法非常简单。在上面的 example_program1.aleo 程序中,它定义了一个映射 data_map,该映射存储的键的类型为 address,值的类型为 u8。在 save_sum transition 中,它接受两个输入,计算总和并将其传递给 finalize_save_sum 函数。然后,在 finalize_save_sum 函数中,它将总和值设置为 data_map,键为调用者地址。

Leo 的独特之处在于它分离了 transitionfunctiontransition 在链下以私有方式执行。执行的正确性由零知识证明来证明,并由网络验证。默认情况下,所有输入(ab)和中间变量都是隐藏的。function 在链上公开执行,以便它可以读取和写入链上的公共状态。

Record 模型

由于 transition 只能在链下执行,因此它无法接触链上状态。相反,它可以创建和使用 Record,后者封装了合约的状态和数据。Record 是一种特殊的结构体,表示 Aleo 上的 UTXO 数据模型。Record 可以具有自定义的字段。私有字段会被加密并存储在账本上。只有创建者和所有者才能解密私有字段。transition 可以通过输出 record 来创建 record,并通过输入 record 来使用 record

program example_program2.aleo {

    record Token {
        owner: address,
        amount: u128
    }

    transition private_transfer_token(private receiver: address, private token: Token) -> Token {
        let new_token: Token = Token {
            owner: receiver,
            amount: token.amount
        };
        return new_token;
    }
}

在上面的代码中,它定义了一个名为 Tokenrecord,其中包含 owneramount 字段。private_transfer_token transition 接受 Token record 作为输入,使用它,并输出一个具有相同金额和新所有者的新 token。默认情况下,Token record 中的所有字段都是私有的。由于 private_transfer_token 是在链下执行的,并且输入和输出是私有的,因此其他人不会知道发送者、接收者和金额。

Aleo 程序的潜在漏洞

Aleo 为构建私有应用程序提供了简单而强大的原语。尽管如此,如果不小心,它可能会引入不直观的行为和漏洞。在本节中,我们来看看 Aleo 程序的编码模式和潜在漏洞。

下溢和溢出

对于整数类型(例如,i8u8),Leo 中始终会捕获下溢和溢出。在 transition 中,如果发生下溢或溢出,证明者将无法创建证明。在 function 中,如果发生这种情况,整个交易将被回滚。

例如,在前面的 example_program1.aleo 程序中,如果我们使用 128u8128u8 运行 save_sum,它将发生 panic

$ leo run save_sum 128u8 128u8
       Leo ✅ Compiled 'example_program1.aleo' into Aleo instructions

Failed constraint at :
        (256 * 1) != 0
Error [ECLI0377012]: Failed to execute the `run` command.
Error: 'example_program1.aleo/save_sum' is not satisfied on the given inputs (14652 constraints).

此外,不可能的强制转换会导致 panic

    // 输入 a 为 256 会发生 panic,因为它无法转换
    transition cast_number(private a: u32) -> u8 {
        return a as u8;
    }

对于 field 类型,不会发生下溢和溢出,因为它是模运算。

program example_program3.aleo {
    transition sub_field(private a: field, private b: field) -> field {
        return a - b;
    }
}

使用 sub_field(0field,1field) 运行上面的程序将获得模数结果:

$ leo run sub_field 0field 1field
       Leo ✅ Compiled 'example_program3.aleo' into Aleo instructions

⛓  Constraints

 •  'example_program3.aleo/sub_field' - 0 constraints (called 1 time)

➡️  Output

 • 8444461749428370424248824938781546531375899335154063827935233455917409239040field

       Leo ✅ Finished 'example_program3.aleo/sub_field'

程序初始化

Leo 没有提供在部署后初始化程序的默认函数(例如 Solidity 中的 constructor 函数)。这意味着开发人员需要自己提供初始化函数。至关重要的是,初始化函数必须得到适当的保护,以防止未经授权的调用或重复调用。

在下面的程序中,initialize 可以被重复调用。然后任何人都可以调用该函数将自己设置为管理员,然后成功调用 mint 函数。

program example_program4.aleo {
    const ADMIN_KEY: u8 = 0u8;
    mapping admin: u8 => address;

    // 这**不**安全!
    async transition initialize() -> Future {
        return finalize_initialize(self.caller);
    }

    async function finalize_initialize(caller: address) {
        admin.set(ADMIN_KEY, caller);
    }

    async transition mint() -> Future {
        return finalize_mint(self.caller);
    }

    async function finalize_mint(caller: address) {
        let current_admin: address = admin.get(ADMIN_KEY);
        assert(current_admin == caller);
        // mint token
    }
}

Record 只能由定义它的程序使用

在 Aleo 程序中,可以在程序 A 中定义 record,并将其传递到程序 B 中。但是,record 只能由定义它的程序(即程序 A)创建或使用。

例如,如果我们想实现一个程序,该程序销毁一个信用 record 并颁发证书,则以下程序将不会销毁信用 record

import credits.aleo;

program example_program5.aleo {
    record BurnerCertificate {
        owner: address,
        amount: u64
    }

    // credit 不会在此 transition 中被使用
    transition burn(credit: credits.aleo/credits) -> BurnerCertificate {
        let certificate: BurnerCertificate = BurnerCertificate {
            owner: credit.owner,
            amount: credit.microcredits
        };
        return certificate;
    }
}

burn transition 接受 credits.aleo/credits 作为输入。但是,信用 record 不会被 transition 使用,因为它不是在当前程序中定义的(即,信用 record 是一个外部 record)。

正确的方法是调用 credits.aleo 中的一个函数来销毁信用 record

    transition burn(credit: credits.aleo/credits) -> BurnerCertificate {
        let certificate: BurnerCertificate = BurnerCertificate {
            owner: credit.owner,
            amount: credit.microcredits
        };
        // 调用 `credits.aleo/transfer_private` 来销毁 credit
        credits.aleo/transfer_private(credit, ZERO_ADDRESS, credit.microcredits);
        return certificate;
    }

在上面的代码中,credit record 被传递到 credits.aleo/transfer_private 中。record 将被销毁,因为 record 和函数是在同一个程序中定义的。

转移到程序的 Record 将会丢失

创建 Record 时,其私有字段使用所有者的地址密钥进行加密。当所有者使用 Record 时,需要使用所有者的私钥进行授权。由于程序没有私钥,因此它不可能使用 Record。这意味着转移到程序的任何 Record 都将丢失。

开发人员应谨慎处理此问题,以确保 record 不会被发送到程序。目前,没有直接的方法可以判断地址是否为程序地址,除非它需要所有者为 self.signer

在 Function 中泄露信息

在 Aleo 程序中,function 在链上执行并且完全公开。开发人员应该谨慎对待传递给 function 的内容。

下面的代码是一个可能泄露有关 Record 信息的示例:

program example_program6.aleo {
    record Token {
        owner: address,
        amount: u128,
        expire_at: u32
    }

    async transition transfer(private receiver: address, private token: Token) -> (Token, Future) {
        let new_token: Token = Token {
            owner: receiver,
            amount: token.amount,
            expire_at: token.expire_at
        };
        return (new_token, finalize_transfer(token.expire_at));
    }

    // 它可能会泄露信息,因为 `expire_at` 字段是公开的。
    async function finalize_transfer(expire_at: u32) {
        assert(expire_at > block.height);
    }
}

transfer transition 中,它会将 Tokenexpire_at 字段传递到 finalize_transfer 函数中。如果某些 record 具有不同的 expire_at 值,则其他人将能够推断出正在转移哪个 Token

缓解此问题的一种方法是避免将确切的 expire_at 字段传递给该函数。相反,我们可以输入一个中间值 intermediate_height 并确保 intermediate_height < height 并且 token.expire_at <= intermediate_heighttransition 中:

program example_program6.aleo {
    record Token {
        owner: address,
        amount: u128,
        expire_at: u32
    }

    async transition transfer(private receiver: address, private token: Token, intermediate_height: u32) -> (Token, Future) {
        assert(token.expire_at >= intermediate_height);
        let new_token: Token = Token {
            owner: receiver,
            amount: token.amount,
            expire_at: token.expire_at
        };
        return (new_token, finalize_transfer(intermediate_height));
    }

    // 仅在公共函数中比较 `intermediate_height`
    async function finalize_transfer(intermediate_height: u32) {
        assert(intermediate_height > block.height);
    }
}

三元条件运算符将评估双方

Leo 支持三元条件运算符。但是,该条件将评估双方。考虑以下代码来计算 sub 的绝对值:

program example_program7.aleo {
    transition abs_sub(private a: u32, private b: u32) -> u32 {
        return a > b ? a - b : b - a;
    }
}

a != b 时,此函数将始终 panic。这是因为 a - bb - a 都将被评估,并且其中一个将下溢:

$ leo run abs_sub 2u32 1u32
       Leo ✅ Compiled 'example_program7.aleo' into Aleo instructions

Failed constraint at :
        (0 * 1) != 1
Error [ECLI0377012]: Failed to execute the `run` command.
Error: 'example_program7.aleo/abs_sub' is not satisfied on the given inputs (13834 constraints).

为了避免下溢,我们可以将无符号整数转换为有符号整数。然后,将结果强制转换回无符号整数:

    transition abs_sub(private a: u32, private b: u32) -> u32 {
        // 使用 i64 确保成功强制转换
        let ai: i64 = a as i64;
        let bi: i64 = b as i64;
        let ci: i64 = ai > bi ? ai - bi : bi - ai;
        return ci as u32;
    }

通过名称识别的程序可能会导致问题

在 Aleo 中,程序通过其程序名称来识别,而与其程序内容无关。程序通过名称导入另一个程序。这意味着开发人员在部署后应手动验证该程序。

例如,假设你要部署一个多重签名程序(比如 my_secure_multisig.aleo)来持有你的资金。通常的步骤是首先部署 my_secure_multisig.aleo 程序,然后通过名称将资金发送到该程序。但是,攻击者可能能够抢在你的部署之前,并部署一个具有相同名称但内容完全不同的程序。然后,my_secure_multisig.aleo 程序将由攻击者控制。一旦你发送资金,你将立即失去所有资金。

另一个例子是关于程序导入。假设你有两个程序:programA.aleoprogramB.aleoprogramA.aleo 导入 programB.aleo。你需要首先部署 programB.aleo,然后部署 programA.aleo。攻击者有可能抢先部署一个假的 programB.aleo 在你的交易之前。然后,你的 programA.aleo 将导入假的 programB.aleo。这将使整个程序不安全。

为了避免此类攻击,我们建议在你的部署交易后始终验证该程序是否由你部署。

结论

作为一个新的区块链平台,Aleo 为构建私有应用程序提供了简单而强大的原语。尽管如此,这些新的原语可能会引入不直观的行为和漏洞,如果不小心。在本文中,我们讨论了 Aleo 程序的潜在漏洞。随着 Aleo 的发展,我们将密切关注其发展,并继续提供有关在此创新空间中维护安全性的见解。

  • 原文链接: blog.zksecurity.xyz/post...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
zksecurity
zksecurity
Security audits, development, and research for ZKP, FHE, and MPC applications, and more generally advanced cryptography.