Huff

Huff 安装

官网:https://docs.huff.sh

Windows系统下,能够成功按照 rust 和 Foundry 的话,直接通过 cargo 命令一次就能成功的安装 Huff。如果没有成功安装 Rust 和 Foundry,大致解决方法:

  • 安装 gcc (我没安装 Visul Studio tool C++ 生成工具,那玩意太大了也不好按,网上有关于 Visul Studio tool C++ 生成工具的轻量级 C++ 生成工具的替代
  • 为 git 配置了代理,因为我感觉 cargo 有时候还是用的 git ,我是之前装 Foundry 卡了好几次,之前为了传博客给 git 挂了代理之后方便,安装 Huff 的时候一次成功
  • rust 安装直接在 c++ 生成工具安装完毕后吗,直接通过 rustup 安装即可。
  • foundry 安装通过 cargo 下载即可,Foundry 官方文档中有该命令

Huff 入门

基本构成:

Huff 需要主函数(main)函数,需要宏,注意,定义宏时,宏函数名需要全大写,然后加上=takesreturns,takes 表示从堆栈上取的字节,returns 表示返回到堆栈上的字节。

#define macro MAIN() = takes(0) returns(0) {}

编译 huff:

huffc path (-b)

使用命令:huffc "路径.huff"

如果要获取对应的 solidity bytecode 可以加上 -b 选项

上面的例子:

#define macro MAIN() = takes(0) returns(0) {}

中,虽然我们没有runtimec code,也没有编写任何代码,但是我们通过运行命令得到了相对应的 solidity bytecode60008060093d393df3。得到的这一部分其实就是上面提到的 contract creation code 这是 huff 编译器知道我们至少需要代码创建合约尽管我们编译这个huff合约不做任何事情。

对于刚刚的60008060093d393df3,我们可以对其进行分析:

比如 opcoded 39 是codecopy,上面的F3 3D 39确实看到了一个代码复制操作

huffc path —bin-runtime

该命令是获取现在的 huff 代码在区块链上的 runtime code

huffc path --bin-runtime

比如下面的代码:

// 60008060093d393df3

#define macro MAIN() = takes(0) returns(0) {
    // 0x00             // [0]
    // // 0x02           // TOP [2, 0] BOTTOM
    // calldataload    // [calldata (32)] 
    // // how do we cut down the calldata -> func selector?
    // // right shift 28 bytes, 224 bits | 224 -> 0xe0
    // 0xe0            // TOP [0xe0, calldata (32)] BOTTOM
    // shr             // [func selector]
    0x00 calldataload 0xe0 shr // [function_selector]
        
    // jump => function data associated with the selector
    // If f_select == updateHorseNumber -> jump to that code
    // If f_select == readHorseNumber -> jump to that code
    // 0xcdfead2e == update 
    // 0xe026c017 == read 

    // updateHorseNumber selector
    0xcdfead2e          // [0xcdfead2e. function_selector]
    eq                  // [true_if_func_selector_matches]
    // jump to updateHorseNumber code if true
    updateJump          // [updateHorseNumberProgramCounter, true/false]
    jumpi
    updateJump:
        SET_NUMBER_OF_HORSE()
}

#define macro SET_NUMBER_OF_HORSE() = takes(0) returns(0) {}

运行huffc path --bin-runtime的结果为:5f3560e01c63cdfead2e1461000f575b

Huff: __FUNC_SIG & Interface

在 Huff 中,我们无需通过 cast sig 命令获取函数选择器(function selector),Huff 提供了一个语法可以使得我们无需计算函数选择器:__FUNC_SIG()

同时,可以像在 Solidity 中一样定义一个接口 (Interface)

注意:

  • huff 中,必须标记函数是否为payable
  • 并且,要加上 returns() 即使没有返回值

代码如下:

/* Interface */
#define function updateHorseNumber(uint256) nonpayable returns()
#define function readNumberOfHorses() view returns(uint256)

#define marco MAIN() = takes(0) returns(0) {
	...
    __FUNC_SIG(readNumberOfHorses) // 等价于手动通过命令 cast sig 计算得到的函数 `updateHorseNumber(uint256)` 的选择器
}

这个语法糖可以很好的帮助我们计算函数选择器

Huff: FREE_STORAGE_POINTER

Huff 内置的一些语法使得我们能够更加轻松的处理内存,他们拥有抽象,有一个关键字称为FREE_STORAGE_POINTER()其本质上只是一个计数器,用于显示那些存储插槽是打开的。我们可以在 huff 文件中通过FREE_STORAGE_POINTER为我们的 slot 值定义一个常量。当然我们可以硬编码,但是每个 slot 存储 32 字节,最好还是使用 Huff 的内置语法来实现 Storage 的存储。

#define constant NUMBER_OF_HOURSES_STORAGE_SLOT = REFF_STORAGE_POINTER()

Huff:访问常量变量

如上,定义了对应slot位置常量之后,就可以直接传入该常量,注意:使用时,要把常量名用[]包裹

#define constant NUMBER_OF_HOURSES_STORAGE_SLOT = FREE_STORAGE_POINTER()

#define macro SET_NUMBER_OF_HORSES() = takes(0) returns(0) {
    // 1. Get the value to store from calldata
    
    // 2. Give it a storage slot
    [NUMBER_OF_HOURSES_STORAGE_SLOT]
    // 3. sstore opcode

}

从 calldata 访问函数参数

Huff opcode

push

在 Huff 中执行 push opcode 时,直接把需要 push 到堆栈中的数据写下来即可,因为 huff 编译器能够根据数据的大小,智能的选择对应的 push 系操作码

所以,在 Huff 中执行 push opcode,只需要输入实际的值即可

当有人向智能合约发送任何调用数据时,智能合约现在唯一要做的就是将 0 push 到堆栈上去。

举例:

#define macro MAIN() = takes(0) returns(0) {
    0x00
}

将下面的代码用 huffc 编译结果:60018060093d393df35f,得到的结果是比刚刚合约创建时的代码更长一些。在f3后,多了一个5f opcode 这个字节码就是 PUSH0

calldataload

接收一个堆栈输入(Stack input),并给出一个堆栈输出(Stack output),所以堆栈输入的数据加载是我们首先将零推到堆栈上(PUSH0 5f)的原因.

查看调用数据,抓取栈顶的值,作为偏移量(offset)

Stack input 堆栈输入

i: byte offset in the calldata

calldata 中的字节偏移量。

Stack output 堆栈输出

data[i]: 32-byte value starting from the given offset of the calldata. All bytes after the end of the calldata are set to 0.data[i]

从给定的 calldata 偏移量开始的 32 字节值。calldata 结束之后的所有字节都设置为 0。

#define macro MAIN() = takes(0) returns(0) {
    0x00          // [0]
    // 0x02         // TOP [2, 0] BOTTOM
    calldataload  // [calldata] (The first 32 bytes of calldata)
}

image-20240304190428283

最开始的calldata为:0xcdfead2e0000000000000000000000000000000000000000000000000000000000000001

因为 calldataload 只能获取32 个字节,所以此时堆栈中的 calldata 其实是:0xcdfead2e00000000000000000000000000000000000000000000000000000000(但其实不是我们想要的)

shr(Right Shift) 右移位

Shift the bits towards the least significant one. The bits moved before the first one are discarded, the new bits are set to 0.

将位移向最不重要的位。丢弃第一个位之前移动的位,新位设置为 0。

Stack input 堆栈输入

  1. shift: number of bits to shift to the right.shift 向右移动的位数。
  2. value: 32 bytes to shift. 要移位的 32 个字节。

Stack output 堆栈输出

value >> shift: the shifted value. If shift is bigger than 255, returns 0.value >> shift

移位值。如果 shift 大于 255,则返回 0。

举例:

// SHR opcode
// 0x0102 (bytes)
// 1 bytes = 8 bits 
// 0b100000010 >> 2
//  0b1000000

可以理解为 value 的二进制数据全部向右移两位(直接去掉后两位)

举例:evm-code playground

PUSH2 0x0102
PUSH1 0x04
SHR

堆栈如下:

image-20240304192101966

shr on calldata

对于calldata:0xcdfead2e0000000000000000000000000000000000000000000000000000000000000001 我们起初想要的其实是0xcdfead2e(函数选择器 selector)

刚刚我们已经使用了calldataload,获得了 calldata(32bytes):0xcdfead2e00000000000000000000000000000000000000000000000000000000。但是接下来我们需要函数选择器,那么,借助 shr 右移位 224 即可(56 / 2 = 28 bytes 28 * 8 = 224 bits | 224 -> 0xe0)

EQ

从栈顶弹出两个数据进行比较,相同将 1 压入堆栈,不同将 0 压入堆栈。

Stack input 堆栈输入

  1. a: left side integer. 左边整数。
  2. b: right side integer 右边整数。

Stack output 堆栈输出

a == b: 1 if the left side is equal to the right side, 0 otherwise.a == b 如果左侧等于右侧,则为 1,否则为 0。

JUMP & JUMPI & JUMPDEST

The program counter (PC) is a byte offset in the deployed code. It indicates which instruction will be executed next. When an ADD is executed, for example, the PC is incremented by 1, since the instruction is 1 byte. The PUSH instructions are bigger than one byte, and so will increment the counter accordingly.

程序计数器 (PC) 是已部署代码中的字节偏移量。它指示接下来将执行的指令。例如,当执行 ADD 时,PC 会递增 1,因为指令是 1 字节。PUSH 指令大于一个字节,因此会相应地递增计数器。

The JUMP instruction alters the program counter, thus breaking the linear path of the execution to another point in the deployed code. It is used to implement functionalities like functions.JUMP

指令更改程序计数器,从而将执行的线性路径中断到已部署代码中的另一个点。它用于实现功能等功能。

JUMP:

Stack input 堆栈输入

counter: byte offset in the deployed code where execution will continue from. Must be a JUMPDEST instruction.

已部署代码中的字节偏移量,从中继续执行。必须是 JUMPDEST 指令。

JUMPI:

Stack input 堆栈输入

第一个值是 JUMPDEST 的偏移量,第二个值是 true/false

counter: byte offset in the deployed code where execution will continue from. Must be a JUMPDEST instruction.

已部署代码中的字节偏移量,从中继续执行。必须是 JUMPDEST 指令。

b: the program counter will be altered with the new value only if this value is different from 0. Otherwise, the program counter is simply incremented and the next instruction will be executed.

只有当新值与不为 0 时,程序计数器才会更改为新值。否则,程序计数器将简单地递增,并执行下一条指令。

JUMPDEST

Mark a valid destination for JUMP or JUMPI. This operation has no effect on machine state during execution.

标记 JUMP 或 JUMPI 的有效目的地。此操作在执行期间对计算机状态没有影响。

使用 JUMP 和 JUMPI 需要计算程序计数器(PC)(因为 JUMP 和 JUMPI 的目标都必须是 JUMPDEST),但是 Huff 有相关的语法可以帮助我们,我们可以在这里写一些文本,将定义某个宏的程序计数器(PC)

比如下面的代码:

    0xe026c017          // [0xe026c017, function_selector]
    eq                  // [true_if_func_selector_matches]
    readJump            // [readJump, true_if_func_selector_matches]
    jumpi

    readJump:
        GET_NUMBER_OF_HORSES()
        
#define macro GET_NUMBER_OF_HORSES() = takes(0) returns(0) {}

堆栈图:

image-20240305195047849

DUP

DUP 是一个大类的 opcode ,其范围为 1 ~ 16(即 DUP1 ~ DUP16,对应 0x80 ~ 0x8f),其功能是复制目前从栈顶开始的第 n 个值,并将复制得到的值压入堆栈

DUP 代码举例:

dup1                // [function_selector, function_selector]
0xcdfead2e          // [0xcdfead2e, function_selector, function_selector]
eq                  // [true_if_func_selector_matches, function_selector]

堆栈图:

image-20240305200602341

接着以 DUP2 为其他的举例:

image-20240305200645472

REVERT

Stop the current context execution, revert the state changes (see STATICCALL for a list of state changing opcodes) and return the unused gas to the caller. It also reverts the gas refund to its value before the current context. If the execution is stopped with REVERT, the value 0 is put on the stack of the calling context, which continues to execute normally. The return data of the calling context is set as the given chunk of memory of this context.

停止当前上下文执行,恢复状态更改(有关状态更改操作码的列表,请参阅 STATICCALL),并将未使用的 gas 返回给调用方。它还会将 gas 退款恢复到当前上下文之前的值。如果使用 REVERT 停止执行,则值 0 将放在调用上下文的堆栈上,该堆栈将继续正常执行。调用上下文的返回数据设置为此上下文的给定内存块。

Stack input 堆栈输入

  1. offset: byte offset in the memory in bytes. The return data of the calling context.offset:内存中的字节偏移量(以字节为单位)。调用上下文的返回数据。
  2. size: byte size to copy (size of the return data).size:要复制的字节大小(返回数据的大小)。
SSTORE

该操作码将堆栈中的数据存储到 Storage 中。

Stack input

  1. key: 32-byte key in storage.key:存储中的 32 字节密钥。
  2. value: 32-byte value to store.value:要存储的 32 字节值。
STOP

Exits the current context successfully.

成功退出当前上下文。

When a call is executed on an address with no code and the EVM tries to read the code data, the default value is returned, 0, which corresponds to this instruction and halts the execution.

在没有代码的地址上执行调用并且 EVM 尝试读取代码数据时,将返回默认值 0,该值对应于此指令并停止执行。

当我们执行完我们想要的目的后,我们就需要加上 STOP 字节码,使用STOP程序会推出执行(不会回滚)。

如果不使用 STOP 程序会继续执行,并且会浪费 gas。

SLOAD

Stack input 堆栈输入

key: 32-byte key in storage

存储的32字节密钥。

Stack output 堆栈输出

value: 32-byte value corresponding to that key. 0 if that key was never written before.

与该键对应的 32 字节值。如果该键以前从未写入过,则为 0。

MSTORE

Stack input 堆栈输入

  1. offset: offset in the memory in bytes.
    内存中的偏移量(以字节为单位)。
  2. value: 32-byte value to write in the memory.
    要写入内存的 32 字节值。
RETURN

成功退出当前上下文。

Stack input

  1. offset: byte offset in the memory in bytes, to copy what will be the return data of this context.
    内存中的字节偏移量(以字节为单位),以复制此上下文的返回数据。
  2. size: byte size to copy (size of the return data).
    要复制的字节大小(返回数据的大小)

本质上和 Return 相同,只是它会返回一些数据。并且,他是从内存中返回数据,因此我们想返回一个值就需要把这个值写进内存中去。将数据写到内存中,需要使用MSTORE关键字

通过 huff 实现函数调用

  1. 首先PUSH一个0x00,然后调用 calldataload 获取 calldata 的前32位数据(calldataload 使用时,从堆栈弹出一个值,这个值是读取 calldata 的偏移量(起始位置))
  2. 然后 PUSH 要右移的位数,使用SHR右移
  3. 复制此时栈顶的函数选择器
  4. 压入合约中已有的函数选择器,EQ 判断,将判断结果压入堆栈
  5. 压入 JUMPDEST的偏移量(可通过 huff 宏编程)
  6. JUMPI判断刚刚 EQ结果是否为真,若真则跳转到对应 JUMPDEST
  7. 重复刚刚的过程
  8. 最后压入两个 0x00 然后REVERT避免非预期输出
// 60008060093d393df3

/* Interface */
#define function updateHorseNumber(uint256) nonpayable returns()
#define function readNumberOfHorses() view returns(uint256)


#define macro MAIN() = takes(0) returns(0) {
    0x00 calldataload 0xe0 shr               // [function_selector]
    dup1                                     // [function_selector, function_selector]
    __FUNC_SIG(updateHorseNumber)            // [0xcdfead2e, function_selector, function_selector]
    eq                                       // [true_if_func_selector_matches, function_selector]

    // jump to updateHorseNumber code if true
    updateJump                               // [updateHorseNumberProgramCounter, true/false, function_selector]
    jumpi                                    // [function_selector]

    // readNumberOfHorses, 0xe026c017
    __FUNC_SIG(readNumberOfHorses)           // [0xe026c017, function_selector]
    eq                                       // [true_if_func_selector_matches]
    readJump                                 // [readJump, true_if_func_selector_matches]
    jumpi

    0x00 0x00 revert    

    updateJump:
        SET_NUMBER_OF_HORSES()
    readJump:
        GET_NUMBER_OF_HORSES()
}

#define macro SET_NUMBER_OF_HORSES() = takes(0) returns(0) {}

#define macro GET_NUMBER_OF_HORSES() = takes(0) returns(0) {}

通过 Huff 向 Storage 中写入数据

这里向 Storage 中写入的数据是 calldata 中的参数

  1. 首先PUSH 0x04作为 CALLDATALOAD 的偏移,这样得到的直接就是 calldata 中的参数
  2. 压入要写入的 slot 的值(这里使用了 Huff 的语法糖,利用FREE_STORAGE_POINTER空闲存储指针定义的常量)
  3. 调用SSTORE写入Storage
  4. 调用STOP终止上下文的执行(此时已经执行完成,使用 STOP 终止,防止后续还会继续消耗 gas)
#define constant NUMBER_OF_HOURSES_STORAGE_SLOT = FREE_STORAGE_POINTER()

#define macro SET_NUMBER_OF_HORSES() = takes(0) returns(0) {
    // 1. Get the value to store from calldata
    0x04                                      // [0x04]
    calldataload                              // [value]
    // 2. Give it a storage slot
    [NUMBER_OF_HOURSES_STORAGE_SLOT]          // [storage_slot, value]
    sstore
    stop
    // 3. sstore opcode
}

通过 Huff 读取 slot 中的值

  1. 压入要写入的 slot 的值(这里使用了 Huff 的语法糖,利用FREE_STORAGE_POINTER空闲存储指针定义的常量)
  2. SLOAD从堆栈中弹出要读取的 Storage 的 slot 的值
  3. PUSH 0x00 ,然后调用 MSTORESLOAD 读取来的值存储到内存中。
  4. PUSH 0x00, PUSH 0x00 然后RETURN把内存中的值返回
#define constant NUMBER_OF_HOURSES_STORAGE_SLOT = FREE_STORAGE_POINTER()

#define macro GET_NUMBER_OF_HORSES() = takes(0) returns(0) {
    // 1. Get the storage slot
    // 2. Load the value of that slot into memory
    // 3. Return
    [NUMBER_OF_HOURSES_STORAGE_SLOT]        // [key]
    sload                                   // [value]
    0x00                                    // [0, value]
    mstore                                  // []           // Memory: [value]

    // 0x20 == 32 bytes
    0x20 0x00 return                        // []
}