给 Solidity 开发者的 Cairo 编程指南

给 Solidity 开发者的 Cairo 编程指南

原文:Moving form Solidity to Cairo
翻译及校对:「StarkNet 中文社区」

概要

  • Starknet 影响力日益扩大,开发者们也在不断拓展技能,包括学习 Cairo 语言。

  • Solidity 实现了全球互操作性的智能合约,但需要大量的重复计算。

  • Cairo 高效之处在于其具备在单台机器上生成可验证交易执行证明的能力。

  • Cairo 和 Solidity 都是智能合约语言,但其功能和原理并不相同。

介绍

随着 Starknet 发展(以及行业对 Rollup 的持续关注),Cairo 已成为 Web3 中最炙手可热的编程语言。
Cairo 被用于在 Starknet 和 StarkEx 上编写智能合约,以及扩展 dYdX 和 ImmutableX 这些应用程序。在过去两年内,Cairo 全职开发者的数量增长了 875%,年增长率达 83%。鉴于 Cairo 的使用场景不仅限于区块链,任何需要计算证明的都可以用到 Cairo,所以预计开发者对 Cairo 的采用将持续上升。
许多刚开始使用 Cairo 的开发者是从 Solidity 转过来的(Solidity 是一种用于以太坊 L1 上编写智能合约的语言)。如果你正好也是其中一员,StarkWare 团队随时为您提供帮助!
在本文中,将为你提供从 Solidity 迁移到 Cairo 的开发者指南。本文会讨论 Cairo 的特点,从技术角度解析 Cairo 的工作原理,并通过一些代码示例来列举两种语言之间的主要区别。

Cairo 是什么?

Cairo 是一种受 Rust 启发、为 Starknet 设计的原生智能合约语言。
根据 Cairo 的文档:「Cairo 是第一个用于为通用计算生成 STARK 可证明程序的图灵完备语言」,以下是对这个定义的解析:
图灵完备 意味着这种编程语言能够模拟一台图灵完备的机器。图灵完备(以艾伦·图灵命名)的机器是一种计算机,只要有足够的时间和资源,它可以进行任何计算,执行任何算法。
STARK 可证明意味着使用 Cairo 编写的程序可以执行,同时可以高效地生成 STARK 证明来验证其执行。当一个 Cairo 语言编写的程序在任意机器上执行时,将生成一个新的 STARK(Scalable Transparent ARguments of Knowledge)证明,让任何独立的验证器都能简洁地验证程序是否被诚实地执行,而无需信任执行计算的机器。
Cairo 1.0 于今年春季发布(取代了 Cairo 0),并已经可以在 Starknet 主网上使用。

哪个更好 —— Cairo 还是 Solidity?

没有哪个更好!在某种程度上,这就像是比较「苹果和橙子」一样。
Solidity 是首个被广泛采用来执行可组合计算的语言(一种编程范式,其中不同的系统组件可以同步使用,并通过使用不同的组件作为乐高积木,组合成复杂的软件)。
Solidity 让智能合约能够复杂地交织在一起,这归功于以下几点:

  • 字节码公开透明。

  • 标准 ABI 结构(允许人们与外部智能合约交互)。

  • 通过使用以太坊网络,几乎可以保持 100% 的在线时间。

  • 使用以太坊网络作为执行金融交易的手段。

  • 由于有一个通用的 EVM(以太坊虚拟机),该网络上的所有验证者都持有其副本,使得全球的智能合约具备互操作性。

尽管 Solidity 已经是被采用最多的智能合约语言,并且实现了多个首创,但 Cairo 具有一个压倒性的特性:它能为通用计算创建可证明程序
这个特点使我们能够从一个系统,其中每个计算必须被重复数十万次(相当于以太坊网络中的验证器数量),转向一个新系统,在这个系统中,只需要一台被称为证明器的机器来创建交易正确执行的证明,网络上的其他人就可以验证这个证明。

上述项目仅使用了我们已知的密码证明系统的一小部分(至少在理论实现中)。

Cairo VM 与 EVM 之间的区别

为了将 Cairo 程序的执行高效地转化为 STARK 证明,Cairo 虚拟机和 EVM(以太坊虚拟机)之间有一些关键的区别:

  1. 内存模型。Cairo VM 使用一种单写内存模型,在这种模型中,内存槽一旦被写入,就不能被覆盖(这与 EVM 的内存模型不同)。尽管看起来这个模型会让编码变得非常复杂,但 Cairo 编译器解决了这个问题,抽象版本允许在代码中使用可变变量(稍后将详细介绍)。单写内存模型使 Cairo 变得更加可预测和可验证。每个内存地址只写入一次,并保持其值,直到程序的执行完成。这意味着有一个清晰、不变的计算记录,这在生成 STARK 证明时至关重要,因为它们依赖于验证计算的正确性。

  2. 域(Fields)。谈论到密码学证明,我们几乎总是谈论这些证明是为某个特定域的元素上的运算创建的。如我们在关于算术化的文章中讨论的,数学中,一个域是一个元素的集合,这些元素支持两种二进制运算:加法和乘法。它满足某些公理,如闭包、结合性、交换性,以及存在逆元和单位元。域的例子包括实数、有理数和复数。这种域的元素是可证明的通用计算中的运算对象。当讨论域时,要特别注意一些域是有限的(Cairo 使用的也是有限的)。在有限域中,元素的数量是有限的,所有元素都小于域的最高「阶」。域的「阶」等于域的元素可以达到的最高数值加一。此外,所有的加法和乘法操作都会产生一个数字,模数为最高阶。所以,例如,在一个阶为 6 的域中,加法 3+4 将得出 (3+4) mod 6 = 7 mod 6 = 1。Cairo 使用一个阶为 2²⁵¹ + 17 * 2¹⁹² + 1 的有限域。因为,在生成证明的过程中,所有的元素都需要是域元素,所以 Cairo VM 以域元素作为所有操作的基础。它并不像 EVM 那样使用常规的 uint256 或 uint32 类型。不过,可以创建抽象技术来使用更方便的 uint256 类型(或类似的)。然而,这些结构需要更多的资源(增加数十倍用于执行的操作数)。

  3. 继承和多态性。Cairo 没有继承和多态性的概念。虽然可以通过导入特定的函数和存储变量来扩展合约,但面向对象编程的一些常用概念需要一些「跳出常规」的思考。

Cairo 编译为 Sierra

需要注意的是, Cairo 1.0 编写的智能合约会首先被编译为 Sierra 代码,这段 Sierra 代码会被发布到链上。Sierra 代码是对原始 Cairo 汇编(CASM)代码的抽象,由 Cairo VM 解释。将 Cairo 代码编译为 Sierra 时,会添加一些安全措施,其中最重要的是避免 DoS 攻击的机制。
根据 Starknet 文档,「去中心化的 L2 的一个关键属性是,保证排序器所做的工作得到补偿。失败交易(reverted transactions)的概念是一个很好的例子:即使用户的交易在执行过程中失败,排序器也应该能够将其包含在一个块中,并收取到相应的执行费用」。
有时用户可能编写的代码行或包含的交易会使得执行无法证明。例如,语句 assert 1 == 0 是一个有效的 Cairo 语句,然而,无法将此执行包含在加密证明中,因为这个语句的结果是 false ,它转换为的多项式约束是不可满足的。因此 Sierra 添加了一个安全层,确保即使无法证明的失败交易也会被收费。这既降低了对排序器的 DoS 攻击的可能性,同时也满足了排序器的经济激励。

比较 Cairo 和 Solidity

现在我们已经了解了 Cairo 的基本类型、函数和结构。将这些与 Solidity 中的对应部分进行比较,以便在这两种智能合约语言之间找到一些相似之处。(需要注意的是,Cairo 不仅可以用来编写智能合约的代码,还可以用来创建可证明的程序。但这超出了我们当前讨论的范围。)
注意:从现在开始,所有关于 Cairo 的讨论都指的是 Cairo 1。不再推荐使用 Cairo 0 作为在 Starknet 上编写智能合约的首选语言。

Cairo 类型

下面是 Cairo 中一些基本类型的列表:

如上面的列表所示(我们刚刚添加了有符号整数以及 Dict.),从 Cairo 1 开始,无符号整数已经被添加到 Cairo 中,类似于它们在 Solidity 中的对应项。尽管使用整数对于排序器来说可能比直接使用 felts 的成本更高,但将整数整合到 Cairo 中将会简化开发过程。
除此之外,在 Cairo 中使用数组的语法非常类似于 Rust,而且从逻辑上讲,它们与 Solidity 中的数组很相似。

Cairo 中的函数

Cairo 中的函数可以是以下类型:

  • 内部(internal)
  • 外部(external)
  • 视图(view)
  • 构造(constructor)

默认情况下,所有的合约函数都被认为是内部的(这样的函数只能从合约内部被调用,有点像其他语言中的私有函数)。

外部函数对外界开放,可以被其他智能合约甚至帐户调用。(万岁,帐户抽象化!)
视图函数是一种只能读取链上状态的外部函数。视图函数不能修改链的状态。
构造函数是 Cairo 中一种特殊的函数属性,用于智能合约的构造器!
现在,我们来比较 Cairo 和 Solidity 在函数声明语法上的差异:

详细看一下几个关键的区别:

  1. 在 Cairo 中,声明函数的关键字是 fn,而在 Solidity 中,它是 function。
  2. 在 Cairo 中,函数类型是在函数关键字之前声明的(#[view]),与在 Solidity 中的格式是不同的(见上文)。
  3. 在 Cairo 编程语言中,使用 → 符号来表示返回值的类型。而在 Solidity 中使用 returns 。

Cairo 中的模块

在 Cairo 中,模块的作用是将相关功能组织在一个命名空间中。使用关键字 mod 来定义一个模块,然后是模块名称和包含函数以及其他声明的代码块。模块可以导入其他模块并使用它们的功能。
这与其他编程语言中的库类似,而在 Solidity 中,模块的概念可以类比为合约的继承。

例如,在上述代码中,导入了 starknet 和 array 模块。其语法与 Solidity 中的 import 语句或使用 is 关键字的继承不同(参见此处)。请注意,在 Cairo 中,use 关键字使得所有导入的模块中的函数都在导入模块中可用。

Cairo 智能合约示例

现在我们已经了解了 Cairo 中的类型和函数集合,让我们来看一个 Cairo ERC20 合约代码的例子。ERC20 合约是标准的智能合约模板。它允许用户之间转移代币,检查用户的余额,以及批准向另一个实体转账。以下是我们的合约:
注意:Cairo 1 的语法正在发生一些变化,并计划在今年(2023年)的晚些时候更新。请查阅这篇文章以获取更多详情。

#[contract]
mod ERC20 {
    // @dev library imports
    use starknet::get_caller_address;

    // @dev storage variables
    struct Storage {
        name: felt,
        symbol: felt,
        decimals: u8,
        total_supply: u256,
        balances: LegacyMap::<felt, u256>,
        allowances: LegacyMap::<(felt, felt), u256>,
    }

    // @dev emitted each time a transfer is carried out
    // @param from_ the address of the sender
    // @param to the address of the recipient
    // @param value the amount being sent
    #[event]
    fn Transfer(from_: felt, to: felt, value: u256) {}

    // @dev emitted each time an Approval operation is carried out
    // @param owner the address of the token owner
    // @param spender the address of the spender
    // @param value the amount being approved
    #[event]
    fn Approval(owner: felt, spender: felt, value: u256) {}

    // @dev intitialized on deployment
    // @param name_ the ERC20 token name
    // @param symbol_ the ERC20 token symbol
    // @param decimals_ the ERC20 token decimals
    // @param initial_supply a Uint256 representation of the token initial supply
    // @param recipient the assigned token owner
    #[constructor]
    fn constructor(
        name_: felt, symbol_: felt, decimals_: u8, initial_supply: u256, recipient: felt
    ) {
        name::write(name_);
        symbol::write(symbol_);
        decimals::write(decimals_);
        assert(recipient != 0, 'ERC20: mint to the 0 address');
        total_supply::write(initial_supply);
        balances::write(recipient, initial_supply);
        Transfer(0, recipient, initial_supply);
    }

    // @dev get name of the token
    // @return the name of the token
    #[view]
    fn get_name() -> felt {
        name::read()
    }

    // @dev get the symbol of the token
    // @return symbol of the token
    #[view]
    fn get_symbol() -> felt {
        symbol::read()
    }

    // @dev get the decimals of the token
    // @return decimal of the token
    #[view]
    fn get_decimals() -> u8 {
        decimals::read()
    }

    // @dev get the total supply of the token
    // @return total supply of the token
    #[view]
    fn get_total_supply() -> u256 {
        total_supply::read()
    }

    // @dev get the token balance of an address
    // @param account Account whose balance is to be queried
    // @return the balance of the account
    #[view]
    fn balance_of(account: felt) -> u256 {
        balances::read(account)
    }

    // @dev returns the allowance to an address
    // @param owner the account whose token is to be spent
    // @param spender the spending account
    // @return remaining the amount allowed to be spent
    #[view]
    fn allowance(owner: felt, spender: felt) -> u256 {
        allowances::read((owner, spender))
    }

    // @dev carries out ERC20 token transfer
    // @param recipient the address of the receiver
    // @param amount the Uint256 representation of the transaction amount
    #[external]
    fn transfer(recipient: felt, amount: u256) {
        let sender = get_caller_address();
        transfer_helper(sender, recipient, amount);
    }

    // @dev transfers token on behalf of another account
    // @param sender the from address
    // @param recipient the to address
    // @param amount the amount being sent
    #[external]
    fn transfer_from(sender: felt, recipient: felt, amount: u256) {
        let caller = get_caller_address();
        spend_allowance(sender, caller, amount);
        transfer_helper(sender, recipient, amount);
    }

    // @dev approves token to be spent on your behalf
    // @param spender address of the spender
    // @param amount amount being approved for spending
    #[external]
    fn approve(spender: felt, amount: u256) {
        let caller = get_caller_address();
        approve_helper(caller, spender, amount);
    }

    // @dev increase amount of allowed tokens to be spent on your behalf
    // @param spender address of the spender
    // @param added_value amount to be added
    #[external]
    fn increase_allowance(spender: felt, added_value: u256) {
        let caller = get_caller_address();
        approve_helper(caller, spender, allowances::read((caller, spender)) + added_value);
    }

    // @dev increase amount of allowed tokens to be spent on your behalf
    // @param spender address of the spender
    // @param added_value amount to be added
    #[external]
    fn decrease_allowance(spender: felt, subtracted_value: u256) {
        let caller = get_caller_address();
        approve_helper(caller, spender, allowances::read((caller, spender)) - subtracted_value);
    }

    // @dev internal function that performs the transfer logic
    // @param sender address of the sender
    // @param recipient the address of the receiver
    // @param amount the Uint256 representation of the transaction amount
    fn transfer_helper(sender: felt, recipient: felt, amount: u256) {
        assert(sender != 0, 'ERC20: transfer from 0');
        assert(recipient != 0, 'ERC20: transfer to 0');
        balances::write(sender, balances::read(sender) - amount);
        balances::write(recipient, balances::read(recipient) + amount);
        Transfer(sender, recipient, amount);
    }

    // @dev infinite allowance check
    // @param owner the address of the token owner
    // @param spender the address of the spender
    // @param amount the Uint256 representation of the approved amount
    fn spend_allowance(owner: felt, spender: felt, amount: u256) {
        let current_allowance = allowances::read((owner, spender));
        let ONES_MASK = 0xffffffffffffffffffffffffffffffff_u128;
        let is_unlimited_allowance =
            current_allowance.low == ONES_MASK & current_allowance.high == ONES_MASK;
        if !is_unlimited_allowance {
            approve_helper(owner, spender, current_allowance - amount);
        }
    }

    // @dev internal function that performs the approval logic
    // @param owner the address of the token owner
    // @param spender the address of the spender
    // @param amount the Uint256 representation of the approved amount
    fn approve_helper(owner: felt, spender: felt, amount: u256) {
        assert(spender != 0, 'ERC20: approve from 0');
        allowances::write((owner, spender), amount);
        Approval(owner, spender, amount);
    }
}

上述代码基于
https://github.com/argentlabs/starknet-build/blob/main/cairo1.0/examples/erc20/ERC20.cairo
让我们详细看一下最重要的部分。
第 1 行和第 2 行初始化智能合约并为其命名。

#[contract]  
mod ERC20 {  

第 4 行从 starknet 模块导入 get_caller_address 函数。get_caller_address() 函数返回了调用该函数的合约的地址。

use starknet::get_caller_address;  

第 7 行到第 14 行定义了一个在后面的代码中需要使用的结构(类似于 Solidity 中的 struct)。

第 20 行和第 21 行定义了一个 名为 Transfer 的事件(event),该事件在每次 ERC20 代币被转移时触发。事件为跟踪合约活动/状态改变提供了有效的方式,使外部监听器得以做出相应的反应。

#[event]  
fn Transfer(from_: felt, to: felt, value: u256) {}  

第 36 行到第 54 行包含一个构造函数(部署合约时调用的函数)和一个视图函数,该函数读取 name 存储变量并将其值返回给用户。

第 97 行到第 101 行定义了一个外部函数(如前所述),该函数用于将 ERC20 代币从一个账户转移到另一个账户。此函数内部调用了 transfer_helper() 函数,并且触发了之前定义的 Transfer() 事件。

总结

如果你熟悉 Solidity ,那么现在应该对 Starknet 的工作原理、Cairo 和 Solidity 之间的差异以及如何阅读基本的 Cairo 智能合约有了很好的了解。
要继续 Cairo 开发之旅,可以参考 Cairo 的文档,注册 Cairo Basecamp,或者参考教程。

原文地址:https://medium.com/@starkware/moving-from-solidity-to-cairo-7d44f9723c68
Mirror:https://mirror.xyz/starknet-zh.eth/TTQly2Tpd_Yk0UqmRcOjyMjVTfmY0zzBtoIM_c_FrAM

关于我们

Starknet 由 StarkWare 开发,采用 STARK 有效性证明,是为未来规模化应用而建的开放式以太坊 Layer2 网络。

「Starknet 中文」是 Starknet 社区项目,致力于 Starknet 在中文社区推广。

Twitter: https://twitter.com/StarkNet_ZH
Substack: https://starknetzh.substack.com/
Mirror: https://mirror.xyz/starknet-zh.eth
GitHub: https://github.com/StarkNet-ZH/Awesome-StarkNet-Chinese
Discord: https://discord.gg/R8A879b8x3
Telegram: https://t.me/starknet_zh

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Starknet 中文
Starknet 中文
江湖只有他的大名,没有他的介绍。