title: 14. 映射 tags: huff interface mapping bytecode WTF Huff极简入门: 14. 映射 我最近在重新学Huff,巩固一下细节,也写一个“Huff极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 推特:@0xAAScience 社区:Discord|微信群|官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTF-Huff 这一讲,我们介绍如何在Huff中使用映射( ),包括将其保存到状态变量和在函数中返回。 映射 在Solidity合约中,我们经常会用到映射变量。Huff并不原生支持映射类型,但是我们可以根据Solidity的存储布局,在Huff中实现它。
title: 14. 映射 tags: - huff - interface - mapping - bytecode
我最近在重新学Huff,巩固一下细节,也写一个“Huff极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。
所有代码和教程开源在github: github.com/AmazingAng/WTF-Huff
这一讲,我们介绍如何在Huff中使用映射(mapping),包括将其保存到状态变量和在函数中返回。
在Solidity合约中,我们经常会用到映射变量。Huff并不原生支持映射类型,但是我们可以根据Solidity的存储布局,在Huff中实现它。
要实现的Solidity合约:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; contract Mapping { mapping(uint => uint) map; function setMap(uint key, uint value) external{ map[key] = value; } function getMap(uint key) external view returns(uint value){ value = map[key]; } }
我们以mapping(uint => uint)类型为例,介绍Solidity的mapping类型的变量如何在存储中布局的。更多关于存储布局的内容可以参照Solidity文档。由于mapping类型不会做为calldata或returndata的内容,所以我们不需要考虑它们。
mapping类型的存储布局与动态数组类似。假设我们开始从存储槽p开始存储一个mapping(uint => uint)类型的数据map,那么slot p不直接存储任何映射的内容。对应于映射键k的值位于keccak256(h(k) . p), 其中.是连接符,h是一个函数,根据键的类型应用于键。
对于值类型,函数h将与在内存中存储值的相同方式来将值填充为32字节。比如uint8类型的1会被填充为0000000000000000000000000000000000000000000000000000000000000001。
对于字符串和字节数组,h(k)只是未填充的数据。
也就是说,如果一个映射键为1,存储槽为0,它的值所在的存储槽由keccak256(00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000)的结果给出,也就是slot 0xad3228b676f7d3cd4284a5443f17f1962b36e491b30a40b2405849e597ba5fb5。
下面我们来用Huff实现setMap()函数,将uint类型的键值对存入状态变量中。它的逻辑就是:
calldata中读取键值对的值。/* 接口 */ #define function setMap(uint256 key, uint256 value) nonpayable returns () #define function getMap(uint256 key) view returns (uint256 value) /* 方法 */ #define macro SET_MAP() = takes (0) returns (0) { // 假设我们将mapping保存在slot 0中,键值对为 k-v 0x24 calldataload // [v] 0x04 calldataload // [k, v] 0x00 mstore // [v] memory: [0x00: k] 0x40 0x00 sha3 // [sha3(k.0), v] memory: [0x00: k] sstore // [] memory: [0x00: k] storage [sha3(k.0): v] stop }
下面我们实现getMap()函数,它的主要逻辑:
calldata中读取键。sload读取键对应的值。return返回。#define macro GET_MAP() = takes (0) returns (0) { // 假设我们将mapping保存在slot 0中 0x04 calldataload // [k] 0x00 mstore // [] memory: [0x00: k] 0x40 0x00 sha3 // [sha3(k.0)] memory: [0x00: k] sload // [v] memory: [0x00: k] // 将值存入内存 0x00 mstore // [] memory: [0x00: v] // 返回值 0x20 0x00 return }
最后,我们在MAIN宏中通过selector判断要调用哪个函数。
#define macro MAIN() = takes (0) returns (0) { // 通过selector判断要调用哪个函数 0x00 calldataload 0xE0 shr dup1 __FUNC_SIG(setMap) eq set_map jumpi dup1 __FUNC_SIG(getMap) eq get_map jumpi // 如果没有匹配的函数,就revert 0x00 0x00 revert set_map: SET_MAP() get_map: GET_MAP() }
我们可以使用huffc命令获取上面合约的runtime code:
huffc src/14_Mapping.huff -r
打印出的bytecode为:
5f3560e01c80636c4668ea1461001e578063025799f01461002d575f5ffd5b6024356004355f5260405f2055005b6004355f5260405f20545f5260205ff3
将这段字节码复制到evm.codes playground。首先,我们调用setMap()函数。将Calldata设为0x6c4668ea00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002(调用setMap函数,键为1,值为2)并点击运行。右下角的Storage被相应更改,运行成功。

接下来,我们调用getMap()函数,读取键对应的值。将Calldata设为0x025799f00000000000000000000000000000000000000000000000000000000000000001(调用getMap函数,查询的键为1),并运行。可以看到,右下角的RETURN VALUE为0000000000000000000000000000000000000000000000000000000000000002,与预期一致,运行成功。

我们可以使用Foundry写一个测试,用Solidity来验证咱们写的Huff合约是否能正常工作。
测试合约:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.15; import "foundry-huff/HuffDeployer.sol"; import "forge-std/Test.sol"; import "forge-std/console.sol"; contract MappingTest is Test { /// @dev Address of the I14_Mapping contract. I14_Mapping public i14_Mapping; /// @dev Setup the testing environment. function setUp() public { i14_Mapping = I14_Mapping(HuffDeployer.deploy("14_Mapping")); } /// @dev Ensure that you can set and get the value. function testSetAndGetMapping() public { uint k = 1; uint v = 2; i14_Mapping.setMap(k, v); assertEq(v, i14_Mapping.getMap(k)); } } interface I14_Mapping { function getMap(uint256) external view returns (uint256); function setMap(uint256, uint256) external; }
在命令行输入中输入forge test运行测试合约,可以看到测试通过!

这一讲,我们介绍了如何在Huff中写入并读取mapping类型的映射,并在evm.codes上成功运行了合约。