Sway 智能合约手动实现 `Hash` Trait 完整指南:`hash` 与 `is_hash_trivial` 的语义、规则与最佳实践

Sway 智能合约手动实现 `Hash` Trait 完整指南:`hash` 与 `is_hash_trivial` 的语义、规则与最佳实践 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.swpub 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_arrayS(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形式写入Hasherimpl 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、OptionT、ResultT, 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 sha256T(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后集合会在内容前前缀写入其长度例如 启用后的VecT实现 先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含位置与长度其内存表示永远不可能与哈希字节一致。另外注意标准库对动态尺寸元素集合如VecT的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 最重要的实际用途之一是为StorageMapK, V提供键类型约束。在 sway-lib-std/src/storage/storage_map.sw 中StorageMap的所有方法都要求K: Hash其存储槽位由键的哈希计算得出implK, V StorageKeyStorageMapK, 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结构体返回truePerson含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),仅供参考