首页
/ fuels-rs Predicate(谓词)深度实战:在 Fuel 网络上实现脚本化条件托管与转账

fuels-rs Predicate(谓词)深度实战:在 Fuel 网络上实现脚本化条件托管与转账

2026-09-08 16:25:44作者:宗隆裙

本文以 fuels-rs(Fuel Network Rust SDK)为背景,系统讲解 Sway Predicate 的原理与完整落地路径:如何编写一个返回布尔值的纯函数式 Predicate,通过 forc build 编译生成字节码,用 SDK 的 abigen! 宏生成编码器与可配置常量结构体,最终实现"向谓词地址锁定资产、凭谓词数据条件化花费资产、用可配置常量在字节码层动态改写条件"的完整闭环。读完本文,你将掌握 Predicate::load_fromencode_datawith_datawith_configurables 以及 Account trait 转账等一整套可复制的实战技能。

Predicate 是什么:Sway 的"纯函数"式资产托管

在 Sway 中,Predicate 是一类只返回布尔值、且没有任何副作用(纯函数)的程序。它的核心作用是以"可编程校验"的方式托管资产:

  • 一个 Predicate 的地址可以拥有资产,该地址由编译后的字节码(byte code)推导生成,与比特币中 P2SH(Pay-to-Script-Hash)地址的构造思路一致——地址本身就是"脚本哈希",而不是某个公钥或私钥的直接承诺。
  • 任何人都可以像向普通地址转账一样,无缝地把资产发送到 Predicate 地址,无需任何特殊操作。
  • 要想花费 Predicate 名下的资金,使用者必须同时提供两样东西:Predicate 的原始字节码(byte code),以及谓词数据(predicate data)。字节码在链上执行时会用到这些谓词数据,只有校验通过(即谓词返回真值),资金才能被转移。

这一模型的含义是:Predicate 天然适合做"锁定条件满足后才允许动用"的资金管理,例如托管、条件支付、多签风格的校验、时间/高度约束、跨层提款等场景,且不需要部署合约——条件逻辑被"烘焙"在字节码本身中。

在 fuels-rs 中,Predicate 的一切交互都由 packages/fuels-accounts/src/predicate.rs 中定义的 Predicate 类型承载,它同时实现了 SDK 的 ViewOnlyAccountAccount trait,因此既能查询余额,也能像钱包一样发起转账(详见下文)。

从一个最小 Predicate 开始:源码、编译与实例化

我们以仓库中最简单的谓词 e2e/sway/predicates/basic_predicate/src/main.sw 为例。它的逻辑非常直观:main 接收两个参数,只有当二者数值相等时谓词才返回 true(即允许花费):

predicate;

fn main(a: u32, b: u64) -> bool {
    b == a.as_u64()
}

forc build 编译该 Sway 工程后,会得到 SDK 侧所需的两个产物(示例代码中反复引用的就是它们):

  • e2e/sway/predicates/basic_predicate/out/release/basic_predicate.bin —— 谓词字节码;
  • e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json —— 谓词 ABI,供 abigen! 使用。

在 SDK 中创建一个可用的 Predicate 实例,最常用的是 Predicate::load_from(file_path)。从源码结构看,它的内部实现是:先用 fs::read 把字节码读入内存,再交给 from_code,由 calculate_address 通过 fuel_tx::Input::predicate_owner(code) 计算出谓词地址,最后把 code、空 data、空 provider 组装成 Predicate(参见 packages/fuels-accounts/src/predicate.rs)。

也就是说,Predicate 地址完全由字节码决定,字节码不同则地址不同;这一点也直接影响了后续"可配置常量"的工作方式(见下文)。

Predicate 结构体本身封装了四个字段:addresscodedata 和(std 特性下的)provider,并提供了一组链式构造方法,with_provider 用于绑定节点 Provider,with_data 用于写入谓词数据。真实的实例化流程,先看下一节如何用 abigen! 生成数据编码器。

abigen! 生成 Encoder 与 Configurables

abigen! 宏会根据谓词的 JSON ABI,为我们在 Rust 侧生成该谓词涉及的全部类型,并额外生成两个自定义结构体:

  1. 一个 Encoder(编码器),提供 encode_data 方法,能按顺序把谓词 main 函数的全部参数自动编码成 predicate data
  2. 一个 Configurables(可配置常量)结构体,为谓词中声明的每个 configurable 提供对应的 with_xxx 设置方法。

注意:abigen! 宏会在谓词 name 字段后面拼接 EncoderConfigurables 两个后缀。例如 name = "MyPredicate" 会生成名为 MyPredicateEncoderMyPredicateConfigurables 的两个结构体。

下面是完整示例的第一步——配置两个钱包资产、启动本地节点,并用 abigen! 绑定上面的 basic_predicate(对应 examples/predicates/src/lib.rs 中的 predicate_data_setup 片段):

let asset_id = AssetId::zeroed();
let wallets_config = WalletsConfig::new_multiple_assets(
    2,
    vec![AssetConfig {
        id: asset_id,
        num_coins: 1,
        coin_amount: 1_000,
    }],
);

let wallets = &launch_custom_provider_and_get_wallets(wallets_config, None, None).await?;

let first_wallet = &wallets[0];
let second_wallet = &wallets[1];

abigen!(Predicate(
    name = "MyPredicate",
    abi = "e2e/sway/predicates/basic_predicate/out/release/basic_predicate-abi.json"
));

其中 AssetId::zeroed() 指基础资产(base asset)。WalletsConfig::new_multiple_assets(2, ...) 创建了两个钱包,每个钱包持有 1 枚面额 1_000 的 coin;launch_custom_provider_and_get_wallets 会连带启动一个节点实例并返回配置好的钱包。abigen!(Predicate(...))abi 指向前面 forc build 产物中的 ABI 文件。

从实现上看,Configurables 生成代码位于 packages/fuels-code-gen/src/program_bindings/abigen/configurables.rs:每个 with_xxx 方法最终会把"常量在字节码中的偏移量 + 新值数据"收集为一个 Configurable,并汇总成 fuels_core::Configurables 结构交给调用方。

编码谓词数据并把它加载进 Predicate

拿到 Encoder 后,就可以调用 MyPredicateEncoder::default().encode_data(...) 编码 main 函数的入参了。对于 basic_predicate,main(a: u32, b: u64) 只有在 a == b 时返回 true,因此这里传入两个相同的值 4096

let predicate_data = MyPredicateEncoder::default().encode_data(4096, 4096)?;
let code_path = "../../e2e/sway/predicates/basic_predicate/out/release/basic_predicate.bin";

let predicate: Predicate = Predicate::load_from(code_path)?
    .with_provider(first_wallet.provider().clone())
    .with_data(predicate_data);

这段代码完成了三件事(对应 examples/predicates/src/lib.rswith_predicate_data 片段):

  1. Predicate::load_from(code_path) 从字节码文件加载谓词(地址自动由字节码推导);
  2. .with_provider(...) 绑定第一个钱包所连接的 Provider;
  3. .with_data(predicate_data) 把编码好的 predicate data 写进谓词。

with_datapackages/fuels-accounts/src/predicate.rs 中仅仅是写入 self.data。这份数据在后面真正发起花费交易时,会被塞进交易输入中随字节码一起交给链上执行。

需要留意的是,Encoder 底层使用 SDK 的 ABI 编码器(ABIEncoder)并受 EncoderConfig 约束(如 max_tokens 上限)。仓库中 e2e/tests/predicates.rspredicate_encoder_config_is_applied 测试就演示了用 MyPredicateEncoder::new(encoder_config) 传入自定义配置后,编码超限会报 token limit ... reached while encoding 错误;默认的 EncoderConfig 则足够覆盖常规参数。如果你在自定义 EncoderConfig,请保证它与数据规模匹配。

向 Predicate 地址锁定资产

谓词地址就是一个普通地址,任何账户都可以直接把资产转给它。下面用第一个钱包向该谓词转入 500 枚基础资产,并通过 get_asset_balance 验证到账(examples/predicates/src/lib.rspredicate_data_lock_amount 片段):

// First wallet transfers amount to predicate.
first_wallet
    .transfer(predicate.address(), 500, asset_id, TxPolicies::default())
    .await?;

// Check predicate balance.
let balance = predicate.get_asset_balance(&AssetId::zeroed()).await?;

assert_eq!(balance, 500);

这一步之所以"像普通地址转账"一样简单,是因为 Predicate 实现了 ViewOnlyAccount trait——get_asset_balance 正是该 trait 提供的查询能力。至此,500 个资产被"锁"在由谓词字节码决定的地址上:任何人知道字节码但拿不出能让谓词返回 truepredicate data,就无法动用它们。

通过 Account trait 花费 Predicate 资产

花费环节是谓词最有价值的部分:SDK 的 Account trait 提供了统一的资产转移接口,WalletPredicate 都实现了它(详见 docs/src/accounts.md)。因此下面这段代码在 API 层面与"钱包转账"几乎没有差别:

let amount_to_unlock = 300;

predicate
    .transfer(
        second_wallet.address(),
        amount_to_unlock,
        asset_id,
        TxPolicies::default(),
    )
    .await?;

// Second wallet balance is updated.
let balance = second_wallet.get_asset_balance(&AssetId::zeroed()).await?;
assert_eq!(balance, 1300);

在 SDK 内部,谓词花费与钱包花费的关键差异是输入构造方式。当 Predicate 需要为转账提供资产输入时,其 ViewOnlyAccount 实现会调用 get_asset_inputs_for_amount,并把选中的 coin/message 资源包装成带谓词证明的输入:

Input::resource_predicate(resource, self.code.clone(), self.data.clone())

也就是说,交易输入里会原样携带谓词字节码与之前设置的 predicate data(见 packages/fuels-accounts/src/predicate.rs)。链上执行时,VM 会运行这份字节码并结合 data 进行校验;校验通过则交易成立。

这种"调用方只需对普通 Address 转账,花费方需出示字节码 + data"的机制,正是 P2SH 式脚本托管在 Fuel 上的体现。仓库 e2e/tests/predicates.rs 中的 spend_predicate_coins_messages_basictransfer_coins_and_messages_to_predicate 等测试完整验证了"转入 → 校验 data → 转出并正确扣除手续费"的整条链路。反之,如果 data 不满足谓词条件,交易会被链上拒绝,报错形如:

PredicateVerificationFailed(Panic { index: 0, reason: PredicateReturnedNonOne })

这一点可在 predicate_with_invalid_data_fails 测试中看到:encode_data(0, 100) 传入不相等参数后,transfer 返回该错误且接收方余额不变。

结合 docs/src/accounts.mdAccount trait 除 transfer 外还提供 force_transfer_to_contract(转给合约)与 withdraw_to_base_layer(跨层提款到底层链),这些方法对 Predicate 同样可用,只是动用谓词资产前必须先设置好满足条件的 predicate data

Configurable 常量:在字节码层动态改写花费条件

Predicate 与合约、脚本一样,可以声明 configurable 常量(可配置常量),在执行期间被改写。仓库中的谓词 e2e/sway/predicates/predicate_configurables/src/main.sw 给出了一个覆盖多种数据类型的完整示例:

configurable {
    BOOL: bool = true,
    U8: u8 = 8,
    TUPLE: (u8, bool) = (8, true),
    ARRAY: [u32; 3] = [253, 254, 255],
    STRUCT: StructWithGeneric<u8> = StructWithGeneric {
        field_1: 8,
        field_2: 16,
    },
    ENUM: EnumWithGeneric<bool> = EnumWithGeneric::VariantOne(true),
}

fn main(
    switch: bool,
    u_8: u8,
    some_tuple: (u8, bool),
    some_array: [u32; 3],
    some_struct: StructWithGeneric<u8>,
    some_enum: EnumWithGeneric<bool>,
) -> bool {
    switch == BOOL && u_8 == U8 && some_tuple.0 == TUPLE.0 && some_tuple.1 == TUPLE.1 && some_array[0] == ARRAY[0] && some_array[1] == ARRAY[1] && some_array[2] == ARRAY[2] && some_struct == STRUCT && some_enum == ENUM
}

可以看到,main 的行为是:只有当每个入参都与对应 configurable 常量的最新值相等时,谓词才返回 true。这意味着你可以编译一次谓词,之后通过"改写常量"来切换有效的解锁参数,而不必为每一种参数重新编译并更换地址。

每个常量对应一个 with 方法

SDK 会为每个 configurable 常量生成一个专用的 with 方法:常量 U8 会得到 with_U8,其参数类型与 Sway 中声明的一致(此处是 u8),依此类推。下面是仓库测试 e2e/tests/predicates.rspredicate_configurables 用法的核心片段——先把若干 with 方法链接起来构造一组新常量,再用与常量一致的参数编码 predicate data,最后整体更新谓词:

abigen!(Predicate(
    name = "MyPredicate",
    abi = "e2e/sway/predicates/predicate_configurables/out/release/predicate_configurables-abi.json"
));

let new_tuple = (16, false);
let new_array = [123, 124, 125];
let new_struct = StructWithGeneric {
    field_1: 32u8,
    field_2: 64,
};
let new_enum = EnumWithGeneric::VariantTwo;

let configurables = MyPredicateConfigurables::default()
    .with_U8(8)?
    .with_TUPLE(new_tuple)?
    .with_ARRAY(new_array)?
    .with_STRUCT(new_struct.clone())?
    .with_ENUM(new_enum.clone())?;

let predicate_data = MyPredicateEncoder::default()
    .encode_data(true, 8u8, new_tuple, new_array, new_struct, new_enum)?;

let mut predicate: Predicate = Predicate::load_from(
    "sway/predicates/predicate_configurables/out/release/predicate_configurables.bin",
)?
.with_data(predicate_data)
.with_configurables(configurables);

这里的 with_* 方法都返回 Result,因此逐个以 ? 解包后链式调用;encode_data 传入的入参必须与改写后的常量值保持一致(main 会逐项比对),否则花费仍会失败。这也揭示了谓词 data 与 configurables 的分工:

  • configurables 决定"什么样的参数是合法的"(即约束条件本身);
  • predicate data 是本次花费实际提供的、用来匹配约束的参数。

配置常量的底层原理:改写字节码并重算地址

为什么"改了常量地址还能对上"?秘密在 Predicate::with_configurables 的实现里(packages/fuels-accounts/src/predicate.rs):

pub fn with_configurables(mut self, configurables: impl Into<Configurables>) -> Self {
    let configurables: Configurables = configurables.into();
    configurables.update_constants_in(&mut self.code);
    let address = Self::calculate_address(&self.code);
    self.address = address;
    self
}

它先把新的常量值直接写回字节码的对应偏移处,然后基于新字节码重新计算谓词地址。而 update_constants_in(定义于 packages/fuels-core/src/lib.rs)做的事情,本质上是按 Configurables 中记录的 (offset, data) 列表把新值逐个拷贝到二进制流里:

pub fn update_constants_in(&self, binary: &mut [u8]) {
    for c in &self.offsets_with_data {
        let offset = c.offset as usize;
        binary[offset..offset + c.data.len()].copy_from_slice(&c.data)
    }
}

因此,从调用方视角看,改变 configurable 常量后,谓词"名义地址"变化了(因为字节码内容变化);但只要你持有更新后的 Predicate 实例(其 address 已同步重算),向它转账、由它花费的流程完全不受影响。测试 predicate_configurables 验证了这一闭环:锁定资金后调用 transfer 花掉几乎全部余额(predicate_balance - 1,保留手续费),最终谓词地址余额归零、接收方正确到账。

谓词的更多真实形态与验证

Predicate 并非只适用于"两个数相等"这类玩具逻辑。结合仓库中的 Sway 谓词与端到端测试,可以梳理出它在真实场景中的几种形态:

  • 多签校验:谓词内预置多个公钥哈希,花费时必须提供对应数量的签名,并用 ec_recover_address 从签名恢复公钥逐一比对。该场景的完整讲解见姊妹文档 docs/src/predicates/send-spend-predicate.md,其 Sway 源码位于 e2e/sway/predicates/signatures/src/main.sw,SDK 侧示例见 examples/predicates/src/lib.rspredicate_signerspredicate_coinspredicate_load 等片段)。
  • 代付交易费用pay_with_predicatediff_asset_predicate_payment 等测试展示了用谓词资产支付合约部署费与调用费,即"由锁定的资金替用户买单"。
  • 定制输入输出 / 见证数据predicate_tx_input_outputpredicate_witnesses 相关测试展示了谓词读取交易首个 input/output 或手工追加的 witness 以决定是否放行,可用来实现更细粒度的交易级策略。
  • 复杂动态类型predicate_vectorpredicate_u128/u256predicate_bytes 等工程把 main 参数扩展到了向量、大整数、字节等类型,Encoder 均能正确编码(例如 encode_data(12, 30, vec![2, 4, 42]))。
  • 超大谓词(Blob 加载):当谓词字节码超过交易携带上限时,可将字节码先转换成 loader 并上传为 Blob,再用 loader 派生的代码构造谓词,参见 predicate_blobspredicate_configurables_in_blobs 测试。

最后提醒两个实战要点:

  1. 编译产物路径:上面所有示例依赖 forc build 生成的 .bin-abi.json。不同工作目录下相对路径写法可能不同(examples 中使用 ../../e2e/...,e2e 测试中使用 sway/...e2e/sway/...),请以你实际运行位置为准。
  2. data 必须满足谓词约束encode_data 只负责把参数编码成字节,不负责"让谓词通过"。参数不满足条件时,交易会在链上因 PredicateReturnedNonOne 之类的失败而回滚,接收方余额不变——这恰恰是谓词托管的安全边界所在。

从"纯函数托管"的概念、P2SH 式地址推导,到 Encoder 编码 data、Configurables 改写字节码常量,再到 Account trait 下的条件花费,一条基于 fuels-rs 的 Predicate 开发与使用路径已经完整打通。你可以直接以 examples/predicates/src/lib.rs 中的测试为蓝本运行体验,或基于 e2e/sway/predicates 目录下的各类谓词工程改造出自己的条件托管方案。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393