fuels-rs Predicate谓词深度实战在 Fuel 网络上实现脚本化条件托管与转账【免费下载链接】fuels-rsFuel Network Rust SDK项目地址: https://gitcode.com/GitHub_Trending/fu/fuels-rs本文以 fuels-rsFuel Network Rust SDK为背景系统讲解 Sway Predicate 的原理与完整落地路径如何编写一个返回布尔值的纯函数式 Predicate通过forc build编译生成字节码用 SDK 的abigen!宏生成编码器与可配置常量结构体最终实现向谓词地址锁定资产、凭谓词数据条件化花费资产、用可配置常量在字节码层动态改写条件的完整闭环。读完本文你将掌握Predicate::load_from、encode_data、with_data、with_configurables以及Accounttrait 转账等一整套可复制的实战技能。Predicate 是什么Sway 的纯函数式资产托管在 Sway 中Predicate 是一类只返回布尔值、且没有任何副作用纯函数的程序。它的核心作用是以可编程校验的方式托管资产一个 Predicate 的地址可以拥有资产该地址由编译后的字节码byte code推导生成与比特币中 P2SHPay-to-Script-Hash地址的构造思路一致——地址本身就是脚本哈希而不是某个公钥或私钥的直接承诺。任何人都可以像向普通地址转账一样无缝地把资产发送到 Predicate 地址无需任何特殊操作。要想花费 Predicate 名下的资金使用者必须同时提供两样东西Predicate 的原始字节码byte code以及谓词数据predicate data。字节码在链上执行时会用到这些谓词数据只有校验通过即谓词返回真值资金才能被转移。这一模型的含义是Predicate 天然适合做锁定条件满足后才允许动用的资金管理例如托管、条件支付、多签风格的校验、时间/高度约束、跨层提款等场景且不需要部署合约——条件逻辑被烘焙在字节码本身中。在 fuels-rs 中Predicate 的一切交互都由 packages/fuels-accounts/src/predicate.rs 中定义的Predicate类型承载它同时实现了 SDK 的ViewOnlyAccount与Accounttrait因此既能查询余额也能像钱包一样发起转账详见下文。从一个最小 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结构体本身封装了四个字段address、code、data和std特性下的provider并提供了一组链式构造方法with_provider用于绑定节点 Providerwith_data用于写入谓词数据。真实的实例化流程先看下一节如何用abigen!生成数据编码器。用abigen!生成 Encoder 与 Configurablesabigen!宏会根据谓词的 JSON ABI为我们在 Rust 侧生成该谓词涉及的全部类型并额外生成两个自定义结构体一个 Encoder编码器提供encode_data方法能按顺序把谓词main函数的全部参数自动编码成predicate data一个 Configurables可配置常量结构体为谓词中声明的每个configurable提供对应的with_xxx设置方法。注意abigen!宏会在谓词name字段后面拼接Encoder与Configurables两个后缀。例如name MyPredicate会生成名为MyPredicateEncoder与MyPredicateConfigurables的两个结构体。下面是完整示例的第一步——配置两个钱包资产、启动本地节点并用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 的 coinlaunch_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_predicatemain(a: u32, b: u64)只有在a b时返回true因此这里传入两个相同的值4096let 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片段用Predicate::load_from(code_path)从字节码文件加载谓词地址自动由字节码推导用.with_provider(...)绑定第一个钱包所连接的 Provider用.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 个资产被锁在由谓词字节码决定的地址上任何人知道字节码但拿不出能让谓词返回true的predicate data就无法动用它们。通过 Account trait 花费 Predicate 资产花费环节是谓词最有价值的部分SDK 的Accounttrait 提供了统一的资产转移接口Wallet与Predicate都实现了它详见 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_basic、transfer_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.mdAccounttrait 除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: StructWithGenericu8 StructWithGeneric { field_1: 8, field_2: 16, }, ENUM: EnumWithGenericbool EnumWithGeneric::VariantOne(true), } fn main( switch: bool, u_8: u8, some_tuple: (u8, bool), some_array: [u32; 3], some_struct: StructWithGenericu8, some_enum: EnumWithGenericbool, ) - 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.rspub fn with_configurables(mut self, configurables: impl IntoConfigurables) - 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.swSDK 侧示例见 examples/predicates/src/lib.rspredicate_signers、predicate_coins、predicate_load等片段。代付交易费用pay_with_predicate、diff_asset_predicate_payment等测试展示了用谓词资产支付合约部署费与调用费即由锁定的资金替用户买单。定制输入输出 / 见证数据predicate_tx_input_output、predicate_witnesses相关测试展示了谓词读取交易首个 input/output 或手工追加的 witness 以决定是否放行可用来实现更细粒度的交易级策略。复杂动态类型predicate_vector、predicate_u128/u256、predicate_bytes等工程把main参数扩展到了向量、大整数、字节等类型Encoder 均能正确编码例如encode_data(12, 30, vec![2, 4, 42])。超大谓词Blob 加载当谓词字节码超过交易携带上限时可将字节码先转换成 loader 并上传为 Blob再用 loader 派生的代码构造谓词参见predicate_blobs、predicate_configurables_in_blobs测试。最后提醒两个实战要点编译产物路径上面所有示例依赖forc build生成的.bin与-abi.json。不同工作目录下相对路径写法可能不同examples 中使用../../e2e/...e2e 测试中使用sway/...或e2e/sway/...请以你实际运行位置为准。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),仅供参考