Cairo 中的整数

本文详细介绍了 Cairo 中整数的工作原理,重点介绍了与 Solidity 的关键区别,包括整数类型、溢出保护、类型转换、常量、最大最小值、字面量表示、位运算、以及特殊的 felt252 类型及其除法运算。文章还提及了 Cairo 编译器如何处理整数与 felt252 之间的转换,并建议在非必要情况下避免直接使用 felt252 以优化 Gas 消耗。

Cairo 不提供像 Solidity 那样完整的整数大小范围。虽然 Solidity 为每个 8 的倍数直到 256 位都提供了整数类型,但 Cairo 仅支持以下整数类型:

  • u8
  • u16
  • u32
  • u64
  • u128
  • u256

对于熟悉 Rust 的读者来说,usize 类型是一个 u32。

以下是一个 Cairo 合约的示例,该合约将两个 u256 数字相加:

#[starknet::interface]
pub trait IAdd<TContractState> {
    fn add(self: @TContractState, a: u256, b: u256) -> u256;
}

#[starknet::contract]
mod Add {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl AddImpl of super::IAdd<ContractState> {
        fn add(self: @ContractState, a: u256, b: u256) -> u256 {
            a + b
        }
    }
}

Cairo 还支持以下类型的有符号整数:

  • i8
  • i16
  • i32
  • i64
  • i128

请注意,不支持 i256

在本文中,我们将介绍整数在 Cairo 中的工作方式,重点介绍与 Solidity 的主要区别。我们将介绍诸如溢出行为、整数大小之间的转换以及有符号和无符号值的使用等概念。我们还将讨论 felt252,这是 Cairo 的原生字段元素,是所有数字运算的核心。

整数具有溢出和下溢保护

默认情况下,Cairo 中为整数(有符号和无符号)类型启用了溢出保护。要查看这一点,请创建一个新的 scarb 项目 scarb new integers,然后删除 ./src/lib.cairo 中的默认合约,并将其替换为以下代码:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn underflow(ref self: TContractState, x: u256, y: u256) -> u256;
}

#[starknet::contract]
mod HelloStarknet {

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn underflow(ref self: ContractState, x: u256, y: u256) -> u256 {
            x - y
        }
    }
}

删除自动创建的测试,并在下面添加以下代码。请注意,函数上方的 #[should_panic] 宏指定如果执行 panic,则测试通过。

use snforge_std::{ContractClassTrait, DeclareResultTrait, declare};
use integers::{ IHelloStarknetDispatcher, IHelloStarknetDispatcherTrait};
use starknet::ContractAddress;

fn deploy_contract(name: ByteArray) -> ContractAddress {
    let contract = declare(name).unwrap().contract_class();
    let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
    contract_address
}

#[test]
#[should_panic]
fn test_flow_protection() {
    let contract_address = deploy_contract("HelloStarknet");

    let dispatcher = IHelloStarknetDispatcher { contract_address };

    dispatcher.underflow(0, 1);
}

使用 scarb test 运行测试,并注意测试通过。

没有浮点数

与 Solidity 一样,Cairo 不支持浮点数

转换整数

有两种类型的转换:

  • 保证成功的转换和
  • 可能失败的转换。

例如,将 u8 转换为 u16 将始终成功,因为 u16 可以容纳 u8 可以容纳的任何数字。但是,从 u16 转换为 u8 可能会失败,因为某些最高有效位可能会被截断。

如果 i16 包含负值,则从 i16 转换为 u16 可能会失败。如果 u16 中保存的数字太大,则从 u16 转换为 i16 可能会失败——与有符号整数的位大小相同的无符号整数可以容纳比有符号整数更大的正数。

始终成功的转换

转换为更大的类型将始终成功,因为目标类型可以容纳来自源类型的任何值。

始终成功的转换示例:

  • u8u16u32u64u128u256
  • u16u32u64u128u256
  • i8i16i32i64i128
  • i16i32i64i128

要执行始终成功的转换,请使用 .into()

let small: u8 = 7;
let large: u16 = small.into();  // Always succeeds - u16 can hold any u8 value

可能失败的转换

与 Solidity 在将较大数字转换为较小类型时会静默截断最高有效位不同,如果无法安全执行转换,Cairo 将会 panic

这是可能失败的转换的代码片段:

// may fail if value is too large
let large: u16 = 300;
let small: u8 = large.try_into().unwrap();  // Panics! 300 > 255

请注意,如果你尝试在转换可能失败的情况下(从大类型转换为小类型)使用 into() 转换,则代码将无法编译。

检测转换是否会失败

当使用 try_into() 在整数类型之间进行转换时,转换会返回一个 Option。这使我们可以在使用结果之前安全地检查转换是否成功。一种常见且惯用的方法是使用 if let 或检查 .is_some()

// Value 300 cannot fit into u8 (max 255), so try_into returns None
let value: u16 = 300;
let result_option: Option<u8> = value.try_into();

if result_option.is_some() {
    // cast succeeded
} else {
    // cast failed
}

使用 if let

if let Some(result) = result_option {
    // cast succeeded, use `result`
} else {
    // cast failed
}

常量

Cairo 中的常量是在编译时已知且在运行时无法更改的值。它们使用 const 关键字在 mod 块内声明,并且必须显式指定其类型,如下所示:

const <*variable_name*>: <variable_*type*> = <*value*>;

以下是如何在 Cairo 中声明和使用常量:

#[starknet::contract]
mod HelloStarknet {
        // DECLARE CONSTANTS
    const num_one: u256 = 1;
    const num_two: i8 = -2;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn get_one(self: @ContractState) -> u256 {
                // USE CONSTANTS
            num_one
        }

        fn get_two(self: @ContractState) -> i8 {
                // USE CONSTANTS
            num_two
        }
    }
}

常量 vs 不可变值

与 Solidity 不同,Cairo 没有单独的 immutable 关键字。需要在合约部署期间设置一次但编译时未知的值应存储在合约存储中并在构造函数中设置。

最大整数大小

在 Solidity 中,type(uint256).max 用于获取整数的最大大小。在 Cairo 中,我们使用 let max_u256: u256 = Bounded::MAX,如下所示:

#[starknet::contract]
mod HelloStarknet {
    use core::num::traits::{Bounded}; // Bounded is how we get the max

    #[storage]
    struct Storage {} // unusued

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn max_demo(ref self: ContractState) -> u256 {
          let max_u256: u256 = Bounded::MAX;
          max_u256
        }
    }
}

大多数时候,Cairo 能够自行确定类型,但是在使用 Bounded::MAX 时,编译器不会自动知道你需要哪个整数类型的最大值。因此,变量需要显式类型注释,即 : 之后的 u256,例如 let max_u256: u256 = Bounded::MAX;

最小整数大小

正如我们可以获取整数类型的最大值一样,Cairo 还通过 Bounded 特性提供对其最小值的访问,使用 Bounded::MIN,如下所示:

#[starknet::contract]
mod HelloStarknet {
    use core::num::traits::{Bounded}; // Bounded provides both MIN and MAX

    #[storage]
    struct Storage {} // unused

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn min_demo(ref self: ContractState) -> (u256, i128) {
                // This will be 0 for unsigned types
            let min_u256: u256 = Bounded::MIN;

                        // This will be the most negative value
            let min_i128: i128 = Bounded::MIN;

            (min_u256, min_i128)
        }
    }
}

了解最小值大小

对于无符号整数类型(u8、u16、u32、u64、u128、u256),最小值始终为 0

let min_u8: u8 = Bounded::MIN;     // 0
let min_u16: u16 = Bounded::MIN;   // 0
let min_u32: u32 = Bounded::MIN;   // 0
let min_u64: u64 = Bounded::MIN;   // 0
let min_u128: u128 = Bounded::MIN; // 0
let min_u256: u256 = Bounded::MIN; // 0

对于有符号整数类型(i8、i16、i32、i64、i128),最小值是可以表示的最小负数:

let min_i8: i8 = Bounded::MIN;     // -128
let min_i16: i16 = Bounded::MIN;   // -32,768
let min_i32: i32 = Bounded::MIN;   // -2,147,483,648
let min_i64: i64 = Bounded::MIN;   // -9,223,372,036,854,775,808
let min_i128: i128 = Bounded::MIN; // a very large negative value

类型注释要求

Bounded::MAX 一样,编译器在使用 Bounded::MIN 时也无法自动猜测类型,因此需要显式类型注释:

// This won't compile - ambiguous type ❌
let min_val = Bounded::MIN;

// This will compile - explicit type annotation ✅
let min_val: u64 = Bounded::MIN;

用于在整数字面量上指定类型的简写

如果我们将一个固定值分配给一个整数,我们可以显式指定整数的类型,或者允许编译器推断该类型。

指定类型:

// first way
let x: i32 = 10;

// second way
let y = 10_i32;

允许编译器推断类型:

如果我们不指定类型,编译器将尝试从上下文中推断它。例如,以下函数返回一个 u32,因此 10 的类型为 u32

fn hello_world() -> u32 {
        let x = 10;
        x
}

有符号整数除法溢出

在 Solidity 中,有符号整数除法存在一个特定的边缘情况,可能会导致意外行为。考虑以下 Solidity 合约:

contract D {
    function div(int8 a, int8 b) public pure returns (int8 c) {
        c = a / b;
    }
}

当你将最小负值除以 -1 时,会出现问题。对于 int8,范围是 -128 到 127。当你执行 -128 / -1 时,从数学上讲,结果应该是 128,但是 128 无法容纳在 int8 中(其最大值为 127)。这会导致溢出。

在 Solidity 中,此操作要么:

  • 环绕到意外值
  • 恢复(在具有溢出保护的较新版本中)

Cairo 如何处理整数除法溢出

就像在 Solidity 版本 ≥ 0.8 中一样,Cairo 提供了内置的溢出保护。如果某个操作会导致溢出,程序将在运行时 panic,从而防止意外行为。

#[starknet::contract]
mod Div {
    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl DivImpl of super::IDiv<ContractState> {
        fn div(self: @ContractState, a: i8, b: i8) -> i8 {
            // This will panic if `a` is -128 and `b` is -1
            a / b
        }
    }
}

为了防止有符号除法溢出导致 panic,我们需要在执行操作之前手动检查条件,如下所示:

#[starknet::contract]
mod Div {
    use core::num::traits::Bounded;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl DivImpl of super::IDiv<ContractState> {
        fn div(self: @ContractState, a: i8, b: i8) -> i8 {
            if b == 0 {
                // Division by zero
            } else if a == Bounded::<i8>::MIN && b == -1 {
                // Overflow case
            } else {
                a / b
            }
        }
    }
}

向上转换失败

此 Solidity 函数看起来很安全,但会产生意外结果:

function mul(uint8 a, uint8 b) public pure returns (uint256 c) {
    c = a * b;
}

问题在于乘法 a * b 首先在 uint8 算术中进行,然后将结果转换为 uint256。如果 a * b 溢出 uint8 范围 (0-255),则乘法在向上转换之前会环绕。

例如:

  • mul(200, 200) 在数学上应返回 40000
  • 但是 200 * 200 = 40000 溢出 uint8(最大值 255)
  • uint8 中的环绕结果将为 40000 % 256 = 64
  • 然后 64 被转换为 uint256,返回 64 而不是 40000,当然这发生在低于 0.8 的 Solidity 版本中

Cairo 中的溢出

Cairo 通过其内置的溢出保护来处理向上转换溢出问题。也就是说,如果某个操作产生的值超出允许的范围,Cairo 将引发错误并停止执行,而不是允许意外行为。

例如,如果 a * b > 255,则以下代码将 panic

// This will panic if the multiplication overflows u8
fn mul(self: @ContractState, a: u8, b: u8) -> u256 {
    let result_u8 = a * b; // Panic if a * b > 255
        result_u8.into()       // This line never executes if overflow occurs
}

安全向上转换

一种安全的方法是避免我们的 Cairo 合约由于溢出而 panic,就是在执行算术运算之前,当我们需要在更大的类型中获得结果时,先进行向上转换。例如,我们可以从 u8 转换为 u256:

// cast up before multiplication
fn safe_mul(self: @ContractState, a: u8, b: u8) -> u256 {
    let a_wide: u256 = a.into();
    let b_wide: u256 = b.into();
    a_wide * b_wide // No overflow possible
}

指数

在 Solidity 中,指数的语法是 b ** e,其中 b 是底数,e 是指数。

在 Cairo 中,你必须使用 use core::num::traits::Pow; 导入 Pow。然后,你可以使用 b.pow(e) 将一个整数提高到某个幂。


#[starknet::contract]
mod HelloStarknet {

      use core::num::traits::Pow; // THIS IMPORT IS REQUIRED

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn upcast_demo(ref self: ContractState, x: u256, y: u32) -> u256 {
                  x.pow(y) // compute exponent
        }
    }
}

.pow() 方法返回与底数类型相同的值,因此这里的 x.pow(y) 生成一个类型为 (u256) 的值。

重要提示: 指数必须是 u32 类型(或 usize,它在 Cairo 中是 u32)。如果使用另一种整数类型,则代码将无法编译。

字面量中的下划线

与 Solidity 一样,Cairo 中的大数字可以用下划线分隔,以使其更易于阅读:

// valid Cairo
let basis_points = 10_000;

科学计数法简写

在 Solidity 中,10 的幂可以用科学计数法编写,例如 10e18。Cairo 不支持这种写法。要在 Cairo 中编写 10e18,请使用 Pow 特性,如下所示:

use core::num::traits::Pow;

// ...

let num = 10_u256.pow(18_u32);

请记住,指数必须是 u32 类型。

位运算、移位运算和比较

位运算

Cairo 支持整数类型的标准位运算:

按位与 (&):

let a: u8 = 0b1100; // 12 in decimal
let b: u8 = 0b1010; // 10 in decimal
let result = a & b; // 0b1100 & 0b1010 = 0b1000 => 8

按位或 (|):

let a: u8 = 0b1100; // 12 in decimal
let b: u8 = 0b1010; // 10 in decimal
let result = a | b; // 0b1110 = 14

按位异或 (^):

let a: u8 = 0b1100; // 12 in decimal
let b: u8 = 0b1010; // 10 in decimal
let result = a ^ b; // 0b0110 = 6

按位非 (~):

let a: u8 = 0b1100; // 12 in decimal
let result = ~a;    // 0b11110011 = 243 (inverts all bits)

移位运算

Cairo 提供左移和右移位运算:

左移 (<<)

将位向左移动,并用零填充:

let a: u8 = 0b0001; // 1 in decimal
let result = a << 3; // 0b1000 = 8 (multiplies by 2**3)

右移 (>>)

将位向右移动:

let a: u8 = 0b1100; // 12 in decimal
let result = a >> 2; // 0b0011 = 3 (divides by 2**2)

比较运算

Cairo 支持所有标准比较运算符:

相等 (==!=)

let a: u32 = 10;
let b: u32 = 20;
let equal = a == b;     // false
let not_equal = a != b; // true

排序 (<<=>>=)

let a: u32 = 10;
let b: u32 = 20;
let less_than = a < b;           // true
let less_or_equal = a <= b;      // true
let greater_than = a > b;        // false
let greater_or_equal = a >= b;   // false

关于 felt252 的说明

如果你阅读较旧的生产 Cairo 代码,你会经常看到数据类型 felt252。类似于 EVM 的默认字长为 256 位,CairoVM 的默认字长略低于 252 位,或者更准确地说:3618502788666131213697322783095070105623107215331596699973092056135872020481 或 2²⁵¹+17⋅2¹⁹²+1。该数字略小于 2²⁵²。

Cairo 将 [0..2²⁵¹+17⋅2¹⁹²+1] 范围内的数字类型称为 felt252

这个大数字是一个素数,针对 Cairo 虚拟机上的零知识证明数学进行了优化。

名称 felt252 来自术语“适合 252 位的字段元素”。“字段元素”是一个数字,它存在于一个数字系统中,其中所有加法和乘法都以某个素数为模进行。

不建议在 Cairo 代码中使用 felt252,因为在以后,CairoVM 可能会将其默认字长更改为更小的值,以提高其证明交易的速度。

对于你来说,Cairo 编译器将无缝处理整数(u8u256)到 felt252 的幕后转换。值得注意的是,u256 不适合 252 位,因此在幕后,u256 实际上是两个 felt252 元素。因此,为了提高 gas 效率,最好尽可能使用 u128 或更小的整数。仅在需要进行极端优化时,使用 felt252 才有意义。 我们将在以后的教程中重新讨论 Starknet 上的 gas 成本。现在,我们建议你不要使用 felt252 类型,而只使用整数。

但是,由于你会在代码中经常看到 felt252,因此值得解释一下它的工作原理。

felt252 没有溢出和下溢保护

与 Solidity 0.8.0 或更高版本不同,Cairo 没有为 felt252 提供内置的溢出和下溢保护。为了演示这一点,请创建一个新项目 scarb new numbers。然后将 lib.cairo 中的生成的代码替换为以下代码:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn math_demo(self: @TContractState, x: felt252, y: felt252) -> felt252;
}

#[starknet::contract]
mod HelloStarknet {

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn math_demo(self: @ContractState, x: felt252, y: felt252) -> felt252 {
            x - y
        }
    }
}

按如下方式替换测试:

use starknet::ContractAddress;

use snforge_std::{declare, ContractClassTrait, DeclareResultTrait};
use numbers::IHelloStarknetDispatcher;
use numbers::IHelloStarknetDispatcherTrait;

fn deploy_contract(name: ByteArray) -> ContractAddress {
    let contract = declare(name).unwrap().contract_class();
    let (contract_address, _) = contract.deploy(@ArrayTrait::new()).unwrap();
    contract_address
}

#[test]
fn test_math_demo() {
    let contract_address = deploy_contract("HelloStarknet");
    let dispatcher = IHelloStarknetDispatcher { contract_address };
    let result = dispatcher.math_demo(0, 1); // 0 - 1
    println!("result: {}", result);
}

控制台将打印:

result: 3618502788666131213697322783095070105623107215331596699973092056135872020480

在低于 0.8.0 的 Solidity 中,假设是无符号算术,0 - 1 会导致下溢。然后,该值环绕到最大可能的 uint256 值。Cairo 中的 felt252 也会发生类似的情况,因为它没有溢出或下溢保护。所有算术运算都以字段素数(2²⁵¹ + 17 × 2¹⁹² + 1)为模进行,因此从 0 中减去 1 会返回最大的有效 felt252 值,看起来像一个很大的数字。

felt252_div

如果尝试将 felt252 除以另一个 felt252,你将收到一个编译错误。以下代码将无法编译:

fn math_demo(self: @ContractState, x: felt252, y: felt252) -> felt252 {
    x / y
}

充分 理解为什么 Cairo 不允许像这样进行除法,请观看我们关于 模算术的视频。

felt252 的正确方法

要使用 felt252 值执行除法,我们必须使用 felt252_div,它是 Cairo 核心库中的内置函数:

#[starknet::contract]
mod HelloStarknet {

        // THIS IS NEW
        use core::felt252_div;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn math_demo(self: @ContractState) -> felt252 {
            felt252_div(4, 2)
        }
    }
}

felt252_div 函数不执行常规除法。相反,它:

  1. 找到除数 y 以字段素数为模的模逆
  2. x 乘以这个逆
  3. 返回以字段素数为模的结果

数学上:

felt252_div(x, y) = x * y^(-1) mod P

其中 y^(-1)y 在有限域中的模逆。

除以零

尝试 felt252_div(x, 0) 将导致运行时 panic

// This will panic!
let result = felt252_div(42, 0);

在执行除法之前,请始终验证你的除数。felt252_div 确保 felt252 值不能为零的一种方法是使用 NonZero<felt252>

非零

felt252_div 函数要求它的第二个参数(除数)是 NonZero<felt252> 类型,而不是普通的 felt252。这可以防止在编译时进行除以零的操作。

// BE SURE TO CHANGE THE TRAIT DEFINITION ALSO

#[starknet::contract]
mod HelloStarknet {

    use core::felt252_div;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {

            // NOTE THE TYPE OF `y`
        fn underflow_demo(self: @ContractState, x: felt252, y: NonZero<felt252>) -> felt252 {
            felt252_div(x, y)
        }
    }
}

总结

Cairo 整数是安全的。所有 u*i* 类型都具有溢出保护,并且在无效操作时会 panic

转换是严格的:.into() 是安全的(在转换为更大的类型时始终成功),.try_into() 检查错误(如果目标类型无法容纳该值,则可能会panic)。

求幂使用 .pow() 通过特性导入。

位运算符和比较运算符在整数类型之间正常工作。

felt252 是 Cairo 的本机字段元素,没有像整数那样的溢出检查。felt 上的除法需要一个函数 (felt252_div) 进行零检查。

本文是 Starknet 上 Cairo 编程 教程系列的一部分

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

0 条评论

请先 登录 后评论
RareSkills
RareSkills
https://www.rareskills.io/