fuels-rs Predicate(谓词)深度实战:在 Fuel 网络上实现脚本化条件托管与转账
2026/9/10 1:25:47 网站建设 项目流程

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

【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs

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

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 的ViewOnlyAccountAccounttrait,因此既能查询余额,也能像钱包一样发起转账(详见下文)。

从一个最小 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.rs 的with_predicate_data片段):

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

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

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

向 Predicate 地址锁定资产

谓词地址就是一个普通地址,任何账户都可以直接把资产转给它。下面用第一个钱包向该谓词转入 500 枚基础资产,并通过get_asset_balance验证到账(examples/predicates/src/lib.rs 的predicate_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实现了ViewOnlyAccounttrait——get_asset_balance正是该 trait 提供的查询能力。至此,500 个资产被"锁"在由谓词字节码决定的地址上:任何人知道字节码但拿不出能让谓词返回truepredicate data,就无法动用它们。

通过 Account trait 花费 Predicate 资产

花费环节是谓词最有价值的部分:SDK 的Accounttrait 提供了统一的资产转移接口,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.md:Accounttrait 除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.rs 中predicate_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.rs(predicate_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 改写字节码常量,再到Accounttrait 下的条件花费,一条基于 fuels-rs 的 Predicate 开发与使用路径已经完整打通。你可以直接以 examples/predicates/src/lib.rs 中的测试为蓝本运行体验,或基于 e2e/sway/predicates 目录下的各类谓词工程改造出自己的条件托管方案。

【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询