十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Sway 智能合约哈希运算完全指南:sha256 与 keccak256 的使用与底层实现

Sway 智能合约哈希运算完全指南:sha256 与 keccak256 的使用与底层实现 Sway 智能合约哈希运算完全指南sha256 与 keccak256 的使用与底层实现【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway在 Fuel 区块链的 Sway 语言生态中哈希运算是智能合约开发的基础能力——从存储键的推导、身份与地址的生成到签名验证与数据指纹的比对几乎处处离不开哈希。本篇技术指南以 Sway 官方参考文档的 Hashing 章节 为核心结合其配套的可运行代码示例与标准库 sway-lib-std/src/hash.sw 的源码实现系统讲解sha256与keccak256两个函数的导入、调用、多值哈希包装技巧并深入剖析Hasher、Hashtrait 与底层汇编指令帮助你写出正确、高效、gas 友好的 Sway 哈希代码。哈希模块概览Sway 标准库通过hash模块位于sway-lib-std/src/hash.sw对外提供两个核心哈希函数函数算法返回值典型用途sha256SHA-2 系列中的 SHA-256256 位摘要b256通用数据指纹、存储键推导、合约内数据校验keccak256Keccak-256以太坊系生态广泛使用b256与以太坊风格签名/ABI 交互、跨链兼容场景它们的共同签名特征是接受一个泛型参数T返回一个b256256 位 / 32 字节的哈希值。b256是 Sway 的内置类型其语义本身就是一个哈希关于该类型的详细说明可参见官方文档的 built-ins 章节。注意sha256与keccak256是两种不同的算法即使输入相同输出也不同。官方文档给出的示例值即为佐证详见后文测试与已知哈希值一节。环境准备引入标准库依赖本文所有示例都基于一个独立的 Sway 库项目。参考文档配套的示例工程 docs/reference/src/code/operations/hashing/Forc.toml 展示了最简配置[project] authors [Fuel Labs contactfuel.sh] entry lib.sw license Apache-2.0 name hashing [dependencies] std { path ../../../../../../sway-lib-std }关键点在于[dependencies]中通过path将std指向仓库内的 sway-lib-std 目录。在实际项目中通常也可以直接写std { git ... , tag ... }或使用 forc 默认自带的std依赖只要你已经初始化了一个 Sway 项目forc init并在src/lib.sw库或src/main.sw合约/脚本中编写代码即可直接使用下文提到的use std::hash::*;导入语句。使用 sha256导入sha256定义在std::hash模块中使用前必须先导入use std::hash::*;示例代码见 docs/reference/src/code/operations/hashing/src/lib.sw 中的import锚点片段。调用方式最直接的调用方式是把任意实现了Hashtrait 的值传给sha256let digest: b256 sha256(42_u64);参考文档配套示例 lib.sw 给出的是一个更完整的函数封装——它演示了如何连续哈希多个不同类型的值fn sha256_hashing(age: u64, name: str, status: bool) - b256 { let mut hasher Hasher::new(); age.hash(hasher); hasher.write_str(name); status.hash(hasher); hasher.sha256() }这个示例揭示了标准库哈希能力的两层 API便捷函数层sha256(val)——适合哈希单个值详见后文。流式构建层Hasher配合Hashtrait——适合按顺序把多个值喂进同一个哈希上下文最后统一收尾输出。使用 keccak256keccak256的用法与sha256完全对称use std::hash::*;配套示例同样提供了流式版本的函数fn keccak256_hashing(age: u64, name: str, status: bool) - b256 { let mut hasher Hasher::new(); age.hash(hasher); hasher.write_str(name); status.hash(hasher); hasher.keccak256() }可以看到除了最后一行的收尾方法hasher.sha256()vshasher.keccak256()其余代码完全一致。这也意味着选择哪种算法只影响最终摘要的计算方式不影响你如何组织和写入数据。在实际合约中你可以根据自己的生态兼容性需求如与以太坊工具链交互时选keccak256与 Fuel 原生存储/签名体系交互时两者皆可自由切换。哈希多个值必须包装成复合类型官方文档特别强调了一个关键约束sha256和keccak256每次只接受一个泛型参数T因此当你想把多个值作为一个整体哈希时必须先将它们包装进一个复合类型例如元组tuple(key, self.slot())—— 这是最常见的做法测试代码test/src/e2e_vm_tests/test_programs/should_fail/generics_in_contract/src/main.sw中就出现了sha256((key, self.slot()))、sha256((key, i, self.slot()))这类对元组直接哈希的写法数组array[elem1, elem2]结构体struct定义一个字段齐全的 struct 后整体传入枚举enum将多个候选值表达为一个枚举实例。为什么必须这样做原因在Hashtrait 的设计上sha256T的约束是T: Hash即每个值都通过Hash::hash把自身写入Hasher。复合类型元组、结构体等的Hash实现会按固定顺序递归哈希其内部字段self.0.hash(state)、self.1.hash(state)……从而天然得到多个值拼接后的确定性摘要。参考文档将这类类型统称为复合类型compound types详细介绍见 built-ins 章节。深入源码Hasher 与 Hash trait要写出正确且高效的哈希代码理解 sway-lib-std/src/hash.sw 中的两个核心构件至关重要。Hasher字节缓冲构建器Hasher是一个内部持有Bytes缓冲区的结构体负责收集所有待哈希的字节pub struct Hasher { bytes: Bytes, }其核心方法包括Hasher::new()创建空缓冲区的 hasherHasher::with_capacity(capacity)按预估容量预分配缓冲区减少动态扩容开销write(ref mut self, bytes: Bytes)追加一段Bytes内容write_raw_slice(ref mut self, slice: raw_slice)追加一个原始字节切片write_u8(ref mut self, value: u8)追加单个字节write_str(ref mut self, s: str)追加字符串的原始字节注意不追加字符串长度只追加内容write_str_arrayS(ref mut self, s: S)追加字符串数组定长字符串的原始字节sha256(self) - b256/keccak256(self) - b256对缓冲区中累积的全部字节计算摘要并消费掉 hasher。源码中sha256与keccak256的最终实现是内联汇编分别调用 Fuel VM 的s256与k256指令把缓冲区指针和字节数交给虚拟机完成运算// Hasher::sha256 内部 asm(hash: result_buffer, ptr: self.bytes.ptr(), bytes: self.bytes.len()) { s256 hash ptr bytes; hash: b256 } // Hasher::keccak256 内部 asm(hash: result_buffer, ptr: self.bytes.ptr(), bytes: self.bytes.len()) { k256 hash ptr bytes; hash: b256 }这意味着哈希运算是原语级的由 Fuel 虚拟机直接支持而不是在 Sway 层用软件循环实现。Hash trait决定怎么把值写进去Hashtrait 定义了两个方法pub trait Hash { fn is_hash_trivial() - bool; fn hash(self, ref mut state: Hasher); }hash把self的哈希字节表示写入stateis_hash_trivial声明内存表示是否与哈希字节表示逐字节一致见下一节。标准库为绝大多数基础类型实现了Hash例如均位于 hash.swu8写入 1 字节u64写入其 8 字节内存b256写入 32 字节u256写入 32 字节bool写入1_u8true或0_u8false元组(A, B, ...)直到 8 元组按序递归哈希每个元素定长数组[T; N]循环哈希每个元素new_hashing关闭时Bytes、str、raw_slice、VecT、OptionT、ResultT, E等以及Address、ContractId、AssetId、Identity等链上类型见sway-lib-std/src/address.sw、contract_id.sw、asset_id.sw等文件中的Hash实现。因此凡是这些类型的值包括由它们组合成的 struct/enum都可以直接传给sha256/keccak256。性能优化trivial hashing 快路径sha256/keccak256的源码hash.sw展示了一个值得注意的 gas 优化分支pub fn sha256T(val: T) - b256 where T: Hash, { const IS_TRIVIAL: bool is_hash_trivial::T(); if IS_TRIVIAL { // 内存表示 哈希字节表示直接对原始内存做 s256 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 { // 走 Hasher 缓冲路径 let mut hasher Hasher::with_capacity(get_initial_capacity::T()); val.hash(hasher); hasher.sha256() } }当T::is_hash_trivial()返回true时编译器直接对值的原始内存执行s256/k256完全跳过构建中间字节缓冲区的过程这比先写入Hasher再计算要节省 gas。但trivial是一把双刃剑——is_hash_trivial的源码注释给出了大量警告性细节hash.swu16、u32在内存中占据 8 字节槽位但哈希字节表示分别只有 2 字节和 4 字节因此它们永远不是 trivial 的bool、u8、u16、u32作为 struct/tuple 字段时会被填充到 8 字节对齐包含这类字段的聚合类型不是 trivial 的枚举标签tag在内存中以u64存储但Hash实现按u8哈希因此枚举不是 trivial 的集合类型Bytes、Vec、raw_slice、str、str[N]、数组等在启用experimental_new_hashing特性时会在内容前加长度前缀此时其哈希字节表示不再等于内存表示也不再 trivial。标准库在处理u16/u32时特意规避了槽位问题例如impl Hash for u32 { fn is_hash_trivial() - bool { false } fn hash(self, ref mut state: Hasher) { // 从 8 字节槽位中取出低 4 字节 let ptr __addr_of(self).add::u8(4); state.write_raw_slice(raw_slice::from_parts::u8(ptr, 4)); } }结论你通常不需要手动实现Hash标准库的内置实现已经保证了正确性。只有当你在自定义类型上手动实现Hash时才需要谨慎判断is_hash_trivial的返回值——源码文档明确告诫返回true是强保证若误报true会产生错误哈希拿不准时返回false永远安全只是放弃优化。测试与已知哈希值仓库的端到端测试 test/src/e2e_vm_tests/test_programs/should_pass/language/b256_ops/src/main.sw 给出了sha256与keccak256的实测断言可以直接用作自测基准// 哈希单个 b256 值 sha256(aaaa) 0xe0e77a507412b120f6ede61f62295b1a7b2ff19d3dcc8f7253e51663470c888e keccak256(aaaa) 0x20ee8f1366f06926e9e8771d8fb9007a8537c8dfdb6a3f8c2cfd64db19d2ec90 // 哈希元组两个 b256 值的组合 sha256((aaaa, abaa)) 0xa4bca8eb8f338f7fda26960fa43bfe34fbc562e2ee0d7c6e8856c1c587f215ce keccak256((aaaa, abaa)) 0x4fce5a297040d82eecf7b0ae4855ad43698f191ee38820e27748648765bc42bd此外hash.sw 的文档注释中还给出字符串哈希的参考值let result sha256(Fuel); assert_eq(result, 0xa80f942f4112036dfc2da86daf6d2ef6ede3164dd56d1000eb82fa87c992450f); let result keccak256(Fuel); assert_eq(result, 0x4375c8bcdc904e5f51752581202ae9ae2bb6eddf8de05d5567d9a6b0ae4789ad);你可以把这些值写进自己的断言中验证实现的一致性。测试目录test/src/e2e_vm_tests下还包含大量哈希相关用例如hash_str、vec等可作为排查问题的参考。典型应用场景哈希在 Sway 合约中主要有以下落点均与本文的 API 直接相关存储键推导StorageMap的键最终会被哈希成存储槽位。官方文档在 storage/in-keyword.md 中明确指出默认存储位置由变量名经sha256(storage.var_name)计算得出而StorageMap之所以要求键是单值正是因为键需要可哈希详见 storage-map 文档。当你的业务键包含多个字段时就可以像测试代码那样用sha256((key, i, slot))之类的元组包装来表达复合键。地址与身份生成Address、ContractId、Identity等类型本身就是哈希的产物官方文档 namespace/address.md 提到地址值是某类哈希的结果而std中这些类型也都实现了Hash可以参与进一步的组合哈希。数据指纹与防篡改在合约内对敏感输入做sha256/keccak256摘要后进行比对如签名消息的规范化、承诺-揭示方案中的承诺值。注意事项小结先导入再使用use std::hash::*;或按需use std::hash::{sha256, keccak256, Hasher};。多值必须包装sha256/keccak256只接受一个参数多个值请用 tuple/array/struct/enum 组合成一个值再哈希。字符串不追加长度Hasher::write_str只写入内容字节若你的场景需要内容长度的编码语义需要显式处理这正与experimental_new_hashing特性下的行为差异相关。选择算法看生态与以太坊系工具链如secp256k1恢复签名、以太坊 ABI交互倾向于keccak256Fuel 原生体系存储键、std内部逻辑两者皆可使用但不同算法摘要不同切勿混用。自定义Hash实现需谨慎is_hash_trivial误报true会导致错误哈希不确定时返回false。通过本文你应当已经掌握了 Sway 哈希 API 的完整使用路径从forc项目的依赖配置到sha256/keccak256的导入调用再到基于Hasher的多值流式哈希以及Hashtrait 与 trivial hashing 优化背后的实现原理。接下来你可以直接在forc test环境中用上文给出的已知哈希值验证你的代码并将这些模式应用到存储键设计、签名验证等实际合约逻辑中。【免费下载链接】sway Empowering everyone to build reliable and efficient smart contracts.项目地址: https://gitcode.com/GitHub_Trending/sw/sway创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表