Aleo智能合约开发:Leo语言核心机制与实战指南

· 55min · Paxon Qiao

Aleo智能合约开发:Leo语言核心机制与实战指南

在Web3的世界里,数据隐私与链上计算成本一直是难以平衡的痛点。作为专注于零知识证明(ZK)的隐私公链,Aleo交出了自己的工程化答卷——专属智能合约语言 Leo

不同于以太坊的“明牌斗地主”,Leo 从底层设计上就贯彻了“默认隐私(Private by Default)”的硬核原则。它通过混合虚拟机架构,将沉重的计算剥离到本地链下执行,链上仅做轻量级的零知识证明验证。

这篇文章将系统性地带你走进 Leo 的世界。我们将褪去 ZK 技术的学术光环,从最基础的变量、可选类型(Option)和辅助函数看起,深入剖析加密记录(Record)的流转逻辑、链上持久化存储(Storage)的操作,以及合约可升级性与动态分派等进阶技巧。无论你是刚接触 Aleo 的新手,还是准备实操写代码的开发者,这篇务实的笔记都能帮你快速梳理出一条清晰的入门路径。

本文全面梳理了Aleo网络专属智能合约语言Leo的基础语法与核心机制。从“默认隐私”原则、数据类型到链下计算与链上状态的交互,并结合私密拍卖实战与动态分派等高阶特性,为你提供一份极具实操性的入门通关手册。

Leo 入门:用于 Aleo 的特定领域语言

Leo:用于 Aleo 的特定领域(智能合约)语言(DSL)

为什么需要另一种语言?

零知识证明(ZK)在本质上是不同的。

一门好的开发语言需要具备以下特性:

  • 直观
  • 可审计
  • 可适应

开始使用 Leo

安装 Leo CLI

cargo install leo-lang

https://docs.leo-lang.org/getting_started/installation

使用 Leo Playground IDE

https://play.leo-lang.org/

Aleo:混合虚拟机

链下(Off-chain)

  • 在本地完成计算:计算过程在用户的本地设备上运行。
  • 保护隐私:由于数据在本地处理,隐私得到了有效的保存。
  • 生成 ZK 证明:生成零知识证明(ZK proof),用于证明计算的完整性与准确性。

链上(On-chain)

  • Final:一种延迟调用机制,代码将被上传至链上虚拟机并在那里执行。
  • 验证 ZK 证明:节点对提交的零知识证明进行验证。
  • 更新链上状态:验证通过后,正式更新区块链上的全局状态。

image-20260520213135873

数据类型 (Data Types)

Leo 有 16 种原始数据类型

代数类型二元类型整数类型整数类型专用类型
fieldbooleanu8i8address
scalaru16i16signature
groupu32i32identifier
u64i64
u128i128

Leo 支持复合类型

用户自定义类型:

  • struct
  • record
  • array
  • tuple
  • option
  • const
  • const generic

struct 类型

struct Message {
    timestamp: u64,
    data: [u8; 10],
    sig: signature,
}

这段代码在 Leo 语言中定义了一个名为 Message 的自定义复合数据结构(结构体),它将三个相关的数据片段捆绑在一起:一个 u64 类型的 timestamp(通常用于记录 64 位无符号时间戳)、一个长度严格限定为 10 的 u8 字节数组 data(用于存储具体的消息载荷),以及一个 Leo 特有的 signature 类型字段 sig(专门用于存放密码学签名,以验证该消息的真实性与来源)。

让我们从以下这个 Leo 程序开始

fn foo(x: u64, y: u64) -> u64 {
  return x - y;
}

program hello.aleo {
  fn main(public x: u64, y: u64) -> private u64 {
    let z: u64 = foo(x, y);
    return z;
  }
}

这段代码非常经典,它用极其简洁的语法完美展示了 Leo 语言中“辅助函数调用”“数据隐私可见性”的核心机制。

帮你拆解一下里面的核心逻辑:

1. 辅助函数 (Helper Function)

fn foo(x: u64, y: u64) -> u64 {
  return x - y;
}
  • 这是一个纯粹的辅助函数,专门用来处理通用的计算逻辑(这里是减法 x - y)。
  • 它不涉及任何链上状态或 Record,没有复杂的上下文。在底层编译时,这段代码会被直接内联(Inline)展开到调用它的主电路中。

2. 主程序与可见性控制 (Visibility)

program hello.aleo {
  fn main(public x: u64, y: u64) -> private u64 {
    let z: u64 = foo(x, y);
    return z;
  }
}

这段代码最精彩的地方,在于它生动地诠释了咱们刚才聊过的 “Private by Default”(默认隐私) 机制:

  • public x:你显式加上了 public 关键字。这意味着当你生成 ZK 证明并广播上链时,x 的具体数值是公开的,全网矿工都能看到它。
  • y: u64:没有加任何修饰符!所以,它触发了默认隐私机制,y 是私密的。全网没人知道你传入的 y 到底是多少,它只在你本地电脑的内存里参与“默读”计算。
  • -> private u64:明确规定计算结果 z 也是隐私的。

💡 一句话总结这段代码在 ZK 里的业务含义

“我拿着一个全网公开的数字 x,减去一个只有我自己知道的私密数字 y,最后得出一个同样只有我自己知道的私密结果 z,并向全网提交了一份证明,告诉大家我算得一点毛病都没有。”

这正是零知识证明(ZK)最迷人的地方。

Leo 为用户提供了熟悉的语法

if (flag) {
  return foo;
} else {
  // Do something...
}

for i:u8 in 0u8..100u8 {
  sum += data[0u8] + data[i];
}

assert(foo != bar);
assert_eq(foo, baz);
assert_neq(self.signer, self.caller);

这段 Leo 代码展示了 Aleo 智能合约开发中的核心逻辑控制与安全校验。

首先,代码通过 if-else 条件分支根据布尔变量 flag 的状态决定是直接返回变量 foo 还是继续执行后续逻辑。紧接着是一个定长循环(for),它使用 u8(8位无符号整数)类型显式声明计数器 i 在 0 到 100 的范围内迭代,将数组 data 的首项以及第 i 项的值累加到变量 sum 中,体现了 Leo 对强类型和明确边界的要求。最后,代码通过三条断言语句进行静态或运行时的安全状态检查:确保 foo 不等于 bar、验证 foo 绝对等于 baz,并严格校验交易的签名者(signer)与当前的调用者(caller)不是同一地址,以此来保障智能合约的访问控制与数据正确性。

Option

program optionals.aleo {
  struct Point { x: u32, y: u32 }
  // 旧语法 transition main() {
  fn main() {
    // 可选整数
    let x: u8? = 42u8;
    let y = x.unwrap(); // 返回 42u8
    let z: u8? = none;
    let a = z.unwrap_or(99u8); // 返回 99u8
    // 可选数组
    let arr: [u16?; 2] = [1u16, none];
    let first_val = arr[0].unwrap(); // 返回 1u16
    let second_val = arr[1].unwrap_or(0u16); // 返回 0u16
    // 可选结构体
    let p: Point? = none;
    let p_val = p.unwrap_or(Point { x: 0u32, y: 0u32 });
    // 返回默认的 Point { x: 0u32, y: 0u32 }
    }
}

这段代码展示了 Leo 语言中可选类型(Optionals)的用法,这在处理可能为空(none)的数据时非常有用,类似于 Rust 中的 Option<T> 或 Swift 中的 Optional

以下是这段代码核心语法的逐行解析与说明:

1. 结构体定义与主函数

struct Point { x: u32, y: u32 }
fn main() { ... }
  • 定义了一个名为 Point 的结构体,包含 xy 两个 32 位无符号整数。
  • fn main() 是程序的入口函数(注:注释中提到的 transition 是 Aleo 旧版本中定义状态转换的关键字,新版本中很多基础逻辑可以直接用 fn 函数实现)。

2. 可选整数(Optional Integers)

let x: u8? = 42u8;
let y = x.unwrap(); // 返回 42u8
  • u8? 表示这是一个可选的 8 位无符号整数,它的值要么是一个具体的数字,要么是空(none)。
  • x.unwrap() 用于“解包”。因为 x 确实包含值 42u8,所以解包成功,y 的值变为 42u8。如果 xnone,程序在此处会直接崩溃(Panic)。
let z: u8? = none;
let a = z.unwrap_or(99u8); // 返回 99u8
  • z 被赋值为 none,代表没有值。
  • unwrap_or(99u8) 是一种安全解包方式:如果 z 有值,就取 z 的值;如果 znone,则返回括号内的默认值(这里是 99u8)。因此 a 的最终结果是 99

3. 可选数组(Optional Arrays)

let arr: [u16?; 2] = [1u16, none];
let first_val = arr[0].unwrap(); // 返回 1u16
let second_val = arr[1].unwrap_or(0u16); // 返回 0u16
  • [u16?; 2] 定义了一个长度为 2 的数组,里面的每个元素都是“可选的 16 位无符号整数”。这里初始化为第一个元素有值(1u16),第二个元素为空(none)。
  • 通过索引访问并解包:arr[0] 成功解包出 1u16arr[1] 因为是 none,通过 unwrap_or(0u16) 降级获取到了默认值 0u16

4. 可选结构体(Optional Structs)

let p: Point? = none;
let p_val = p.unwrap_or(Point { x: 0u32, y: 0u32 });
  • 不仅是基础数据类型,自定义的结构体也可以是可选的。这里 p 的类型是 Point?,初始值为 none
  • 同样使用 unwrap_or,当 p 为空时,提供一个实例化的默认结构体 Point { x: 0u32, y: 0u32 } 作为兜底数据。

总结

这段代码的核心目的就是演示在 Aleo (Leo) 中如何安全地声明、存放以及读取可能不存在的数据。使用 ? 声明可选型,配合 .unwrap()(强行解包)和 .unwrap_or()(安全替代解包),可以有效避免智能合约在运行时因空指针或未定义数据而引发的安全漏洞。

Const & const generic

# const & const generic

struct Matrix::[N: u32, M: u32] {
    data: [field; N * M],
}

// 使用示例
let m = Matrix::[2, 2] { data: [0, 1, 2, 3] };

fn sum_first_n_ints::[N: u32]() -> u32 {
    let sum = 0u32;
    for i in 0u32 .. N {
        sum += i;
    }
    return sum;
}

program main.aleo {
    const MAX: u32 = 100u32; // 常量定义

    fn main() -> bool {
        return assert(sum_first_n_ints::[5u32]() < MAX);
    }
}

这段代码展示了 Leo 语言中非常强大的两个特性:常量定义(Constants)常量泛型(Const Generics)。在零知识证明(ZKP)的智能合约中,电路的大小通常需要在编译时确定,而这两个特性正是为了在编译期确定数组长度、循环次数和边界而设计的。

以下是代码的详细解析说明:

1. 常量泛型结构体(Const Generic Struct)

struct Matrix::[N: u32, M: u32] {
    data: [field; N * M],
}

// 使用示例
let m = Matrix::[2, 2] { data: [0, 1, 2, 3] };
  • 语法解析::[N: u32, M: u32] 是 Leo 的常量泛型语法。这里的 NM 不是普通的运行时变量, 而是编译期常量
  • 核心作用:它允许你定义一个通用的矩阵结构体,其内部数组 data 的长度动态取决于 N * M 的乘积。
  • 实例化:在 Matrix::[2, 2] 中,编译期会将 NM 替换为 2,从而自动推导出来 data 的实际类型为长度为 4(2 * 2)的 field 数组。

2. 常量泛型函数(Const Generic Function)

fn sum_first_n_ints::[N: u32]() -> u32 {
    let sum = 0u32;
    for i in 0u32 .. N {
        sum += i;
    }
    return sum;
}
  • 语法解析:函数名后面跟着 ::[N: u32],意味着这个函数接收一个编译期常量 N
  • 核心作用:在零知识证明电路中,循环的次数必须是固定的(即不能是运行时的变量)。通过使用常量泛型 N 作为循环的终点(0u32 .. N),Leo 编译器可以在编译时将这个循环“展开”(Unroll),生成确定大小的零知识证明电路。

3. 常量定义与主程序(Constants & Program)

program main.aleo {
    const MAX: u32 = 100u32; // 常量定义

    fn main() -> bool {
        return assert(sum_first_n_ints::[5u32]() < MAX);
    }
}
  • 常量定义const MAX: u32 = 100u32; 全局定义了一个不可变的值 100
  • 函数调用与断言
    • sum_first_n_ints::[5u32]() 调用了上面的泛型函数,并将 N 传为 5。此时函数会计算 0 + 1 + 2 + 3 + 4 = 10
    • assert(... < MAX) 校验计算结果(10)是否小于 MAX(100)。如果条件成立(True),则返回 true 并顺利通过,否则程序会中断报错。

总结

这段代码的核心意义在于:让 Leo 的代码既具备灵活性,又满足 ZK 电路编译期的严格限制。通过 ::[...] 语法,你可以像写现代编程语言(如 Rust 的 Const Generics)一样,优雅地复用那些需要固定数组长度或固定循环次数的底层安全逻辑。

熟悉 Leo 语法

请定义一个名为 max 的 fn 函数,要求如下:

  • 输入:一个长度为 10 的 u8 类型数组。
  • 返回:数组中的最大元素。
// The 'max' program.
program max.aleo {
    // This is the constructor for the program.
    // The constructor allows you to manage program upgrades.
    // It is called when the program is deployed or upgraded.
    // It is currently configured to **prevent** upgrades.
    // Other configurations include:
    //  - @admin(address="aleo1...")
    //  - @checksum(mapping="credits.aleo/fixme", key="0field")
    //  - @custom
    // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
    @noupgrade
    constructor() {}

    fn main(public a: u32, b: u32) -> u32 {
        let c: u32 = a + b;
        return c;
    }

    fn max(arr: [u8; 10]) -> u8 {
        let max: u8 = 0;
        for i:u8 in 0..=9 {
            if max < arr[i] {
                max = arr[i];
            }
        }
        return max;
    }
}

这段 Leo 代码完整展示了一个名为 max.aleo 的智能合约,包含合约可升级性控制基础加法运算以及数组最值查找三个核心功能。

首先,合约头部使用 @noupgrade 装饰器修饰 constructor() 构造函数,明确规定该合约在部署后不可被升级,以此确保合约逻辑的不可篡改与安全性(注释中也提到了支持通过管理员地址或校验和进行升级的其他配置项)。

在业务逻辑上,合约提供了两个函数:

  1. main 函数接收一个公开的 a 和一个私有的 b(均为 32 位无符号整数),计算它们的和并返回,演示了 Leo 处理公开/私有输入的基本方法;
  2. max 函数则接收一个长度为 10 的 u8 类型数组,通过一个从 0 到 9 的定长循环(0..=9)遍历数组,在循环内部不断对比并更新最大值变量 max,最终返回该数组中的最大元素。整个合约非常清晰地体现了 Aleo 链上开发关于权限控制、隐私输入与定长循环的典型设计模式。

运行

Code/Aleo/max
➜ leo run max "[0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8]"
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'max.aleo'
       Leo     44 statements before dead code elimination.
       Leo     44 statements after dead code elimination.
       Leo     The program checksum is: '[153u8, 4u8, 250u8, 119u8, 87u8, 33u8, 29u8, 9u8, 203u8, 236u8, 53u8, 182u8, 5u8, 1u8, 162u8, 12u8, 181u8, 4u8, 17u8, 177u8, 149u8, 227u8, 141u8, 63u8, 182u8, 220u8, 23u8, 34u8, 149u8, 231u8, 210u8, 230u8]'.
       Leo     Program size: 0.91 KB / 500.00 KB
       Leo ✅ Compiled 'max.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No valid private key specified, defaulting to 'APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH'.

➕Adding programs to the VM in the following order:
  - max.aleo (local)

➡️  Output

 • 9u8

这段日志展示了使用 Leo 命令行工具(CLI)在本地运行并测试 max 合约中 max 函数的完整执行过程。

🛠️ 执行流程拆解

1. 运行命令与环境初始化

➜ leo run max "[0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8]"
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.
  • 命令含义:执行 max 合约,传入一个包含 10 个 u8 类型元素的数组(0 到 9)作为输入参数。
  • 环境警告:由于没有指定具体的网络和节点,Leo 自动将网络设置为 testnet,并将 API 端点默认指向 Provable 的测试网节点。

2. 代码编译与优化(Compilation)

Leo 🔨 Compiling 'max.aleo'
Leo     44 statements before dead code elimination.
Leo     44 statements after dead code elimination.
Leo     The program checksum is: '[153u8, 4u8, ...]'.
Leo     Program size: 0.91 KB / 500.00 KB
Leo ✅ Compiled 'max.aleo' into Aleo instructions.
Leo ✅ Generated ABI at 'build/abi.json'.
  • 死代码消除(Dead Code Elimination):编译器检查是否有未使用的变量或无法到达的代码。这里前后都是 44 条语句,说明代码非常精简,没有冗余。
  • 校验和与体积:生成了该程序的唯一校验和(Checksum),并显示编译后的体积仅为 0.91 KB(远低于 500 KB 的限制)。
  • 产物生成:成功将高级的 Leo 代码编译成了底层的 Aleo 字节码(Aleo instructions),并生成了用于前端交互的 ABI 接口文件

3. 虚拟机加载与执行(Execution)

⚠️ No valid private key specified, defaulting to 'APrivateKey1zkp8...'.
➕Adding programs to the VM in the following order:
  - max.aleo (local)
  • 本地测试密钥:由于是在本地进行测试运行,没有指定真实的私钥,Leo 自动使用了一个内置的默认测试私钥来模拟签名。
  • 加载合约:将刚刚编译好的 max.aleo 合约加载到 Aleo 虚拟机(AVM)中。

4. 最终输出(Output)

➡️  Output
 • 9u8
  • 经过 AVM 虚拟机的计算,循环遍历了你输入的 [0u8, 1u8, ... 9u8] 数组,成功找出了其中的最大值,并正确输出了结果:9u8

零知识证明(ZK)程序的额外超能力

与其重新计算所有内容,只需通过额外参数来检查正确性。

// The 'max' program.
program max.aleo {
    // This is the constructor for the program.
    // The constructor allows you to manage program upgrades.
    // It is called when the program is deployed or upgraded.
    // It is currently configured to **prevent** upgrades.
    // Other configurations include:
    //  - @admin(address="aleo1...")
    //  - @checksum(mapping="credits.aleo/fixme", key="0field")
    //  - @custom
    // For more information, please refer to the documentation: `https://docs.leo-lang.org/guides/upgradability`
    @noupgrade
    constructor() {}

    fn main(public a: u32, b: u32) -> u32 {
        let c: u32 = a + b;
        return c;
    }

    fn max(arr: [u8; 10]) -> u8 {
        let max: u8 = 0;
        for i:u8 in 0..=9 {
            if max < arr[i] {
                max = arr[i];
            }
        }
        return max;
    }

    fn max_verify(input: u8, min: u8) -> u8 {
        assert(input > min);
        return input;
    }
}

这段 Leo 代码定义了一个名为 max.aleo 的智能合约,它通过 @noupgrade 装饰器配置构造函数,规定合约部署后不可升级以确保逻辑的不可篡改性;在业务逻辑上,合约除了包含基础的公开与私有输入加法运算(main 函数)以及通过 for 循环遍历 10 位定长数组查找最大值(max 函数)之外,还新增了一个 max_verify 安全校验函数,该函数利用 assert 断言严格限制输入的数值 input 必须大于设定的最小值 min,只有通过该安全门槛才会返回结果,完整地展示了 Aleo 智能合约在可升级性控制、隐私计算、循环遍历以及边界条件阻断校验上的典型开发模式。

运行

Code/Aleo/max
➜ leo run max_verify 5u8 10u8
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'max.aleo'
       Leo     47 statements before dead code elimination.
       Leo     47 statements after dead code elimination.
       Leo     The program checksum is: '[69u8, 39u8, 130u8, 175u8, 86u8, 84u8, 179u8, 8u8, 170u8, 53u8, 85u8, 196u8, 70u8, 211u8, 179u8, 169u8, 114u8, 172u8, 48u8, 5u8, 166u8, 85u8, 87u8, 131u8, 34u8, 102u8, 29u8, 207u8, 192u8, 153u8, 91u8, 43u8]'.
       Leo     Program size: 1.06 KB / 500.00 KB
       Leo ✅ Compiled 'max.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No valid private key specified, defaulting to 'APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH'.

➕Adding programs to the VM in the following order:
  - max.aleo (local)
Error [ECLI0377045]: Failed to evaluate program: Stack evaluation failed: Instruction (assert.eq r2 true;) at index 1 failed: 'assert.eq' failed: 'false' is not equal to 'true' (should be equal)


Code/Aleo/max
➜ leo run max_verify 15u8 10u8
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'max.aleo'
       Leo     47 statements before dead code elimination.
       Leo     47 statements after dead code elimination.
       Leo     The program checksum is: '[69u8, 39u8, 130u8, 175u8, 86u8, 84u8, 179u8, 8u8, 170u8, 53u8, 85u8, 196u8, 70u8, 211u8, 179u8, 169u8, 114u8, 172u8, 48u8, 5u8, 166u8, 85u8, 87u8, 131u8, 34u8, 102u8, 29u8, 207u8, 192u8, 153u8, 91u8, 43u8]'.
       Leo     Program size: 1.06 KB / 500.00 KB
       Leo ✅ Compiled 'max.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No valid private key specified, defaulting to 'APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH'.

➕Adding programs to the VM in the following order:
  - max.aleo (local)

➡️  Output

 • 15u8

这段运行结果展示了对 max.aleo 合约中 max_verify 函数进行正反两次输入测试的完整对比:在第一次测试中,由于传入的输入值 5u8 并未大于设定的最小值 10u8,直接触发了函数内部的 assert 断言失败,导致虚拟机抛出“false 不等于 true”的错误并终止了程序(Error: Failed to evaluate program);而在第二次测试中,由于传入的输入值升为了 15u8,成功满足了大于 10u8 的断言条件(15 > 10 为真),程序因此顺利通过了底层的堆栈评估与安全检查,最终在 AVM 虚拟机中成功执行完毕并正确输出了校验后的结果 15u8

将应用程序状态编码为加密的数据块(Encrypted Blobs of Data)

记录

record Token {
    owner: address,
    balance: u64,
}
  • 应用状态以记录的形式进行编码。
  • 用户对其记录拥有专属性的所有权。
  • 记录实现了并发性和隐私性。

Leo 为用户提供了强大的构建模块

record + struct

struct Payload {
  amount: u128,
  timestamp: u64,
  ticker: string,
}

record State {
  owner: address,
  message: Payload,
}

Record 可以拥有 struck,struck 不可以拥有 Record。

在 Aleo 上使用记录(Record)

image-20260520230005176

program token.aleo

fn transfer(sender: token, receiver: address, amount: u64) -> (token, token) {
    let difference: u64 = sender.amount - amount;
    let remaining: token = token {
        owner: sender.owner,
        amount: difference,
    };
    let transferred: token = token {
        owner: receiver,
        amount: amount,
    };
    return (remaining, transferred);
}

这段 Leo 代码实现了一个典型的代币转账(Transfer)逻辑,完整地展示了在 Aleo 零知识隐私网络中“记录(Record)拆分与重组”的核心机制。

当调用该函数时,它接收一个包含资产和所有者信息的 sender 代币记录、一个接收者地址 receiver 以及转账金额 amount。函数内部首先通过计算原资产总额与转账金额的差值得到 difference,随后将原本的一笔大额代币“拆分”并克隆出两笔全新的代币记录:第一笔是属于发送者原所有者的找零记录(remaining,金额为扣除后的余数;第二笔则是属于接收者的到账记录(transferred,金额为指定的转账数额。最后,函数以元组的形式同时返回这两笔新生成的代币记录,在保证账目配平的前提下,完美契合了 Aleo 基于 UTXO(更准确地说是 Record 模型)的隐私交易链上状态更新逻辑。

image-20260521104949070

账本中怎么记录

image-20260521105434358

Leo 为用户提供了链上状态

storage & mapping

program storage_ops.aleo {
    mapping balances: address => u64;
    storage counter: u32;
    storage vec: [u32];
}

fn main() -> Final {
    return final {
        vec.push(10u32); // 在向量 `vec` 的末尾添加元素 10u32
        let x = vec.pop(); // 弹出并返回向量 `vec` 的最后一个元素
        let y = vec.get(5); // 获取索引为 5 的元素
        vec.set(3, 5u32); // 将索引为 3 的元素设置为 5u32
        let y = vec.len(); // 返回向量 `vec` 中元素的数量
        vec.swap_remove(3); // 从向量 `vec` 中移除索引为 3 的元素并返回它。
        // 被移除的元素将被向量中的最后一个元素替换
    }
}

这段 Leo 代码展示了 Aleo 智能合约中关于链上持久化存储(Storage)与动态向量(Vector)操作的核心语法,特别演示了如何在 final 异步块(即链上执行阶段)中管理状态。

在合约定义部分,它首先通过 mapping 声明了地址到余额的映射,并首次引入了 storage 关键字来声明持久化变量,包括一个标量计数器 counter 和一个动态向量 vec。在主函数 main() 中,通过 return final { ... } 将涉及链上状态修改的代码隔离在 final 块中执行。在 final 块内部,代码集中演示了对 vec 向量的各种标准数据结构操作:利用 push 在末尾追加元素、pop 弹出尾部元素、getset 依照索引进行读写、len 获取当前长度,以及非常高效的 swap_remove 操作(通过将末尾元素移动到被删除位置来快速移除指定索引元素)。这组高级集合操作极大地丰富了 Leo 语言在处理复杂链上状态和列表数据时的灵活性。

final

final fn update_balance(receiver: address, amount: u64) {
  let current: u64 = balances.get_or_use(receiver, 0u64);
  balances.set(receiver, current + amount);
}

program token.aleo {
  mapping balances: address => u64;

  fn airdrop(public r1: address, public r2: address, public amount: u64) -> Final {
    return final {
      update_balance(r1, amount);
      update_balance(r2, amount);
    };
  }
}

这段 Leo 代码展示了 Aleo 智能合约中空投(Airdrop)功能的实现,它完美地呈现了如何利用 final fn 异步函数和 mapping 在链上原子化地更新多个账户的状态。

token.aleo 合约中,首先定义了一个名为 balances 的哈希映射,用于在链上持久化存储每个地址对应的代币余额(address => u64)。核心的空投逻辑由 airdrop 函数实现,它接收两个接收者地址(r1r2)以及空投金额(amount),并返回一个 Final 块。真正的状态修改发生在 return final { ... } 内部,它连续调用了两次辅助函数 update_balance。该辅助函数被显式标记为 final fn(意味着它只能在 final 块内被异步调用),在执行时会先通过 balances.get_or_use(receiver, 0u64) 安全地获取接收者当前的余额(如果不存在则默认为 0u64),然后将空投金额累加并使用 balances.set 重新写回链上存储。这种将计算与状态修改分离的设计,确保了两个地址的余额更新在同一笔交易中以原子化的方式安全完成。

利用链上状态实现公开与隐私的转换执行

第一步:Alice 想网络查询她的加密记录

image-20260521225543128

第二步:Alice 在本地执行 fn transfer 调用

image-20260521230100036

第三步:Alice 生成一笔交易并将其发送到网络

image-20260521230825377

私密拍卖

你被要求使用Leo语言实现一个私密拍卖程序。在此次设计中,竞买人(Bidders)需无法获知彼此的任何信息。

我们并不介意将信息泄露给拍卖方(Auctioneer),即负责运行该拍卖的机构或个人。

挑战:

使用记录(Record)实现私密拍卖

1. Alice 提交一个出价

image-20260521231331043

2. Bob 提交一个出价

Using records to implement an auction

image-20260521231445687

3. 拍卖方选择获胜的出价

image-20260521231556826

4. 拍卖方锁定获胜的出价

image-20260521231844302

实操

查看项目目录结构

Code/Aleo/auction
➜ tree . -L 6 -I ".gitignore|.github|.git|target|node_modules|data"
.
├── build
│   ├── abi.json
│   ├── imports
│   ├── main.aleo
│   └── program.json
├── outputs
├── program.json
├── readme.md
├── src
│   └── main.leo
└── tests
    └── test_auction.leo

6 directories, 7 files

src/main.leo

// The 'auction' program.
program auction.aleo {
    const OWNER: address = aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl;

    record Bid {
        owner: address,
        amount: u64,
        bidder: address,
    }

    record WinningBid {
        owner: address,
        amount: u64,
        isWinner: bool,
    }

    @noupgrade
    constructor() {}

    fn place_bid(amount: u64) -> Bid {
        return Bid { owner: OWNER, amount, bidder: self.caller };
    }

    fn resolve(bid1: Bid, bid2: Bid) -> Bid {
        if bid1.amount > bid2.amount {
            return bid1;
        } else {
            return bid2;
        }
    }

    fn finish(bid: Bid) -> WinningBid {

        return WinningBid {
            owner: bid.bidder,
            amount: bid.amount,
            isWinner: true
        };

    }
}

这段 Leo 代码实现了一个去中心化的链上盲拍(Auction)智能合约,利用 Aleo 特有的 record(记录)机制在保障用户隐私的同时完成了竞价与裁决逻辑。

合约首先硬编码了拍卖方的官方地址 OWNER,并定义了两种链上加密数据载体:用于表示用户竞标的出价记录 Bid,以及最终获胜的凭证记录 WinningBid。合约的运行流程分为三个核心步骤:

  1. 出价(place_bid:任何人都可以调用此函数,传入自己想要竞拍的金额 amount,系统会生成并返回一张所有权属于拍卖方 OWNER、但标记了出价人(self.caller)和金额的 Bid 记录。
  2. 两两裁决(resolve:拍卖方作为 Bid 的拥有者,可以在本地离线运行此函数,对任意两张 Bid 记录进行金额对比,并淘汰较小者,只返回金额更高的那张 Bid(通过多次两两对比,最终就能筛选出全场最高价)。
  3. 结束拍卖(finish:拍卖方将最终筛选出的那张最高价 Bid 传入此函数,合约会将其转换为一张全新的 WinningBid 记录并返回,此时该记录的拥有者(owner)被交还给了竞标成功者(bid.bidder),并将其赢家状态 isWinner 标记为 true,从而安全、隐私地完成了整个拍卖结算闭环。

运行测试

初始化账户与创建项目说明文档

Code/Aleo/auction
➜ leo account new

  Private Key  APrivateKey1zkp8hHQtE3gZwUB6n1fNCKjVeQGgj4U9HtTCXUDVKp1PGab
     View Key  AViewKey1ty22aHiRtDwj8dWoeuJtQzexBb4wc1VpDpLFPMKyAvsL
      Address  aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl


Code/Aleo/auction
➜ touch readme.md

这段运行结果和终端操作展示了在进行 Aleo 智能合约(这里是上面的 auction 拍卖合约)部署或测试前,初始化账户与创建项目说明文档的准备工作。

首先,通过执行 leo account new 命令,Leo 命令行工具在本地成功创建了一个全新的 Aleo 链上加密账户,并完整输出了该账户的“三件套”凭证:用于签名交易和掌控资产的私钥(Private Key)、用于解密和查看隐私状态的观影钥(View Key),以及对外公开、同时也被硬编码在上方拍卖合约中作为 OWNER钱包地址(Address)。紧接着,通过 touch readme.md 命令在当前 auction 项目根目录下新建了一个 Markdown 格式的自述文件,用于后续编写该拍卖合约的使用说明或部署指南,这是规范化开发 Aleo 智能合约的标准前置流程。

编译构建合约

Code/Aleo/auction
➜ leo build
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
Error [EPAR0370021]: The type of `self` has no associated function `caller` that takes 0 argument(s).
    --> /Users/qiaopengjun/Code/Aleo/auction/src/main.leo:15:52
     |
  15 |         return Bid { owner: OWNER, amount, bidder: self.caller() };
     |                                                    ^^^^^^^^^^^^^



Code/Aleo/auction
➜ leo build
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
Error [ETYC0372117]: Expected type `address` but type `string` was found.
    --> /Users/qiaopengjun/Code/Aleo/auction/src/main.leo:3:28
     |
   3 |     const OWNER: address = "aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl";
     |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^



Code/Aleo/auction
➜ leo build
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
       Leo     4 statements before dead code elimination.
       Leo     4 statements after dead code elimination.
       Leo     The program checksum is: '[80u8, 146u8, 244u8, 43u8, 87u8, 60u8, 192u8, 185u8, 136u8, 133u8, 155u8, 208u8, 200u8, 247u8, 95u8, 67u8, 161u8, 248u8, 147u8, 34u8, 147u8, 112u8, 223u8, 98u8, 35u8, 75u8, 57u8, 133u8, 45u8, 176u8, 153u8, 28u8]'.
       Leo     Program size: 0.35 KB / 500.00 KB
       Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.

Code/Aleo/auction
➜ leo build
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
       Leo     26 statements before dead code elimination.
       Leo     25 statements after dead code elimination.
       Leo     The program checksum is: '[165u8, 198u8, 62u8, 131u8, 216u8, 177u8, 10u8, 69u8, 44u8, 130u8, 157u8, 129u8, 176u8, 122u8, 118u8, 201u8, 236u8, 79u8, 188u8, 109u8, 189u8, 220u8, 155u8, 255u8, 235u8, 228u8, 254u8, 100u8, 52u8, 181u8, 133u8, 203u8]'.
       Leo     Program size: 0.90 KB / 500.00 KB
       Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.

Code/Aleo/auction
➜ leo build
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
       Leo     26 statements before dead code elimination.
       Leo     25 statements after dead code elimination.
       Leo     The program checksum is: '[165u8, 198u8, 62u8, 131u8, 216u8, 177u8, 10u8, 69u8, 44u8, 130u8, 157u8, 129u8, 176u8, 122u8, 118u8, 201u8, 236u8, 79u8, 188u8, 109u8, 189u8, 220u8, 155u8, 255u8, 235u8, 228u8, 254u8, 100u8, 52u8, 181u8, 133u8, 203u8]'.
       Leo     Program size: 0.90 KB / 500.00 KB
       Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.

这段日志完整地记录了开发者在编译 auction.aleo 合约时,从经历语法报错到最终连续编译成功的调试全过程

🔍 编译与调试过程逐段解析

1. 第一次尝试:函数调用误用(报错)

Error [EPAR0370021]: The type of `self` has no associated function `caller` that takes 0 argument(s).
--> ... bidder: self.caller() };
  • 原因说明:开发者在代码中写成了 self.caller()(带括号的函数调用形式)。
  • 修正逻辑:在 Leo 语言中,self.caller 是一个关键字属性(存储当前调用者的地址),而不是一个需要加括号调用的函数。

2. 第二次尝试:类型不匹配(报错)

Error [ETYC0372117]: Expected type `address` but type `string` was found.
--> const OWNER: address = "aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl";
  • 原因说明:开发者给地址赋值时加上了双引号,导致编译器将其识别为了字符串(string)类型,从而引发了类型不匹配错误。
  • 修正逻辑:Leo 中的地址字面量非常特殊,不需要加双引号,直接写出 aleo1... 即可被识别为 address 类型。

3. 第三次尝试:修正后初次编译成功

Leo 🔨 Compiling 'auction.aleo'
Leo     4 statements before dead code elimination.
...
Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
  • 运行状态:开发者移除了双引号和括号,代码通过了类型检查。此时合约中可能仅包含了极简逻辑(只有 4 条语句),顺利编译出了第一版 Aleo 字节码并生成了 ABI 文件。

4. 第四、五次尝试:代码补全并稳定编译

Leo     26 statements before dead code elimination.
Leo     25 statements after dead code elimination.
Leo     The program checksum is: '[165u8, 198u8, ...]'.
Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
  • 运行状态:开发者随后补全了整个拍卖合约的全部逻辑(包括 place_bidresolvefinish 三个函数),语句数量增加到了 26 条。
  • 优化与重复验证:编译器通过“死代码消除”将语句精简到了 25 条,生成了最终的程序校验和(以 165u8 开头),程序体积锁定在 0.90 KB。最后一次重复运行 leo build 结果完全一致,表明合约已经完全写好,处于稳定、随时可部署的状态。

执行 auction 合约中 place_bid(参与竞价)函数

Code/Aleo/auction
➜ leo run place_bid 10u64
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
       Leo     26 statements before dead code elimination.
       Leo     25 statements after dead code elimination.
       Leo     The program checksum is: '[165u8, 198u8, 62u8, 131u8, 216u8, 177u8, 10u8, 69u8, 44u8, 130u8, 157u8, 129u8, 176u8, 122u8, 118u8, 201u8, 236u8, 79u8, 188u8, 109u8, 189u8, 220u8, 155u8, 255u8, 235u8, 228u8, 254u8, 100u8, 52u8, 181u8, 133u8, 203u8]'.
       Leo     Program size: 0.90 KB / 500.00 KB
       Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No valid private key specified, defaulting to 'APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH'.

➕Adding programs to the VM in the following order:
  - auction.aleo (local)

➡️  Output

 • {
  owner: aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl.private,
  amount: 10u64.private,
  bidder: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px.private,
  _nonce: 4066524231836790570235444141258707019509242910964260933586241985977401917635group.public,
  _version: 1u8.public
}

这段运行结果展示了在本地执行 auction 合约中 place_bid(参与竞价)函数的输出产物,它非常典型地体现了 Aleo 的底层隐私记录(Record)数据结构

当执行 leo run place_bid 10u64 后,虚拟机根据合约逻辑创建并输出了一张专属的 Bid 记录对象。值得注意的是,该记录中关键的业务数据(如接收竞标的拍卖方 owner、出价金额 amount 以及当前调用命令的出价人 bidder)尾部全部带有 .private 后缀,这意味着这些隐私状态在 Aleo 网络中是以加密(零知识证明)的形式存在的,外界无法直接窥探。同时,记录中还自动包含了由系统生成的 _nonce 唯一哈希值(用于防止双花和标识该加密数据)以及当前数据版本的 _version,这两个底层系统字段则标记为 .public(公开),从而在保障出价人和金额绝对隐私的前提下,允许链上虚拟机对其合法性进行验证。

第二位竞标者参与拍卖(place_bid

Code/Aleo/auction
➜ leo account new

  Private Key  APrivateKey1zkp83cyhG9C3bJYuAs8eaL4bo2mWopibmKz4Xapyw8DoR93
     View Key  AViewKey1oDLNZncHk3Wqz3erEc8Fx9DiHqTNP6MtAkiPscMHCB8E
      Address  aleo1mtradsy9u8z04p05xgu6alcjdycw54e55ruejmdkj35xeqjq0gyqg5j58f


Code/Aleo/auction
➜ leo run place_bid 15u64 --private-key APrivateKey1zkp83cyhG9C3bJYuAs8eaL4bo2mWopibmKz4Xapyw8DoR93
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
       Leo     26 statements before dead code elimination.
       Leo     25 statements after dead code elimination.
       Leo     The program checksum is: '[165u8, 198u8, 62u8, 131u8, 216u8, 177u8, 10u8, 69u8, 44u8, 130u8, 157u8, 129u8, 176u8, 122u8, 118u8, 201u8, 236u8, 79u8, 188u8, 109u8, 189u8, 220u8, 155u8, 255u8, 235u8, 228u8, 254u8, 100u8, 52u8, 181u8, 133u8, 203u8]'.
       Leo     Program size: 0.90 KB / 500.00 KB
       Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.
⚠️ No network specified, defaulting to 'testnet'.

➕Adding programs to the VM in the following order:
  - auction.aleo (local)

➡️  Output

 • {
  owner: aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl.private,
  amount: 15u64.private,
  bidder: aleo1mtradsy9u8z04p05xgu6alcjdycw54e55ruejmdkj35xeqjq0gyqg5j58f.private,
  _nonce: 1073002290077143570857620859491159498761487034777575286009652771658385737276group.public,
  _version: 1u8.public
}

这段运行结果展示了在 Leo 中通过显式指定新创建的私钥来模拟第二位竞标者参与拍卖(place_bid)的完整过程。

开发者首先通过 leo account new 生成了一个全新的 Aleo 账户,其钱包地址为以 aleo1mtra... 开头的竞标者。随后,在执行 leo run place_bid 15u64 时,通过尾部添加 --private-key 参数硬编码传入了该新账户的私钥;这个操作改变了虚拟机的当前调用者(self.caller)身份,成功替代了系统默认的测试密钥。执行完成后,AVM 虚拟机输出了一张金额为 15u64.private 的全新加密 Bid 记录,可以看到该记录的 bidder 字段已正确变更为这位新生成的竞标者地址(aleo1mtra...private),而资产所有权 owner 依然归属于拍卖方(aleo14kdr...private),从而在链下(本地)成功模拟出了第二个真实的隐私竞价样本。

调用 resolve 函数两两裁决(对比筛选)

Code/Aleo/auction
➜ leo run resolve "{
  owner: aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl.private,
  amount: 10u64.private,
  bidder: aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px.private,
  _nonce: 4066524231836790570235444141258707019509242910964260933586241985977401917635group.public,
  _version: 1u8.public
}" "{
  owner: aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl.private,
  amount: 15u64.private,
  bidder: aleo1mtradsy9u8z04p05xgu6alcjdycw54e55ruejmdkj35xeqjq0gyqg5j58f.private,
  _nonce: 1073002290077143570857620859491159498761487034777575286009652771658385737276group.public,
  _version: 1u8.public
}" --private-key APrivateKey1zkp8hHQtE3gZwUB6n1fNCKjVeQGgj4U9HtTCXUDVKp1PGab
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
       Leo     26 statements before dead code elimination.
       Leo     25 statements after dead code elimination.
       Leo     The program checksum is: '[165u8, 198u8, 62u8, 131u8, 216u8, 177u8, 10u8, 69u8, 44u8, 130u8, 157u8, 129u8, 176u8, 122u8, 118u8, 201u8, 236u8, 79u8, 188u8, 109u8, 189u8, 220u8, 155u8, 255u8, 235u8, 228u8, 254u8, 100u8, 52u8, 181u8, 133u8, 203u8]'.
       Leo     Program size: 0.90 KB / 500.00 KB
       Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.
⚠️ No network specified, defaulting to 'testnet'.

➕Adding programs to the VM in the following order:
  - auction.aleo (local)

➡️  Output

 • {
  owner: aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl.private,
  amount: 15u64.private,
  bidder: aleo1mtradsy9u8z04p05xgu6alcjdycw54e55ruejmdkj35xeqjq0gyqg5j58f.private,
  _nonce: 1873350360997221443614903466027565592528458191483749408444337895833533699519group.public,
  _version: 1u8.public
}

这段运行结果展示了拍卖方(OWNER)通过显式指定其私钥,在本地调用 resolve 函数对前两次生成的两张加密 Bid 记录进行两两裁决(对比筛选)的过程。

在命令中,开发者将第一笔出价 10u64 和第二笔出价 15u64 的完整 Record 结构体作为两个输入参数传给函数,并附加了拍卖方本人的私钥(APrivateKey1zkp8hHQt...)以获取这两张记录的合法控制权。AVM 虚拟机在执行时,由于两张记录的 owner 均属于该拍卖方,因此能够成功解密并运行合约内部的条件分支:对比发现 15u64 大于 10u64,于是判定高价者胜出,并最终输出并返回了那张属于 aleo1mtra...、金额为 15u64.private 的获胜 Bid 记录(同时系统为其自动刷新生成了全新的 _nonce 标识),而低价的 10u64 记录则在此处被正式淘汰消费。

执行 finish 函数,正式结束拍卖并结算赢家凭证

Code/Aleo/auction
➜ leo run finish "{
  owner: aleo14kdr2mpk9u2ueejllu2rxg66dmgwug8um59cl0wtsfgj963pc59q2cnhyl.private,
  amount: 15u64.private,
  bidder: aleo1mtradsy9u8z04p05xgu6alcjdycw54e55ruejmdkj35xeqjq0gyqg5j58f.private,
  _nonce: 1873350360997221443614903466027565592528458191483749408444337895833533699519group.public,
  _version: 1u8.public
}" --private-key APrivateKey1zkp8hHQtE3gZwUB6n1fNCKjVeQGgj4U9HtTCXUDVKp1PGab
⚠️ No network specified, defaulting to 'testnet'.
⚠️ No endpoint specified, defaulting to 'https://api.explorer.provable.com/v1'.

       Leo 🔨 Compiling 'auction.aleo'
       Leo     26 statements before dead code elimination.
       Leo     25 statements after dead code elimination.
       Leo     The program checksum is: '[165u8, 198u8, 62u8, 131u8, 216u8, 177u8, 10u8, 69u8, 44u8, 130u8, 157u8, 129u8, 176u8, 122u8, 118u8, 201u8, 236u8, 79u8, 188u8, 109u8, 189u8, 220u8, 155u8, 255u8, 235u8, 228u8, 254u8, 100u8, 52u8, 181u8, 133u8, 203u8]'.
       Leo     Program size: 0.90 KB / 500.00 KB
       Leo ✅ Compiled 'auction.aleo' into Aleo instructions.
       Leo ✅ Generated ABI at 'build/abi.json'.
⚠️ No network specified, defaulting to 'testnet'.

➕Adding programs to the VM in the following order:
  - auction.aleo (local)

➡️  Output

 • {
  owner: aleo1mtradsy9u8z04p05xgu6alcjdycw54e55ruejmdkj35xeqjq0gyqg5j58f.private,
  amount: 15u64.private,
  isWinner: true.private,
  _nonce: 6423978589849577440127687208261664134806246974305582378718173708963212457247group.public,
  _version: 1u8.public
}

这段运行结果展示了拍卖方通过其私钥执行 finish 函数,正式结束拍卖并结算赢家凭证的最终闭环阶段。

在命令中,拍卖方将上一轮筛选出的最高价 15u64Bid 记录作为输入传入,AVM 虚拟机根据合约逻辑对其进行转换,最终输出了一张全新的 WinningBid(获胜凭证)记录。对比输入的 Bid 记录可以发现,这笔加密资产的 owner(所有权)已经由拍卖方正式移交给了中标者本人aleo1mtra...private),同时其业务字段中被赋予了全新的隐私属性 isWinner: true.private,这意味着中标者现在在链下私密地拥有了这笔获胜资产和中标身份,拍卖会至此宣告圆满结束。

程序升级与动态分派

让隐私应用具备“可进化性”与“模块化组合”的进阶能力


程序升级

  • 是否可变
program noupgrade_example.aleo {
    // 它是不可变的,并防止任何未来的升级。
    @noupgrade
    constructor() { // Leo 编译器会自动生成构造函数逻辑。
}

指定管理员升级程序

  • 谁有权升级
program admin_example.aleo {
  // 确保只有指定的管理员才能升级该程序。
 @admin(address="aleo1rhgdu77hgyqd8xjj8ucu3jj9r2p31am3tc3h0nwv2d3k0rp2ca5sqsech7")
 constructor() {}   // Leo 编译器会自动生成构造函数逻辑。
}

校验和模式 / 多签升级

program vote_example.aleo {
  // 该构造函数用于“校验和”模式。
 @checksum(mapping="basic_voting.aleo::approved_checksum", key="true")
 constructor() {}   // Leo 编译器会自动生成构造函数逻辑。
}
  • mapping:指向另一个程序(basic_voting.aleo)中的 approved_checksum 映射
  • key:查询时使用的键(这里是 "true"
  • 当需要升级时,系统会检查该 mapping 中存储的校验和是否与新版程序匹配
  • 通常用于去中心化治理场景,比如 DAO 投票通过后才允许升级

三种升级方式对比

注解升级控制说明
@noupgrade不可升级程序一旦部署,永久固定,任何人都无法修改
@adminAddress单管理员升级只有指定的 Aleo 地址可以授权升级
@checksum校验和模式 / 多签升级通过链上 mapping 存储的校验和来决定是否允许升级(通常配合 DAO 或投票机制)

自定义升级逻辑

program timelock_example.aleo {
    @custom
    constructor() {
        // 对于升级 (edition > 0),要求在构造函数可成功调用时必须满足区块高度条件
        if self.edition > 0u16 {
            assert(block.height >= 1300u32);
        }
    }
}

timelock_example.aleo 解释

这是一个时间锁升级模式:

要素说明
@custom自定义注解,表示使用非标准升级逻辑
self.edition程序的版本号(首次部署为 0,第一次升级为 1,以此类推)
block.height当前区块高度
assert(block.height >= 1300u32)只有当区块高度 ≥ 1300 时才能成功调用构造函数

工作原理

  1. 首次部署edition = 0):条件不触发,直接部署成功
  2. 第一次升级edition = 1):需要等到区块高度达到 1300 后才能升级
  3. 后续升级edition > 0):同样需要满足区块高度条件

典型应用场景

  • 时间锁保护:给社区预留时间审查新版本代码
  • 协调升级:确保所有节点在某个时间点后统一升级
  • 防止抢跑:升级不能立即生效,有缓冲期

四种升级模式总结

注解/模式升级控制方式适用场景
@noupgrade不可升级稳定合约、无需变更的逻辑
@adminAddress单地址管理员中心化应用、团队控制
@checksum链上校验和多签/DAO去中心化治理、投票升级
时间锁(@custom + block.height时间/区块约束延迟升级、社区审查缓冲

注意:constructor 是定义程序可升级的逻辑是什么,且这个逻辑本身是不可升级的永久的,在部署前要确保逻辑正确。


程序升级相关内置操作数

OperandLeo TypeDescription
self.editionu16程序的版本号。起始值为 0,每次升级时会自动增加 1。版本信息由网络自动跟踪。
self.program_owneraddress提交部署交易的地址。
self.checksum[u8; 32]程序的校验和,是程序代码的唯一标识符。

三个内置操作数的用途

操作数类型说明典型用途
self.editionu16程序版本号,从 0 开始,每次升级 +1版本控制、时间锁(如只有 edition > 0 时检查条件)
self.program_owneraddress最初部署该程序的地址管理员验证,如 assert(self.program_owner == caller)
self.checksum[u8; 32]程序代码的哈希标识符校验和升级模式,验证链上批准的代码哈希

示例:组合使用

program upgrade_example.aleo {
    @adminAddress(owner)  // 这里只能用外部指定的管理员地址
    constructor() {
        // 检查是否是原部署者
        assert(self.program_owner == aleo1...);

        // 只有版本 1 及以上才检查某个条件
        if self.edition > 0u16 {
            assert(block.height >= 1000u32);
        }

        // 校验和记录(可用于校验和模式)
        let current_checksum: [u8; 32] = self.checksum;
    }
}

这些内置操作数让你可以在构造函数中编写自定义升级逻辑,而不仅仅依赖内置注解。

动态分派 - 程序接口

代码示例

interface Transfer {
  record Token {
    owner: address,
    balance: u64,
    ..
    }

    fn transfer(input: Token, to: address, amount: u64) -> Token;
}

interface Pausable {
    mapping paused: address => bool;
    fn pause() -> (bool, Final);
}

// my_token.aleo 必须同时满足 Transfer 和 Pausable
program my_token.aleo : Transfer + Pausable {
    mapping paused: address => bool;
    record Token {
        owner: address,
        balance: u64
    }

    fn transfer(input: Token, to: address, amount: u64) -> Token {
        return Token { owner: to, balance: input.balance - amount };
    }

    fn pause() -> (bool, Final) {
        return (true, final {
            Mapping::set(paused, self.caller, true);
        });
    }
}

也就是说我的合约或者是我的程序


动态分派 - 程序接口 解释

这是 Aleo 中的 接口约束 机制:

概念说明
interface定义一组必须实现的方法和记录结构
program ... : Interface1 + Interface2程序必须实现所有指定的接口
动态分派可以通过接口类型引用不同程序,实现多态

代码结构解析

1. 定义接口 Transfer

interface Transfer {
    record Token { owner: address, balance: u64 }
    fn transfer(input: Token, to: address, amount: u64) -> Token;
}

2. 定义接口 Pausable

interface Pausable {
  mapping paused: address => bool;
  fn pause() -> (bool, Final);
}

3. 程序实现多个接口

program my_token.aleo : Transfer + Pausable {
    // 必须实现 Transfer 中的 record Token
    // 必须实现 transfer() 函数
    // 必须实现 pause() 函数
    // 必须包含 mapping paused
}

核心作用

  • 标准化:确保不同代币合约实现相同的函数签名
  • 可组合性:一个程序可以同时满足多个接口要求
  • 动态调用:可以通过接口类型调用不同程序的方法

这类似于其他语言中的 trait(Rust)或 interface(TypeScript/Java)概念。

静态调用

// 静态调用:被调用的程序在编译时已固定
import token_a.aleo;

program router.aleo {
    fn route_transfer_static(to: address, amount: u64) {
        return token_a.aleo::transfer(to, amount);
    }
}

就是比较简单直接的跨合约调用方式


静态调用解释

这是 Aleo 中最直接、最简单的跨合约调用方式:

特点说明
编译时确定被调用的程序在编写代码时就已固定
语法import 程序名.aleo + 程序名::函数名()
优点简单直接,类型安全,gas 较低
缺点不够灵活,无法动态切换目标程序

代码结构解析

import token_a.aleo;  // 导入目标程序

program router.aleo {
    fn route_transfer_static(to: address, amount: u64) {
        return token_a.aleo::transfer(to, amount);  // 直接调用
    }
}

静态调用 vs 动态分派

对比项静态调用动态分派(接口)
目标程序编译时固定运行时确定
灵活性
复杂度
适用场景确定性依赖可插拔模块

实际使用示例

// 假设有一个 USDC 代币合约
import usdc.aleo;

program payment_router.aleo {
    fn pay(to: address, amount: u64) {
        // 静态调用 USDC 的转账函数
        let receipt = usdc.aleo::transfer(to, amount);
    }
}

静态调用适合依赖关系明确、不需要替换的场景,比如路由固定调用某个特定的代币合约。


动态分派

L1    interface ARC20 {
L2        record Token;
L3        fn transfer_private(token: Token, to: address) -> Token;
L4    }
L5    // 动态调用:任何实现了 ARC20 接口的程序都可以被调用
program caller.aleo {
  fn main(target: identifier, token: dyn record, to: address) -> dyn record {
    return ARC20@(target)::transfer_private(token, to); // 隐式转换,返回 dyn record
  }
}

动态分派解释

与静态调用的关键区别

对比项静态调用动态分派
目标程序编译时固定运行时通过 target: identifier 参数指定
参数类型具体类型dyn record(动态记录)
返回值具体类型dyn record
语法token_a.aleo::transfer()ARC20@(target)::transfer_private()
灵活性

代码结构解析

// 1. 定义接口
interface ARC20 {
    record Token;                           // 定义记录结构
    fn transfer_private(token: Token, to: address) -> Token;
}

// 2. 动态调用程序
program caller.aleo {
    fn main(target: identifier, token: dyn record, to: address) -> dyn record {
        // 3. 通过接口 + 目标程序标识符动态调用
        return ARC20@(target)::transfer_private(token, to);
    }
}

关键语法点

语法含义
target: identifier运行时传入的程序名称(如 "token_a.aleo"
dyn record动态记录类型,可接受任何实现了接口的记录
ARC20@(target)通过接口名称 + 目标程序标识符调用
-> dyn record返回值也是动态类型

使用场景

  • 路由器/聚合器:用户指定要使用哪个代币合约
  • 插件化架构:主程序不知道具体调用哪个子合约
  • 可升级组件:通过更换目标程序实现逻辑升级

简单来说:静态调用是“我知道我调用谁“,动态分派是“让调用者告诉我调用谁“。


动态分派

program logger.aleo {
    storage counter: u64;
    storage entries: [u64];
}

import logger.aleo;

program reader.aleo {
    fn main(target: field, i: u32) -> Final {
        return final { do(target, i); };
    }
    final fn do(target: field, i: u32) {
        let n: u32 = logger.aleo::Logger@(target)::entries.len();
        let entry: u64 = logger.aleo::Logger@(target)::entries.get(i);
        let counter: u64 = logger.aleo::Logger@(target)::counter;
    }
}

除了调用外部函数,读取外部合约的链上状态


动态分派 - 访问外部 Storage

动态分派不仅用于调用函数,还可以访问其他程序的 storage 变量

代码结构解析

logger.aleo - 数据提供方

program logger.aleo {
    storage counter: u64;      // 存储一个计数器
    storage entries: [u64];    // 存储一个数组
}

reader.aleo - 数据读取方

import logger.aleo;

program reader.aleo {
    fn main(target: field, i: u32) -> Final {
        return final { do(target, i); };
    }
    final fn do(target: field, i: u32) {
        // 动态读取其他程序的 storage 变量
        let n: u32 = logger.aleo::Logger@(target)::entries.len();
        let entry: u64 = logger.aleo::Logger@(target)::entries.get(i);
        let counter: u64 = logger.aleo::Logger@(target)::counter;
    }
}

动态分派的两种能力

能力语法示例说明
调用函数ARC20@(target)::transfer(...)动态调用外部函数
读取 storagelogger.aleo::Logger@(target)::counter动态读取外部存储变量

语法拆解

logger.aleo :: Logger @ (target) :: counter
    ↑           ↑        ↑         ↑
   程序名     接口名   目标标识符  变量名

关键点

要点说明
target: field运行时指定的目标程序标识符
Logger接口名称,目标程序必须实现该接口
可以读 storage不需要函数,直接访问存储变量
不能写 storage外部程序的 storage 只读,不能修改

典型应用场景

  • 跨合约查询:读取其他程序的状态数据
  • 数据聚合器:从多个合约汇总信息
  • 预言机:读取多个数据源的状态

总结:动态分派 = 动态调用函数 + 动态读取 storage

思考

Q1. Leo 中的 “Private by Default”(默认隐私)语义是什么?

在 Leo 语言中,“默认隐私”的语义是:所有变量、函数输入和输出在默认情况下都是私有的,计算完全在本地(链下)闭门进行。

链上节点不会看到你的明文数据或执行过程,它们只负责接收并验证你本地生成的零知识证明(ZK Proof)。

Q2. Tuple 包含 array structs 的示例,以及如何访问 struct 中的元素

在 Leo 语言中,一个包含结构体数组(Array of Structs)的元组(Tuple)示例如下:

1. 定义与初始化

// 定义一个基础结构体
struct Point {
    x: u32,
    y: u32,
}

function test_tuple() {
    // 初始化一个元组
    // 第一个元素是一个长度为 2 的 Point 结构体数组
    // 第二个元素是一个普通的 u32 整数
    let my_tuple: ([Point; 2], u32) = (
        [
            Point { x: 10u32, y: 20u32 },
            Point { x: 30u32, y: 40u32 }
        ],
        99u32
    );
}

2. 如何访问其中的元素

在 Leo 中,不能在一行代码里进行连续的多级深度点选或索引(例如直接写 let val = my_tuple.0[0].x; 会引发编译器解析错误)。

你必须使用解构(Destructuring)**或者**中间变量来安全地逐层拆解访问:

方法 A:解构赋值(最推荐,标准的 Leo 写法)

// 1. 将元组解构,直接提取出里面的数组
let (my_array, _): ([Point; 2], u32) = my_tuple;

// 2. 从数组中获取特定索引的结构体
let first_point: Point = my_array[0u8];

// 3. 访问结构体内部的具体字段
let point_x: u32 = first_point.x; // 返回 10u32

方法 B:使用中间变量提取

// 1. 先用点运算符将整个数组提取到中间变量
let arr_copy: [Point; 2] = my_tuple.0;

// 2. 再通过索引和点运算符访问字段
let point_y: u32 = arr_copy[1u8].y; // 返回 40u32

Q3. Aleo record 中 owner 字段的作用是什么?

在 Aleo 的 Record 模型中,owner 字段的作用是显式定义该加密记录的绝对所有权

具体表现为以下两点:

  1. 访问与解密权限:所有的 Record 数据在链上默认都是加密的。只有 owner 字段指定的 address(地址)所对应的私钥(PrivateKey)持有人,才能在本地解密并查看该 Record 的明文数据(如余额、属性等)。
  2. 消费与状态转换权限:当需要花费或转换一个 Record 的状态时(例如转账),只有该 Record 的 owner 才能对交易进行本地授权并生成零知识证明(ZK Proof)。非 owner 地址试图消耗该 Record 时,在本地或链上共识阶段均无法通过验证。

Q4. 程序中的 final 是什么?

在 Leo 语言中,final 是用于实现链上状态持久化(更新 Ledger)的延迟执行机制

由于 Leo 采用混合虚拟机架构,程序的执行分为两个阶段:

  1. 链下(Off-chain)隐私阶段:代码在用户本地设备上默默执行(transition),数据是隐私的,并在这里生成零知识证明(ZK Proof)。此时无法修改链上的全局状态(比如 mapping)。
  2. 链上(On-chain)公开阶段:当本地计算完成后,通过 return final 将需要公开修改状态的代码块“递交”给链上的 Final 节点。

核心作用:

当且仅当本地提交的 ZK Proof 在链上通过验证后,Final 节点才会在公链上真正执行 final 块内部的代码,去安全地更新链上状态(如修改 mapping 或读写 storage),从而完成数据的链上持久化。

Q5. 如何创建 helper functions(辅助函数)?

在 Leo 语言中,创建 helper functions(辅助函数) 只需要使用 fn 关键字在程序中定义即可。

1. 语法示例

辅助函数可以定义在 program 块的内部或外部。

// 1. 定义辅助函数(计算 x - y)
fn foo(x: u64, y: u64) -> u64 {
    return x - y;
}

program hello.aleo {
    // 2. 在 transition(主入口)中调用它
    fn main(public x: u64, y: u64) -> private u64 {
        let z: u64 = foo(x, y); // 直接调用
        return z;
    }
}

2. 核心规则与限制

编写辅助函数时,必须遵守以下几个冷酷的硬性规定:

  • 不能包含 Transition 逻辑:辅助函数只能用于纯粹的数值计算、算法逻辑或结构体(Struct)处理。
  • 不能读写链上状态:在 fn 内部不能操作 mappingstorage,这些活只能由 final fn 来干。
  • 不能产生/消耗 Record:由于辅助函数在编译时会被完全内联展开(Inline)到调用它的环境中,它本身没有生成全新资产记录(Record)或花费资产的权限。

Q6. helper functions 能否创建 records?

不能。

在 Leo 语言中,辅助函数(fn绝对无法创建或返回 record

为什么不行?

  1. 编译机制(Inlining):辅助函数在编译时会被完全内联展开到调用它的主程序中。它只负责纯粹的数值计算、算法逻辑或 struct 结构体处理,没有独立的零知识证明(ZK)电路上下文来承载状态资产。
  2. 权限限制:在 Leo 的资产模型中,record 代表的是链上的加密隐私资产状态。只有 transition(以及被其调用的构造函数)才拥有创建、销毁或修改 record 的特权

如果你强行在 fn 里尝试初始化或返回一个 record,Leo 编译器会直接无情地报错。

Q7. constructor 的目的是什么?

用来定义程序是否可升级,并规定该程序后续升级时必须遵守的验证逻辑与约束条件。

Q8. 如何组合多个 interfaces(接口)?

在声明 program 时,使用冒号(:加号(+)将多个定义好的接口拼接起来即可。

示例语法:

program my_token.aleo : Transfer + Pausable {
    // 程序内部必须完整实现 Transfer 和 Pausable 接口中要求的所有 Record、Mapping 和函数
}

Q9. record interface 中********..********的含义是什么?

record interface 中,.. 的含义是通配符/匿名省略

它表示该接口只对业务核心字段(如 owneramount)进行硬性约束;而在具体的 program 实现中,该 Record 允许包含其他任意数量的自定义额外字段,而不会破坏接口的兼容性。

Q10. 何时使用 dyn record(动态 record)?

动态分派(Dynamic Dispatch)场景下,当程序需要在运行时动态调用外部合约,并且需要接收或处理外部合约传来的隐私资产记录时,必须使用 dyn record(动态 record)。

它充当了一个泛型/通配符资产类型,允许你的函数在编译时不绑定具体的合约,就能安全地接收并处理任何实现了指定接口约束的加密 Record。

典型场景:

  • 编写聚合器或路由器(如 DEX 路由、跨币种转账),需要处理用户指定的目标代币记录。
  • 主程序不知道未来会调用哪个子合约,通过 dyn record 保持对底层隐私状态的兼容。

Q11. storage vector 支持的核心操作有哪些?

在 Leo 语言中,storage vector(链上向量)支持以下 6 个核心操作:

  • .push(value):在向量的末尾添加一个新元素。
  • .pop():弹出并返回向量的最后一个元素。
  • .get(index):获取指定索引处的元素。
  • .set(index, value):将指定索引处的元素修改为新值。
  • .len():返回当前向量中元素的总数量。
  • .swap_remove(index):从向量中移除指定索引的元素并返回它。为了保持内存紧凑,被移除的元素会被向量中的最后一个元素直接替换补位。

总结

总体而言,Leo 语言不仅是一门智能合约开发工具,更是 Aleo 实现“链下隐私计算、链上公开验证”这一宏大愿景的核心抓手。

通过本文的梳理我们可以看到,Leo 在语法上尽力贴近现代编程语言(如 Rust),同时又为了适应零知识证明电路的严苛环境,引入了常量泛型、死代码消除以及严格的“链下计算/链上 Final 更新”分离机制。从简单的状态管理,到复杂的“私密盲拍”实战闭环,Leo 为构建无需信任且绝对保护用户隐私的 DApp 提供了强大的基础设施。

零知识证明正在从晦涩的密码学论文走向真实的商业代码落地。掌握 Leo 语言及 Record 状态流转模型,不仅是熟悉一条公链的开发,更是提前适应下一代 Web3“隐私与可组合性并存”范式的重要一步。代码已备好,随时可以开始你的构建。

参考