Starknet上的哈希函数

本文详细介绍了Starknet/Cairo中三种哈希函数:Pedersen、Poseidon和Keccak-256。

Solidity 依赖 keccak-256 作为其主要哈希函数,用于从任意数据中派生确定性标识符,例如计算函数选择器或计算映射(Mapping)的存储槽位,但 Cairo 针对不同上下文提供了独立的哈希函数。

本文涵盖了 Starknet/Cairo 中的三种哈希函数:Pedersen、Poseidon 和 Keccak-256。我们将解释如何在智能合约中使用它们以及何时使用。

为什么 Cairo 使用三种哈希函数?

像 Starknet 这样的 ZK 证明系统在有限域上运行,每个操作都表达为一个约束。约束是一个算术方程,它编码了必须满足的计算规则,以生成有效证明。一个操作所需的约束越多,证明成本就越高。

在这方面,Keccak-256 特别昂贵,因为它基于位运算(如 XOR、AND 和位旋转)构建。这些操作不能直接映射到域算术,必须使用许多约束进行模拟,这使得 Keccak-256 的证明成本远高于原生友好的域操作。

因此,Cairo 提供了三种哈希函数,每种适用于不同的场景:

  1. Keccak-256 哈希

此哈希函数包含在 Cairo 中,用于与以太坊兼容,例如计算函数选择器、复制存储布局以及验证以太坊签名。

虽然由于约束过多而不适合 STARK 证明,频繁使用成本较高,但对于跨链互操作性以及在 Cairo 中派生存储变量的基地址是必需的。

  1. Pedersen 哈希

一种基于椭圆曲线的哈希函数,定义在 Starknet 的原生域(felt252)上。它是 Starknet 上最早使用的哈希函数,至今仍用于计算存储地址(例如,Map 类型依赖 Pedersen 对存储键进行哈希)。因此,对于已经依赖它的现有合约、状态承诺或 Merkle 树,Pedersen 通常是必需的。

与 Keccak 不同,Keccak 依赖位运算,在 STARK 证明中需要大量约束,而 Pedersen 使用 Stark-curve 上的点加法和标量乘法,这使得在 Cairo 中的证明成本更低。Pedersen 输出 felt252 值。

  1. Poseidon 哈希

它直接在 Starknet 使用的素数域上操作,完全避免了椭圆曲线算术。这导致约束数量大大减少,使其比 Pedersen 和 Keccak 更便宜、更快,并且是 Cairo 中通用哈希、Merkle 树和承诺的推荐选择。它输出 felt252 值。

注意:如果需要与遗留合约交互,请使用 "Pedersen"。如果可以自由选择效率最高的选项,请使用 "Poseidon"。如果需要与以太坊兼容,请使用 "Keccak"。

如何在 Cairo 中使用哈希函数

现在我们已经了解了这些哈希函数是什么以及它们的作用,接下来看看如何在实际中使用它们。Cairo 在其核心库中提供了这些哈希函数。对于 Pedersen 和 Poseidon 等函数,其原像(要哈希的输入值)类型必须实现一个名为 Hash 的 trait。

Hash Trait

此 trait 标记一个类型为可哈希的。它为 Cairo 的基本类型(felt252boolu8 等)实现,这些类型都可以表示为 felt252 值。对于复杂类型(如 struct 和 enum),派生 Hash#[derive(Hash)])使它们可以使用任何支持的哈希函数(Poseidon 或 Pedersen)进行哈希,前提是每个字段或变体本身都是可哈希的。

另一方面,集合类型不能也不能派生 Hash trait;因此是不可哈希的。以下示例说明了哪些类型可以派生 Hash,哪些不能(我们将在代码块后解释原因):

// ✅ 所有字段都是可哈希的
// 基本类型可以被哈希
#[derive(Hash)]
struct A {
    f1: felt252,
    f2: u256, // u256 实际上是一个结构体 { low: u128, high: u128 }
              // 但由于两个字段都是可哈希的,默认派生了 Hash
}

// ❌ 不能派生 Hash:存储(STORAGE)集合
// 存储中的 Vec 和 Map 不能被哈希,因为它们没有实现 `Hash`。
struct B {
    f1: Vec<felt252>,
    f2: Map<felt252, u128>,
}

// ❌ 不能派生 Hash:内存(MEMORY)集合
// 内存中的 Array 和 Dictionary 不能被哈希,因为它们没有实现 `Hash`。
struct C {
    f1: Array<felt252>,
    f2: Felt252Dict<u128>,
}

结构体 A 可以派生 Hash,因为两个字段都是可哈希的。虽然 felt252 显然是基本类型,但 u256 实际上是一个结构体 { low: u128, high: u128 }。然而,由于 u256 本身派生自 Hash(两个 u128 字段都是可哈希的),因此在哈希方面其行为类似基本类型。其他结构体不能派生 Hash,因为:

内存集合(如 ArrayFelt252Dict)默认不实现 Hash,因此包含它们的结构体不能使用 derive(Hash),因为 derive(Hash) 要求所有字段都是可哈希的。

MapVec 这样的类型专门用于存储,这意味着它们的数据存在于合约的持久存储中,而不是 Cairo 的执行内存中。由于 Hash trait 需要内存中的值,因此这些类型无法实现它。

要在 Cairo 中哈希值,你需要两个组件:Hash trait(标记类型为可哈希)和一个哈希状态(执行实际的哈希操作)。

哈希状态

哈希状态逐步哈希值,然后最终确定以生成摘要(最终的哈希输出)。这种设计使得可以对任意数量的输入进行哈希。

从概念上讲,哈希一个值列表就像这样:

hashFunc(x1, x2, x3, …, xn)

而不是一步计算,哈希器逐步工作:

  1. 初始化哈希状态

状态从一个预定义的常量值开始,该值硬编码在哈希函数的规范中。

  1. 哈希第一个输入
state₁ = h(state₀, x1)
  1. 哈希下一个输入
state₂ = h(state₁, x2)
  1. 继续哈希输入

此过程对每个输入重复,直到所有值都被哈希:

stateₙ = h(stateₙ₋₁, xn)
  1. 最终确定状态

最终状态是摘要:

digest = finalize(stateₙ)

Cairo 公开了两个用于处理哈希状态的 trait:

  1. HashStateTrait: 此 trait 旨在哈希 Cairo 的原生域元素(felt252)。

它提供两个核心方法:

  • .update:仅使用 felt252 类型的值更新哈希状态。
  • .finalize:最终确定状态并返回 felt252 类型的哈希摘要。

显示 HashStateTrait 提供的 update 和 finalize 方法的图片

  1. HashStateExTrait: 此 trait 对 HashStateTrait 进行了“扩展”。

它提供一个方法:

  • .update_with:使用实现 Hash trait 的“任何类型”的值更新哈希状态。

显示 HashStateExTrait 提供的 update_with 方法的图片

下一步是看看 Hash trait 和哈希状态在实际中如何结合。我们将演示如何使用它们通过 Pedersen 和 Poseidon 哈希值。

Pedersen 和 Poseidon

PedersenPoseidon 在 Cairo 中暴露了相同的哈希工作流:

  1. 初始化 哈希状态
  2. 更新 一个或多个值
  3. 最终确定 以生成 felt252 摘要

这使得它们在使用上几乎相同,但有一个关键区别:Pedersen 需要一个“基础”值。

以下是使用 Poseidon 哈希两个字段的示例:

PoseidonTrait::new()
    .update(a)
    .update(b)
    .finalize()

使用 Pedersen:

PedersenTrait::new(<基础值>)   // base
    .update(a)
    .update(b)
    .finalize()

从使用角度来看,两者之间唯一可见的区别是传递给 PedersenTrait::new 的额外参数,称为 base

Pedersen "base" 实际上是什么

Pedersen 的 base 只是一个 felt252 类型的 初始值

Pedersen 是一个 2 输入哈希函数(它总是哈希两个 felt252 值),这意味着没有原生的“哈希单个值”操作。要哈希单个值,基础作为第一个输入,实际要哈希的值(原像)作为第二个输入。

从概念上讲:

final_hash = pedersen(base, a)

当哈希多个值时,第一个哈希输出成为下一个状态并向前链接。例如,使用基础哈希三个值 abc 如下所示:

initial_state = pedersen(base, a)

state1 = pedersen(initial_state, b)

final_hash = pedersen(state1, c)

注意前一个状态如何成为下一次哈希操作的第一个输入,这正是使其成为链的原因。

基础也可以用作 域分隔符。使用不同的基础值将哈希置于不同的逻辑域中,即使以相同顺序哈希相同的值。例如,假设你的合约在空投领取和转账批准中都哈希了 (user, amount) 对。如果没有域分隔,两个操作对于相同的输入会产生相同的哈希。通过对每个操作使用不同的基础值,两个哈希被置于不同的逻辑域中,即使输入相同也总是产生不同的输出:

// 基础 0:空投领取域
let claim_hash = PedersenTrait::new(0)
    .update(user)
    .update(amount)
    .finalize();

// 基础 1:转账批准域
let approval_hash = PedersenTrait::new(1)
    .update(user)
    .update(amount)
    .finalize();

// claim_hash != approval_hash,即使输入相同

Poseidon:基础已内置

另一方面,Poseidon 不需要显式的基础值,因为它已经有一个内部固定状态。当调用 PoseidonTrait::new() 时,它从该预定义状态开始。

所以从概念上讲,使用 Poseidon 哈希单个值如下所示:

initial_state = PREDEFINED_STATE

hash = poseidon(initial_state, a)

对于两个输入 ab

initial_state = PREDEFINED_STATE

state1 = poseidon(initial_state, a)

hash = poseidon(state1, b)

让我们看一些在合约中使用 Poseidon 和 Pedersen 的示例。

示例 1:使用 .update 哈希两个域元素

以下代码示例演示了如何使用 Poseidon 哈希两个 felt252 值:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_two_felts_poseidon(self: @TContractState, a: felt252, b: felt252);
}

#[starknet::contract]
mod HelloStarknet {
    // 导入 `Poseidon` 哈希函数
    use core::poseidon::PoseidonTrait;

    // 导入 trait
    use core::hash::{
        HashStateTrait // .update(felt252) 和 .finalize()
    };

    #[storage]
    struct Storage {}

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

        //*** 实现 Poseidon 哈希的函数 ***//
        fn hash_two_felts_poseidon(self: @ContractState, a: felt252, b: felt252) {
            let digest = PoseidonTrait::new() // 初始化哈希状态
                .update(a) // 取第一个 felt252
                .update(b) // 取第二个 felt252
                .finalize(); // 生成摘要

            println!("Digest: {:?}", digest);
        }
    }
}

Pedersen 同理;唯一的区别是创建状态时需要传递一个 base0 是常见的选择,本文中也将使用它:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_two_felts_pedersen(self: @TContractState, a: felt252, b: felt252);
}

#[starknet::contract]
mod HelloStarknet {
    // 导入 `Pedersen` 哈希函数
    use core::pedersen::PedersenTrait;

    // 导入 trait
    use core::hash::{
        HashStateTrait // .update(felt252) 和 .finalize()
    };

    #[storage]
    struct Storage {}

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

        //*** 实现 Pedersen 哈希的函数 ***//
        fn hash_two_felts_pedersen(self: @ContractState, a: felt252, b: felt252) {
            let digest = PedersenTrait::new(0) // 0 作为运行哈希的基础
                .update(a)
                .update(b)
                .finalize();

            println!("Digest: {:?}", digest);
        }

    }
}

示例 2:使用 .update_with 哈希

到目前为止,我们只使用 .update 哈希了 felt252 值。但在实践中,我们经常需要哈希其他类型的值。

这就是 .update_with 的用途。它来自 HashStateExTrait,用于哈希任何实现(或派生)Hash trait 的类型。

使用 Poseidon 哈希 ContractAddress

在下面的代码中,我们哈希了两个 ContractAddressab。由于 ContractAddress 实现了 Hash trait,我们可以直接使用 .update_with() 将其传递到哈希状态中。

use starknet::ContractAddress;

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_two_addresses_poseidon(
                self: @TContractState,
                a: ContractAddress,
                b: ContractAddress
             );
}

#[starknet::contract]
mod HelloStarknet {
    use starknet::ContractAddress;

    // 导入 `Poseidon` 哈希函数
    use core::poseidon::PoseidonTrait;

    // 导入 trait
    use core::hash::{
        HashStateTrait,
        HashStateExTrait // .update_with(<T>) *** 新添加 ***
    };

    #[storage]
    struct Storage {}

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

        //*** 实现 Poseidon 哈希的函数 ***//
        fn hash_two_addresses_poseidon(self: @ContractState, a: ContractAddress, b: ContractAddress) {
            let digest = PoseidonTrait::new()  // 初始化哈希状态
                .update_with(a)   // 取第一个地址
                .update_with(b)   // 取第二个地址
                .finalize();  // 生成摘要

             println!("Digest: {:?}", digest);
        }
    }
}

使用 Pedersen 哈希 ContractAddress

以下合约使用 Pedersen 哈希函数哈希两个地址:

use starknet::ContractAddress;

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_two_addresses_pedersen(
                self: @TContractState,
                a: ContractAddress,
                b: ContractAddress
             );
}

#[starknet::contract]
mod HelloStarknet {
    use starknet::ContractAddress;

    // 导入 `Pedersen` 哈希函数
    use core::pedersen::PedersenTrait;

    // 导入 trait
    use core::hash::{
        HashStateTrait,
        HashStateExTrait // .update_with(<T>) *** 新添加 ***
    };

    #[storage]
    struct Storage {}

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

        //*** 实现 Pedersen 哈希的函数 ***//
        fn hash_two_addresses_pedersen(self: @ContractState, a: ContractAddress, b: ContractAddress) {
            let digest = PedersenTrait::new(0) // 初始化哈希状态
                .update_with(a) // 取第一个地址
                .update_with(b) // 取第二个地址
                .finalize(); // 生成摘要

             println!("Digest: {:?}", digest);
        }
    }
}

使用 Poseidon 哈希结构体

在下面的代码中,由于 MyStruct 在接口中用作参数,我们在合约模块外部定义它,以便接口和合约实现都能访问它。

任何出现在 #[starknet::interface] 中的结构体必须:

  • 在合约模块 外部 定义
  • 派生 Serde,以便可以序列化为 felt252 序列,并反序列化回来
  • 派生 Drop,以便值在超出作用域时可以安全丢弃
// 定义我们的结构体
#[derive(Hash, Serde, Drop)]
struct MyStruct {
    a: u256,
    b: felt252,
}

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_struct_poseidon(self: @TContractState, my_struct: MyStruct);
}

#[starknet::contract]
mod HelloStarknet {
    use core::hash::{ HashStateTrait, HashStateExTrait };
    use core::poseidon::PoseidonTrait;

    //*** 导入我们在合约外部定义的结构体 ***//
    use super::MyStruct;

    #[storage]
    struct Storage {}

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

        fn hash_struct_poseidon(self: @ContractState, my_struct: MyStruct) {
            let digest = PoseidonTrait::new().update_with(my_struct).finalize();

            println!("Digest: {:?}", digest);
        }

    }
}

使用 Pedersen 哈希结构体

与 Pedersen 同理,只需将导入替换为 Pedersen 哈希函数,并将状态替换为 PedersenTrait::new(0),其余保持不变:

// 定义我们的结构体
#[derive(Hash, Serde, Drop)]
struct MyStruct {
    a: u256,
    b: felt252,
}

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_struct_pedersen(self: @TContractState, my_struct: MyStruct);
}

#[starknet::contract]
mod HelloStarknet {
    use core::hash::{ HashStateTrait, HashStateExTrait };
    use core::pedersen::PedersenTrait;

    //*** 导入我们在合约外部定义的结构体 ***//
    use super::MyStruct;

    #[storage]
    struct Storage {}

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

        fn hash_struct_pedersen(self: @ContractState, my_struct: MyStruct) {
            let digest = PedersenTrait::new(0) // *** 此处替换 *** //
                                            .update_with(my_struct).finalize();

            println!("Digest: {:?}", digest);
        }

    }
}

如果结构体仅在合约内部使用,不通过接口暴露,则不需要派生 Serde

以下是在合约内定义和使用的可哈希结构体示例:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_struct_pedersen(
                self: @TContractState,
                value1: u256,
                value2: felt252
        );
}

#[starknet::contract]
mod HelloStarknet {
    use core::hash::{HashStateExTrait, HashStateTrait};
    use core::pedersen::PedersenTrait;

    // *** 在合约内部定义结构体 *** //
    #[derive(Hash, Drop)]
    struct MyStruct {
        a: u256,
        b: felt252,
    }

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn hash_struct_pedersen(self: @ContractState, value1: u256, value2: felt252) {

            // *** 初始化结构体 *** //
            let my_struct = MyStruct { a: value1, b: value2 };

            let digest = PedersenTrait::new(0).update_with(my_struct).finalize();

            println!("Digest: {:?}", digest);

        }
    }
}

示例 3:哈希数组

我们不能像对其他类型那样直接哈希数组,因为数组本身没有实现 Hash trait。要哈希数组,你必须 遍历每个元素,逐步更新哈希状态,然后在最后调用 .finalize()

使用 Poseidon 哈希 Array<felt252>

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_array_manual_poseidon(self: @TContractState, values: Array<felt252>);
}

#[starknet::contract]
mod HelloStarknet {
    use core::hash::HashStateTrait;
    use core::poseidon::PoseidonTrait;

    #[storage]
    struct Storage {}

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

        fn hash_array_manual_poseidon(self: @ContractState, values: Array<felt252>) {
            let mut state = PoseidonTrait::new(); // 哈希状态
            let mut i = 0;
            let len = values.len();

            // 循环
            while i != len {
                state = state.update(*values.at(i));
                i += 1;
            }

            // 最终确定状态
            let digest = state.finalize();

            println!("Digest: {:?}", digest);
        }

    }
}

Cairo 有一个内置的 Poseidon 辅助函数,它在底层自动处理循环和状态更新:poseidon_hash_span

使用内置 Poseidon 辅助函数哈希 felt252 数组

poseidon_hash_span 函数接受一个 Span<felt252> 作为输入,遍历每个元素以构建哈希状态,然后最终化并返回单个 felt252 摘要。

以下是一个示例。与手动 Poseidon 哈希不同,手动哈希需要导入 PoseidonTraitHashStateTrait,而 poseidon_hash_span 是一个独立函数,在内部处理所有内容。我们只需要导入并使用它:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_array_builtin_poseidon(self: @TContractState, values: Array<felt252>);
}

#[starknet::contract]
mod HelloStarknet {
    // 导入 `poseidon_hash_span`
    use core::poseidon::poseidon_hash_span;

    #[storage]
    struct Storage {}

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

        fn hash_array_builtin_poseidon(self: @ContractState, values: Array<felt252>) {
            // 将 Array<felt252> 转换为 Span<felt252>
            let span = values.span();

            // 使用 `poseidon_hash_span`
            let digest = poseidon_hash_span(span);

            println!("Digest: {:?}", digest);
        }

    }
}

由于 poseidon_hash_span 函数接受 Span<felt252> 作为输入,我们首先使用 .span() 将数组转换为 span,然后将其传递给内置函数,该函数返回单个 felt252 摘要。

如果我们将相同的数组同时传递给 hash_array_manual_poseidonhash_array_builtin_poseidon 函数,它们将产生相同的 Poseidon 哈希,因为 poseidon_hash_span 只是在底层执行手动循环。

使用 Pedersen 哈希 Array<felt252>

使用 Pedersen 哈希 felts 数组的步骤与 Poseidon 类似:手动遍历数组并顺序哈希每个元素。然而,在 Starknet 中使用 Pedersen 哈希数组时有一个约定:“数组长度必须作为最后一个元素包含在内”。该模式在 Starknet 生态系统中一致遵循,包括 协议实现 和标准库(如 starknet.js)。

下面的 hash_array_manual_pedersen 函数展示了这个模式。在哈希所有数组元素之后,我们在最终确定哈希状态之前将数组长度作为最后一个元素进行哈希:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash_array_manual_pedersen(self: @TContractState, values: Array<felt252>);
}

#[starknet::contract]
mod HelloStarknet {
    use core::hash::HashStateTrait;
    use core::pedersen::PedersenTrait;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn hash_array_manual_pedersen(self: @ContractState, values: Array<felt252>) {
            let mut state = PedersenTrait::new(0);
            let mut i = 0;
            let len = values.len();

            while i != len {
                state = state.update(*values.at(i));
                i += 1;
            }

            // 关注这里:包含数组长度
            state = state.update(len.into());

            let digest = state.finalize();

            println!("Digest: {:?}", digest);
        }
    }
}

要点:

  • 当需要自定义逻辑(例如混合其他数据类型或条件更新)时使用手动循环。
  • 使用 poseidon_hash_span 计算 felt252 数组的 Poseidon 哈希。它更简洁,代码更少。
  • 使用 Pedersen 哈希数组的标准模式是在最终确定哈希状态之前将数组长度作为最后一个元素包含在内。

Keccak256

Cairo 的 core::keccak 模块提供了四个函数:

  • compute_keccak_byte_array:哈希一个 ByteArray
  • keccak_u256s_be_inputs:哈希一个以大端格式编码的 u256 值数组。
  • keccak_u256s_le_inputs:哈希一个以小端格式编码的 u256 值数组。
  • cairo_keccak:使用自定义填充哈希字节序列(相当于 Solidity 的 keccak256(abi.encodePacked(val)))。

所有这些都返回一个 u256,表示与 Solidity 的 keccak256 生成的 32 字节摘要相同的值。区别在于值的表示方式:Solidity 以大端 bytes32 格式返回摘要,而 Cairo 以小端 u256 格式返回。

例如,假设 Solidity 对一个值进行 keccak 哈希产生的摘要为 0x1234...5678。Cairo 的 keccak 会将相同的摘要表示为一个 little-endian u256,因此其字节顺序是相反的:0x7856...3412。我们将在最后一节中看到如何使 Cairo 和 Solidity 的 keccak 结果匹配。

Cairo 的 Keccak 函数及其 Solidity 等效项

  • compute_keccak_byte_array → 哈希一个 ByteArray

Solidity 合约:

contract Example {
      function hashHello() external pure returns (bytes32) {
          return keccak256(abi.encodePacked("Hello RareSkills"));
      }
}

Cairo 等效项:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
      fn hash_hello(self: @TContractState);
}

#[starknet::contract]
mod HelloStarknet {

      // 导入
      use core::keccak::compute_keccak_byte_array;

      #[storage]
      struct Storage {}

      #[abi(embed_v0)]
      impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
          fn hash_hello(self: @ContractState) {
                // 执行哈希
              let digest = compute_keccak_byte_array(@"Hello RareSkills");

              println!("Digest: {:?}", digest);
          }
      }
}
  • keccak_u256s_be_inputs → 以大端顺序哈希一个 u256 值数组,匹配 Solidity 的默认编码。

Solidity 合约:

contract Example {
      function hash() external pure returns (bytes32) {
          return keccak256(abi.encode(1,2));
      }
}

Cairo 等效项:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
      fn hash(self: @TContractState);
}

#[starknet::contract]
mod HelloStarknet {

      // 导入
      use core::keccak::keccak_u256s_be_inputs;

      #[storage]
      struct Storage {}

      #[abi(embed_v0)]
      impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
          fn hash(self: @ContractState) {
                // 执行哈希
              let digest = keccak_u256s_be_inputs([1, 2].span());

              println!("Digest: {:?}", digest);
          }
      }
}
  • keccak_u256s_le_inputs → 以小端顺序哈希一个 u256 值数组。Solidity 可以通过在哈希前手动将输入转换为小端来复制此操作。

Solidity 合约:

contract Example {
      function hash() external pure returns (bytes32) {
        // 将 1_u256 和 2_u256 转换为小端
        uint256 one_le =
            0x0100000000000000000000000000000000000000000000000000000000000000;
        uint256 two_le =
            0x0200000000000000000000000000000000000000000000000000000000000000;

        return keccak256(abi.encode(one_le,two_le));
      }
}

Cairo 等效项:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
      fn hash(self: @TContractState);
}

#[starknet::contract]
mod HelloStarknet {

      // 导入
      use core::keccak::keccak_u256s_le_inputs;

      #[storage]
      struct Storage {}

      #[abi(embed_v0)]
      impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
          fn hash(self: @ContractState) {
                // 执行哈希
              let digest = keccak_u256s_le_inputs([1, 2].span());

              println!("Digest: {:?}", digest);
          }
      }
}
  • cairo_keccak → 它接受三个参数:

    • input - 以小端格式表示的完整 64 位字的数组
    • last_input_word - 来自 input 且不构成完整 8 字节字的剩余字节。例如,如果你总共哈希 12 字节,前 8 字节作为完整字放入 input,最后 4 字节放入 last_input_word。如果你的输入恰好能被 8 整除(例如 8、16 或 32 字节),则 last_input_word0
    • last_input_num_bytes - last_input_word 中的字节数。必须是 07 之间(含)的整数。

下图显示了这些参数如何协同工作:

显示文本 “Hello world!” 如何转换为十六进制并用作 cairo_keccak 函数参数的图片

示例 1 – 哈希 u64 或 8 字节倍数的数据

Solidity 合约:

contract Example {
    function hash() external pure returns (bytes32) {
      uint64 one = 1; // 0x0000000000000001

      return keccak256(abi.encodePacked(one));
    }
}

Cairo 等效项:

#[starknet::interface]
pub trait IHelloStarknet<TContractState> {
    fn hash(self: @TContractState);
}

#[starknet::contract]
mod HelloStarknet {

    // 导入
    use core::keccak::cairo_keccak;

    #[storage]
    struct Storage {}

    #[abi(embed_v0)]
    impl HelloStarknetImpl of super::IHelloStarknet<ContractState> {
        fn hash(self: @ContractState) {
            let mut input = array![0x0100000000000000]; // 1_u64 作为小端
            let digest = cairo_keccak(ref input, 0, 0); // 没有额外字节

            println!("Digest: {:?}", digest);
        }
    }
}

最后两个 cairo_keccak 参数;last_input_wordlast_input_num_bytes 为 0,因为我们没有额外的字节需要哈希。

示例 2 – 哈希不是 u64 或 8 字节倍数的数据

假设我们要哈希 0x48656c6c6f20776f726c6421,它代表 "Hello world!"(12 字节)。由于 12 不能被 8 整除,我们将需要使用 last_input_word 参数。

Solidity 合约:

contract Example {
    function hash() external pure returns (bytes32) {
      bytes12 input = 0x48656c6c6f20776f726c6421;

      return keccak256(abi.encodePacked(input));
    }
}

Cairo 等效项:

fn hash(self: @ContractState) {
    // 要哈希的字节 - 0x48656c6c6f20776f726c6421 (12 字节)

    // 前 8 字节 (48656c6c6f20776f),反转成小端
    let mut input = array![0x6f77206f6c6c6548];

    // 执行哈希
    // 剩余 4 字节 (726c6421) 转换为小端 - 0x21646c72
    let digest = cairo_keccak(ref input, 0x21646c72, 4);

    println!("Digest: {:?}", digest);
}

这里:

  • 0x6f77206f6c6c6548 是前 8 字节反转后的结果。
  • 0x21646c72 是剩余 4 字节反转后的结果。
  • 4 表示这些剩余字节的数量。

这些示例展示了 Cairo 的 Keccak 函数如何与 Solidity 的 keccak256 对应,但它们的输出在字节顺序上有所不同。Cairo 返回一个小端 u256,而 Solidity 生成一个大端 bytes32。在将 Cairo 结果与 Solidity 哈希进行比较之前,请反转 Cairo 结果的字节顺序。

小端与大端之间的转换

下面的动画显示了字节从小端到大端的转换:

要在 Cairo 中执行相同操作,我们通过反转每个 128 位半部分并交换它们的位置来反转 u256 值的字节顺序。Cairo 从 core::integer 模块提供了一个内置函数 u128_byte_reverse 来反转字节。

以下代码示例展示了如何通过反转字节顺序将 u256 值从小端表示转换为大端表示(反之亦然):

fn u256_reverse_bytes(x: u256) -> u256 {
    u256 {
        // 取高 128 位,反转其字节顺序,放入低位
        low: core::integer::u128_byte_reverse(x.high),

        // 取低 128 位,反转其字节顺序,放入高位
        high: core::integer::u128_byte_reverse(x.low),
    }
}

由于 Cairo 中的 u256 由两个 u128 值(lowhigh)组成,反转 256 位整数的字节顺序需要:

  1. 反转每个 128 位半部分内的字节顺序。
  2. 交换两个半部分。

core::integer::u128_byte_reverse 函数对每个 u128 执行字节级反转。通过将其应用于两个半部分并交换它们的位置,我们反转了整个 256 位值的字节顺序。

由于字节反转是对称的,相同的函数可用于转换:

  • 小端 → 大端
  • 大端 → 小端

应用两次将返回原始值。

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

0 条评论

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