用Fe语言重写以太坊存款合约

cburgdorf 发布于 2026-07-08 阅读 10

文章作者用Fe语言重写了以太坊存款合约,无需内联汇编或手动内存管理,代码更简洁。与最优化Solidity版本相比,Fe实现字节码更小(运行时代码减少16.9%),部署Gas降低6.5%,每次调用Gas平均节省3.9%。文章详细介绍了Fe的消息模型、事件、存储结构及存款逻辑,并附有完整对比数据。

图像

我用 Fe 重写了以太坊存款合约——也就是每个质押的 ETH 都要经过的合约。没有内联汇编,没有手动内存管理,全是高级代码。它在字节码大小、部署成本和调用 Gas 上依然优于经过最优化编译的 Solidity 版本。

让我们深入了解!

什么是存款合约?

存款合约是以太坊中执行层与共识层之间的桥梁。想要运行验证者的人需要向该合约存入 32 ETH,并提供一些信息,例如验证者的公钥和提款凭证。存款合约维护一个包含所有存款的默克尔树,并生成存款事件,共识层可以监听这些事件并激活相应的验证者。

用 Fe 实现存款合约

DepositMsg 消息

Fe 中的合约遵循基于消息的编程模型。合约并没有可调用的函数,而是接收消息,合约的行为由其处理这些消息的方式定义。

这不仅类似于 EVM 底层的运行方式(消息是交互的基本单元),还为 Fe 带来了更强大的编程模型,我将在未来的文章中探讨这一点。

现在,我们来定义 DepositMsg,它包含实现合约对外功能所需的四个变体:DepositSupportsInterfaceGetDepositRootGetDepositCount。每个变体都有一个选择器,用于定义外部如何调用它,以及它包含哪些字段。Deposit 是主要的变体,当用户想要进行存款时会调用它。其他三个变体用于查询合约状态。

msg DepositMsg {
    #[selector = sol("deposit(bytes,bytes,bytes,bytes32)")]
    Deposit {
        pubkey: Bytes,
        withdrawal_credentials: Bytes,
        signature: Bytes,
        deposit_data_root: Bytes32,
    },

    #[selector = sol("supportsInterface(bytes4)")]
    SupportsInterface { interface_id: Bytes4 } -> bool,

    #[selector = sol("get_deposit_root()")]
    GetDepositRoot -> u256,

    #[selector = sol("get_deposit_count()")]
    GetDepositCount -> Bytes,
}

注意:选择器属性中的 sol 函数并不是什么神奇的编译器技巧,它实际上是一个在标准库中定义的 const fn,在编译时计算 4 字节选择器。

事件和存储结构体

结构体是 Fe 中定义数据结构的主要方式。与 Rust 一样,你可以通过 impl 块为结构体添加函数,并使用泛型和 trait 创建强大的抽象。结构体也是 Fe 中定义事件和存储布局的常用方式。

要将结构体用作事件,它需要实现 Event trait。幸运的是,这不需要手动完成:用 #[event] 属性标注结构体会让编译器自动实现该 trait。

#[event]
struct DepositEvent {
    pubkey: Bytes,
    withdrawal_credentials: Bytes,
    amount: Bytes,
    signature: Bytes,
    index: Bytes,
}

与 Solidity 不同,Fe 中的函数不能自动访问全局状态。相反,你必须显式地将 存储效果 传递给每个希望读写存储的函数,并且必须在合约级别声明存储效果。你还需要明确表达意图,说明是只读还是可写。

处理存储的一种常见方法是将连贯的存储片段封装到一个结构体中,如下面的 DepositStore

struct DepositStore {
    branch: [u256; DEPOSIT_CONTRACT_TREE_DEPTH],
    deposit_count: u256,
    zero_hashes: [u256; DEPOSIT_CONTRACT_TREE_DEPTH],
}

pub contract DepositContract uses (ctx: Ctx, mem: mut RawMem, log: mut Log) {
    mut store: DepositStore, // <-- 这里 `DepositStore` 作为可变的存储效果可用
    // 合约其余部分省略
}

接受存款

存款合约的主要任务是接受存款并维护存款的默克尔树。Fe 实现是对等价的 Solidity 代码进行逐行翻译,但尽可能使用更高级的抽象。由于 Fe 自带丰富的标准库,我可以从 ssz 导入一些方便的功能,以帮助进行存款合约所需的基于 SSZ 的哈希和序列化。

第一段代码是对输入数据的验证。我还根据输入数据重新计算 deposit_data_root,并检查它是否与用户提供的值匹配。如果输入数据无效,尽早失败并给出清晰的错误信息非常重要,以防止用户因发送格式错误的存款而丢失资金。请注意,我故意将 compute_deposit_data_root 实现为一个自由函数,因为这样在单元测试中使用也很方便。

接下来,我发出包含存款相关信息的 DepositEvent。这样共识层就能监听新存款并激活相应的验证者。

注意,在 Fe 中,事件通过 log 效果发出,DepositEvent 结构体可以直接传递给 emit 函数,无需手动编码。在后台,编译器会生成必要的代码来实现 Event trait,因为该结构体使用了 #[event] 标注。

最后,我用新存款更新默克尔树。由于这涉及更新合约状态,因此需要合约级别声明的 store 效果。

pub contract DepositContract uses (ctx: Ctx, mem: mut RawMem, log: mut Log) {
    mut store: DepositStore,

    init()
    uses (mut store, mut mem)
    {
        for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH - 1 {
            let cur: u256 = store.zero_hashes[height]
            store.zero_hashes[height + 1] = ssz::hash_pair(left: cur, right: cur)
        }
    }

    recv DepositMsg {
        #[payable]
        Deposit { pubkey, withdrawal_credentials, signature, deposit_data_root }
        uses (mut store, ctx, mut mem, mut log)
        {
            assert!(pubkey.len == 48, "DepositContract: invalid pubkey length")
            assert!(withdrawal_credentials.len == 32, "DepositContract: invalid withdrawal_credentials length")
            assert!(signature.len == 96, "DepositContract: invalid signature length")

            let value: u256 = ctx.value()
            assert!(value >= ether(1), "DepositContract: deposit value too low")
            assert!(value % gwei(1) == 0, "DepositContract: deposit value not multiple of gwei")
            let deposit_amount: u256 = value / gwei(1)
            assert!(deposit_amount <= u64::MASK, "DepositContract: deposit value too high")
            // 截断转换匹配 Solidity 的 `uint64(x)`。上面的断言保证信息不会丢失。
            let amount_gwei: u64 = deposit_amount.downcast_truncate()

            let mut node: u256 = compute_deposit_data_root(
                pubkey,
                withdrawal_credentials,
                signature,
                amount_gwei,
            )
            assert!(
                node == deposit_data_root.val,
                "DepositContract: reconstructed DepositData does not match supplied deposit_data_root",
            )

            // 发出 DepositEvent(index 是自增前的 deposit_count)。
            log.emit(
                DepositEvent {
                    pubkey,
                    withdrawal_credentials,
                    amount: ssz::serialize_u64(amount_gwei),
                    signature,
                    index: ssz::serialize_u64(store.deposit_count.downcast_truncate()),
                },
            )

            assert!(store.deposit_count < MAX_DEPOSIT_COUNT, "DepositContract: merkle tree full")
            store.deposit_count += 1

            let mut size: u256 = store.deposit_count
            for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH {
                if size & 1 == 1 {
                    store.branch[height] = node
                    return
                }
                node = ssz::hash_pair(left: store.branch[height], right: node)
                size = size / 2
            }
            // 不可达:上面的 MAX_DEPOSIT_COUNT 检查保证我们在 DEPOSIT_CONTRACT_TREE_DEPTH 次迭代内通过 `size & 1 == 1` 分支退出。
            assert!(false)
        }
        // 其他消息处理程序省略
    }
}

// 从各部分重新构造存款数据的 SSZ 根。在模块级别暴露,以便测试可以预计算有效的 `deposit_data_root` 传入。
#[inline(always)]
pub fn compute_deposit_data_root(
    pubkey: Bytes,
    withdrawal_credentials: Bytes,
    signature: Bytes,
    amount_gwei: u64,
) -> u256
uses (mem: mut RawMem)
{
    assert!(withdrawal_credentials.len == 32)
    (
        ssz::hash_tree_root<ssz::ByteVector<48>>(pubkey),
        withdrawal_credentials.word_at(0),
        ssz::u64_chunk(amount_gwei),
        ssz::hash_tree_root<ssz::ByteVector<96>>(signature),
    )
        .merkleize()
}

其他消息处理程序

剩下的处理程序很简单。GetDepositCount 返回当前存款数量,SupportsInterface 实现标准的 ERC-165 接口发现,两者基本上都是一行代码,你可以在文末链接的完整源码中找到。唯一有趣的是 GetDepositRoot,它计算存款树的当前默克尔根:

GetDepositRoot -> u256 uses (store, mut mem) {
    let mut node: u256 = 0
    let mut size: u256 = store.deposit_count
    for height in 0 .. DEPOSIT_CONTRACT_TREE_DEPTH {
        if size & 1 == 1 {
            node = ssz::hash_pair(left: store.branch[height], right: node)
        } else {
            node = ssz::hash_pair(left: node, right: store.zero_hashes[height])
        }
        size = size / 2
    }
    // 使用 SSZ mix-in-length 在 `deposit_count` 上最终化。
    // MAX_DEPOSIT_COUNT = 2**32 - 1 意味着截断转换永远不会丢失信息。
    ssz::mix_in_length(root: node, len: store.deposit_count.downcast_truncate())
}

Gas 费用与字节码大小比较

现在来看数据。我通过一系列测试和基准测试运行了两个实现,结果如下:

bytecode size
path         fe-O2        sol     sol-IR   fe vs sol-IR
-----------------------------------------------------------
init          2562       4819       3082  -520 (-16.9%)
runtime       2478       4445       2844  -366 (-12.9%)

deployment gas
path        fe-O2        sol     sol-IR    fe vs sol-IR
-----------------------------------------------------------
deploy    1290072    1738847    1379411  -89339 (-6.5%)

call gas
path                            fe-O2        sol     sol-IR    fe vs sol-IR
-------------------------------------------------------------------------------
supportsInterface(erc165)       21600      21603      21600      +0 (+0.0%)
supportsInterface(deposit)      21600      21641      21600      +0 (+0.0%)
supportsInterface(unknown)      21600      21641      21600      +0 (+0.0%)
get_deposit_root(empty)        102969     117558     109178   -6209 (-5.7%)
get_deposit_count(empty)        23616      24510      24099    -483 (-2.0%)
deposit#0                       79719      84626      81089   -1370 (-1.7%)
get_deposit_root(after#0)      102977     117570     109174   -6197 (-5.7%)
get_deposit_count(after#0)      23616      24510      24099    -483 (-2.0%)
deposit#1                       65088      70411      66629   -1541 (-2.3%)
get_deposit_root(after#1)      102977     117570     109174   -6197 (-5.7%)
get_deposit_count(after#1)      23616      24510      24099    -483 (-2.0%)
deposit#2                       45507      50414      46877   -1370 (-2.9%)
get_deposit_root(after#2)      102985     117582     109170   -6185 (-5.7%)
get_deposit_count(after#2)      23616      24510      24099    -483 (-2.0%)
TOTAL                          761486     838656     792487  -31001 (-3.9%)

Fe 实现全面优于经过最优化编译的 Solidity 构建:字节码更小,部署消耗更少的 Gas,并且每次合约交互至少同样便宜,大多数情况下更便宜。约束条件三:✅

结论

以上就是整个合约:高级、可读的 Fe,在大小、部署和每次调用上都击败了经过 Gas 优化的最优化 Solidity 版本。你可以在 GitHub 仓库 中找到完整代码,包括我上面跳过的处理程序。如果你对此产生了兴趣,Fe 文档 是开始使用该语言的最佳起点。我计划在未来的文章中介绍更复杂的合约,我很想听听你的想法,欢迎随时回复。

  • 原文链接: x.com/cburgdorf/status/2...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论