Sway 智能合约手动实现HashTrait 完整指南:hash与is_hash_trivial的语义、规则与最佳实践
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
本篇技术指南围绕 Sway 语言(Fuel 生态智能合约语言)中std::hash::Hashtrait 的手动实现展开,面向需要将自定义类型(结构体、元组、枚举等)用于sha256/keccak256哈希计算或作为StorageMap键的合约开发者。读完本文,你将掌握Hashtrait 两个方法(hash与is_hash_trivial)的准确语义、安全实现is_hash_trivial的全部边界规则,以及平凡哈希优化在标准库底层是如何生效的,从而写出既正确又省 gas 的自定义类型哈希实现。
Sway 为什么必须手动实现Hash
在 Rust 中,#[derive(Hash)]可以自动为类型生成哈希实现;而 Sway目前不支持 derive trait 实现,因此Hashtrait 必须由开发者手动为每个自定义类型编写。这一点决定了任何涉及自定义类型哈希的 Sway 合约,都绕不开对本文所述规则的理解。
Hashtrait 来自标准库(std::hash::Hash),它定义了“一个值如何被哈希”。为自定义类型实现它之后,该类型就可以:
- 通过
sha256与keccak256函数被直接哈希(返回b256); - 在需要确定性哈希的场景中被使用,典型例子是作为
StorageMap的键类型。
标准库对该 trait 的完整定义位于 sway-lib-std/src/hash.sw:
pub trait Hash { fn is_hash_trivial() -> bool; fn hash(self, ref mut state: Hasher); }一个完整实现由两个方法组成,二者职责截然不同:
hash:定义值如何将字节写入Hasher,这是唯一影响最终哈希值的部分;is_hash_trivial:一个优化提示,声明值的内存表示是否与其hash方法写入Hasher的哈希字节表示逐字节一致。
认识Hasher:哈希字节的累加器
在深入两个方法之前,先理解hash方法的“落笔之处”Hasher。标准库中Hasher本质是一个基于Bytes的字节缓冲,其核心方法在 sway-lib-std/src/hash.sw 中定义:
pub struct Hasher { bytes: Bytes, } impl Hasher { pub fn new() -> Self { ... } pub fn with_capacity(capacity: u64) -> Self { ... } /// 追加 `bytes` 的内容(不追加长度)。 pub fn write(ref mut self, bytes: Bytes) { ... } pub fn write_raw_slice(ref mut self, slice: raw_slice) { ... } /// 追加一个 `u8` 值。 pub fn write_u8(ref mut self, value: u8) { ... } /// 追加单个 `str`(不追加长度)。 pub fn write_str(ref mut self, s: str) { ... } pub fn write_str_array<S>(ref mut self, s: S) { ... } pub fn sha256(self) -> b256 { ... } // 通过 asm 指令 s256 计算 pub fn keccak256(self) -> b256 { ... } // 通过 asm 指令 k256 计算 }可以看到,Hasher提供了多种“写入方式”:write_u8写入单字节,write_raw_slice/write_str/write_str_array写入原始字节(均不附带长度前缀)。write与write_raw_slice的文档注释特别强调“length 不会写入”,这一点正是后面new_hashing特性影响平凡哈希判定的关键前提。sha256与keccak256方法最终通过内联汇编s256/k256指令对缓冲内容求哈希。
实现hash:向Hasher写入哈希字节表示
hash方法把值的哈希字节表示写入Hasher。典型做法是:
- 对聚合类型(结构体、元组、数组等):依次哈希每个字段;
- 对枚举:先哈希判别子(tag),再哈希载荷(payload)。
一个最基础的结构体实现示例:
use std::hash::{Hash, Hasher}; struct Point { x: u64, y: u64, } impl Hash for Point { fn is_hash_trivial() -> bool { true } fn hash(self, ref mut state: Hasher) { self.x.hash(state); self.y.hash(state); } }这里self.x.hash(state)会调用标准库为u64预置的Hash实现(见 sway-lib-std/src/hash.sw),后者将值所在内存的 8 个字节以raw_slice形式写入Hasher:
impl Hash for u64 { fn is_hash_trivial() -> bool { true } fn hash(self, ref mut state: Hasher) { state.write_raw_slice(raw_slice::from_parts::<u8>(__addr_of(self), 8)); } }标准库已为u8、u16、u32、u64、b256、u256、bool、()、元组、数组、str、str[N]、Bytes、Vec、raw_slice、Option<T>、Result<T, E>等类型提供了实现(均在 sway-lib-std/src/hash.sw 中),自定义类型的hash通常就是按序委托这些已有实现。
实现is_hash_trivial:优化提示与强保证
当一个类型是平凡可哈希(trivially hashable)的,其哈希可以直接从值的原始内存计算:取__size_of::<Self>()个字节、以值所在地址为起点,直接喂给哈希指令,无需先在Hasher中构建中间字节缓冲。这正是sha256与keccak256对平凡类型所做的,而且显著更省 gas。
标准库在 sway-lib-std/src/hash.sw 中通过const IS_TRIVIAL在编译期分支选择两条路径:
#[inline(never)] pub fn sha256<T>(val: T) -> b256 where T: Hash, { const IS_TRIVIAL: bool = is_hash_trivial::<T>(); if IS_TRIVIAL { // 内存表示与哈希字节表示逐字节一致, // 直接哈希原始内存,避免在 Hasher 中构建中间缓冲。 let mut result_buffer = b256::zero(); asm( hash: result_buffer, ptr: __addr_of(val), bytes: __size_of::<T>(), ) { s256 hash ptr bytes; hash: b256 } } else { let capacity = get_initial_capacity::<T>(); let mut hasher = Hasher::with_capacity(capacity); val.hash(hasher); hasher.sha256() } }keccak256采用完全相同的结构(对应k256指令,见 sway-lib-std/src/hash.sw)。因此可以明确两个结论:
- 返回
true是一项强保证:如果错误地返回true,经sha256/keccak256哈希出的结果是错误的; - 返回
false永远安全,只是放弃了平凡哈希优化,走Hasher路径。
拿不准时,就返回
false。
安全实现is_hash_trivial的完整规则
只有满足“类型的内存表示与hash方法写入Hasher的字节逐字节一致”时才应返回true。以下规则整理自 sway-lib-std/src/hash.sw 的 trait 文档注释,也是整个判定体系的核心:
u16与u32永远不是平凡可哈希的。它们在内存中以八字节槽(即u64宽度)存储,但哈希字节表示分别只有 2 字节与 4 字节。任何包含它们的聚合类型(结构体、元组、数组……)因此也不平凡可哈希。这从标准库实现可直接印证:u16 的实现 取内存地址偏移 6 处的 2 字节,u32 的实现 取偏移 4 处的 4 字节。聚合类型内部的填充(padding)会破坏平凡性。结构体或元组中的
bool、u8、u16、u32字段在内存中被填充(对齐)到八字节,而hash写入时不含这些填充。因此,包含此类字段的聚合类型不平凡可哈希——即使这些字段单独哈希时是平凡的(例如bool单独哈希时平凡,见 bool 的实现,但作为结构体字段就破坏了平凡性)。枚举的 tag 按
u64存储。标准库中的Hash实现将枚举 tag 按u8哈希(参见 Option 的实现 与 Result 的实现,均以0_u8.hash(state)/1_u8.hash(state)写入 tag),而 tag 在内存中是u64。遵循这一约定的枚举不平凡可哈希。集合类型取决于
new_hashing实验特性。Bytes、Vec、raw_slice、str、str[N]、数组以及包含它们的聚合类型,是否平凡可哈希取决于new_hashing实验特性(对应上游 issue FuelLabs/sway#7256)。启用new_hashing后,集合会在内容前前缀写入其长度(例如 启用后的Vec<T>实现 先len.hash(state)再写元素),哈希字节表示不再匹配内存表示,因此不平凡可哈希。标准库中用#[cfg(experimental_new_hashing = false/true)]成对区分两种行为。
据此可归纳判定标准:平凡可哈希的类型 = 定长、无填充、且其hash方法恰好写入其内存字节的类型。包括u64、b256、u256、bool、(),以及所有字段本身平凡可哈希且按字对齐的结构体与元组(例如仅含u64、b256、u256字段的类型)。注意,标准库对元组的is_hash_trivial还会额外用__mem_repr_eq::<Self>("runtime", "hashing")比较运行时与哈希两种内存表示(见 sway-lib-std/src/hash.sw),以确保元组元素之间不存在填充。
实践示例一:平凡可哈希的结构体
所有字段均按字对齐且平凡可哈希、无填充的结构体是平凡可哈希的:
use std::hash::{Hash, Hasher}; struct Stats { strength: u64, agility: u64, } impl Hash for Stats { fn is_hash_trivial() -> bool { // 两个 `u64` 字段,无填充:内存字节与 `hash` 写入的字节完全一致。 true } fn hash(self, ref mut state: Hasher) { self.strength.hash(state); self.agility.hash(state); } }实践示例二:不平凡可哈希的结构体
带填充字段(bool)与动态尺寸字段(str)的结构体不平凡可哈希:
use std::hash::{Hash, Hasher}; struct Account { id: u64, active: bool, // 在内存中填充到八字节。 name: str, // 动态尺寸。 } impl Hash for Account { fn is_hash_trivial() -> bool { // `active` 在内存中填充到八字节,`name` 是动态尺寸, // 内存表示与哈希字节不匹配。 false } fn hash(self, ref mut state: Hasher) { self.id.hash(state); self.active.hash(state); self.name.hash(state); } }str永不平凡可哈希还有更深层的原因:从 str 的实现注释 可以看到,str是一个“胖指针”(fat pointer,含位置与长度),其内存表示永远不可能与哈希字节一致。另外注意,标准库对动态尺寸元素集合(如Vec<T>)的hash实现,会先把底层指针通过__transmute转成数组引用再逐元素写入(见 sway-lib-std/src/hash.sw),自定义实现可参考这一手法。
实践示例三:枚举的哈希与平凡性
遵循标准库“tag 按u8哈希”的约定,会使枚举不平凡可哈希,因为 tag 在内存中按u64存储:
use std::hash::{Hash, Hasher}; enum Shape { Circle: u64, Square: u64, } impl Hash for Shape { fn is_hash_trivial() -> bool { // tag 按 `u8` 哈希,但在内存中按 `u64` 存储。 false } fn hash(self, ref mut state: Hasher) { match self { Shape::Circle(radius) => { 0_u8.hash(state); radius.hash(state); }, Shape::Square(side) => { 1_u8.hash(state); side.hash(state); }, } } }若将 tag 按u64哈希(与内存表示一致),枚举则可以变得平凡可哈希。最简单安全的情形是仅含 tag 的枚举,即所有变体都是单元(零尺寸)的枚举:
use std::hash::{Hash, Hasher}; enum Location { Earth: (), Mars: (), } impl Hash for Location { fn is_hash_trivial() -> bool { // 该枚举仅由 tag 组成,按 `u64` 哈希,与内存表示一致。 true } fn hash(self, ref mut state: Hasher) { match self { Location::Earth => 0_u64.hash(state), Location::Mars => 1_u64.hash(state), } } }真实应用:自定义类型作为StorageMap键
Hashtrait 最重要的实际用途之一是为StorageMap<K, V>提供键类型约束。在 sway-lib-std/src/storage/storage_map.sw 中,StorageMap的所有方法都要求K: Hash,其存储槽位由键的哈希计算得出:
impl<K, V> StorageKey<StorageMap<K, V>> where K: Hash, { fn get_slot_key(self, key: K) -> b256 { sha256((STORAGE_MAP_DOMAIN, key, self.field_id())) } }可以看到:存储槽位是sha256((1u8, key, field_id))的结果,其中1u8是存储映射域的域前缀(STORAGE_MAP_DOMAIN,用于避免与编译器生成的其他存储字段槽位冲突),key会经过其Hash实现被哈希。这意味着键类型的hash方法直接决定存储槽位的计算结果,不同实现会导致读写定位到不同的槽位——所以键类型的Hash实现必须稳定、确定,而is_hash_trivial的误报则会产生错误哈希值,进而读写到错误存储位置。这正是把本文规则视为合约安全事项的原因。
端到端示例:完整的哈希使用场景
仓库中的 examples/hashing/src/main.sw 给出了一个完整的可运行脚本(项目配置见 examples/hashing/Forc.toml,依赖本地sway-lib-std),它同时演示了平凡与非平凡类型的Hash实现,以及sha256/keccak256对各类值的调用:
script; use std::hash::*; impl Hash for Stats { fn is_hash_trivial() -> bool { // `Stats` 是含两个 `u64` 的结构体,平凡可哈希。 true } fn hash(self, ref mut state: Hasher) { self.strength.hash(state); self.agility.hash(state); } } impl Hash for Person { fn is_hash_trivial() -> bool { // `Person` 含 `bool`、`str` 与数组,不平凡可哈希。 false } fn hash(self, ref mut state: Hasher) { self.name.hash(state); self.age.hash(state); self.alive.hash(state); self.location.hash(state); self.stats.hash(state); self.some_tuple.hash(state); self.some_array.hash(state); self.some_b256.hash(state); } } fn main() { // 各类基础值与自定义类型均可直接哈希 let sha_hashed_u64 = sha256(u64::max()); let sha_hashed_b256 = sha256(VALUE_A); let sha_hashed_str = sha256("Fastest Modular Execution Layer!"); let sha_hashed_tuple = sha256((true, 7)); let sha_hashed_array = sha256([4, 5, 6]); let sha_hashed_enum = sha256(Location::Earth); let sha_hashed_struct = sha256(Person { /* ... */ }); // keccak256 用法一致 let keccak_hashed_u64 = keccak256(u64::max()); let keccak_hashed_enum = keccak256(Location::Earth); // ... }该示例中Location(纯 tag 枚举)与Stats(双u64结构体)返回true,Person(含bool、str、数组、元组等字段)返回false,与本文前述规则一一对应,可以作为自查模板对照使用。哈希与签名恢复等更广泛的密码学能力可参见 docs/book/src/blockchain-development/hashing_and_cryptography.md。
总结与最佳实践清单
为 Sway 自定义类型实现Hashtrait 时,建议按以下清单执行:
- 逐字段委托:
hash方法按确定性顺序(结构体按声明顺序、枚举先 tag 后载荷)委托各字段的Hash实现,不要自创编码格式; - 默认返回
false:除非能逐字节证明内存表示与哈希字节一致,否则一律返回false;返回false永远安全,只是少一次优化; - 识别平凡类型的充分条件:定长、无填充、字段全部平凡可哈希且按字对齐(仅
u64、b256、u256这类);含bool/u8/u16/u32字段的聚合、含u16/u32的类型、遵循u8tag 约定的枚举、以及任何集合类型,都不要返回true; - 留意
new_hashing特性:该实验特性会让集合类型在哈希字节中前缀长度,从而改变平凡性判定,标准库 sway-lib-std/src/hash.sw 中以#[cfg(experimental_new_hashing = ...)]区分两种行为,自定义实现需与所选特性保持一致; - 键类型必须稳定:作为
StorageMap键的类型,其Hash实现一旦上线便不能随意变更,否则将无法定位到既有存储槽位; - 回归验证:为自定义类型编写基于
sha256/keccak256的断言(可参考标准库文档中的assert_eq示例),确保平凡/非平凡两条路径的哈希结果都符合预期。
【免费下载链接】sway🌴 Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考