首页
/ fuels-ts Predicates 入门:链下校验的 Sway 布尔程序如何守护与花费链上资产

fuels-ts Predicates 入门:链下校验的 Sway 布尔程序如何守护与花费链上资产

2026-09-08 22:42:36作者:柯茵沙

Predicates(谓词)是 Fuel 上一种特殊的 Sway 程序,它以 bool 作为返回值,本质上是一组交易必须满足的"规则"。本文基于 fuels-ts 仓库文档 apps/docs/src/guide/predicates/index.md 展开,系统讲解 Predicates 的概念模型、与普通账户的差异、"先链下校验、后链上记账"的执行流程,并结合仓库源码与 docs/sway 下的真实 Sway 示例,给出从编译、实例化、充值到花费资金与排错调试的完整实战路径。

Predicates 是什么:返回布尔值的 Sway 程序

在 Sway 中,Predicates 是一种特定类型的程序,其核心特征是返回一个布尔值(true/false)。也就是说,它们的功能相当于一组"规则",一笔交易必须遵守这些规则才能被视为有效。

Predicates 有两个与普通智能合约截然不同的重要特性:

  • 无法访问链上信息:它们不能读取区块链上已写入的状态或数据,只能纯粹基于接收到的参数做出判断;
  • 是纯函数:执行过程不会产生任何非预期副作用。

正因为如此,Predicates 天然适合做"条件门槛"类校验——例如"只有当 input_address 与预设地址一致时才放行资产"、"只有当传入的 PIN 码正确时才解锁资金"等。

核心设计:先在链下校验,再上链记账

Predicates 与传统"链上检查规则"模式的关键区别在于执行位置与时机:

不是直接在区块链上检查这些规则,而是先在链外(off the blockchain)校验;只有确信校验通过后,才把交易记录到链上。

这种"先本地验证、再提交上链"的做法带来了两方面的收益:

  1. 更高效:减少了链上重复计算的负担;
  2. 更便宜、防拥堵:由于把大量重复性的校验逻辑搬到了链下,链上资源占用下降,交易成本随之降低,也缓解了网络拥堵。

从燃料费的角度理解:Predicate 的校验失败不会浪费链上 gas——SDK 在本地/模拟阶段就能发现验证不通过并抛出错误,交易根本不会被作为有效交易打包。

与 Predicates 交互的基本工作方式

存款:像给普通地址转账一样简单

用户可以向 Predicate 的地址发送资产,就像向区块链上的任何其他地址转账一样,无需任何特殊处理

花费:需要提供原始字节码与 Predicate Data

要花费存放在 Predicate 地址上的资金,用户必须提供:

  • Predicate 的原始字节码(byte code)
  • Predicate data(如果要求的话)

Predicate data 与 Predicate main 函数接收的参数一一对应,它会在字节码执行过程中发挥作用。也就是说,data 就是你把"规则需要判断的输入"作为参数传给 main 的方式。

这里有一个重要推论:如果 main 函数没有任何参数,那么也就没有所谓的 data 需要提供。比如下面这个极简 Predicate(源码见 apps/docs/sway/return-true-predicate/src/main.sw):

predicate;

fn main() -> bool {
    true
}

它无条件返回 truemain 不接受参数,因此实例化时不需要传入 data。在 TS SDK 中(来自 apps/docs/src/guide/predicates/snippets/instantiation/simple.ts):

import { Provider } from 'fuels';

import { LOCAL_NETWORK_URL } from '../../../../env';
import { ReturnTruePredicate } from '../../../../typegend';

const provider = new Provider(LOCAL_NETWORK_URL);

const predicate = new ReturnTruePredicate({
  provider,
});

校验结果:通过则可花费,失败则 SDK 抛错

如果 Predicate 校验成功,资金将被解锁、可以正常花费;反之,如果校验失败,SDK 会抛出一个验证错误(详见后文"校验失败"一节的报错实例)。

Predicate 实例的三个关键属性与"地址即字节码哈希"

一个 Predicate 实例(继承自 Account)除继承的方法外,有三个重要属性:

  • bytes:predicate 的字节码;
  • chainId:当前网络链 ID;
  • address:由字节码推导出的 Predicate 地址。

关键点在于这个 address 的生成方式——它由字节码哈希而来,对应比特币中 Pay-to-Script-Hash(P2SH)地址的概念。在源码中,构造器正是用处理后的字节码调用 getPredicateRoot(predicateBytes) 得到根哈希、再包装为 Address(见 packages/account/src/predicate/predicate.ts#L86packages/account/src/predicate/utils/getPredicateRoot.ts):

const address = new Address(getPredicateRoot(predicateBytes));
super(address, provider);

这一设计带来一个后续会反复遇到的推论:任何对字节码的改动(例如改变 configurable 常量)都会产生新的哈希与新地址,即生成一个新的 Predicate,而不是修改原有 Predicate 的行为。

实战:让一个 Predicate 校验你的交易

第一步:编写并编译 Sway Predicate

Predicate 与合约一样用 Sway 编写。以 docs 中用于演示"发送与花费资金"的 Predicate 为例(源码见 apps/docs/sway/simple-predicate/src/main.sw):

predicate;

fn main(input_address: b256) -> bool {
    let valid_address = 0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4;

    input_address == valid_address
}

它接收一个 b256 类型的地址参数,与硬编码的 valid_address 比较:相等返回 true,否则返回 false

编写完成后,使用 forc build 编译。编译产物有两个关键文件:

  • JSON ABI(Application Binary Interface);
  • Predicate 的二进制代码(bytecode)

第二步:带 data 实例化 Predicate

在 TypeScript 中,将上述 ABI 与字节码(通常经 TypeGen 生成类型化类)结合,并在构造时传入 main 所需的参数(即 predicate data)。上面的 SimplePredicate 要求传入 b256 类型的 input_address,因此(见 apps/docs/src/guide/predicates/snippets/cookbook/transferring-assets.ts):

import { Provider, Wallet } from 'fuels';

import { LOCAL_NETWORK_URL, WALLET_PVT_KEY } from '../../../../env';
import { SimplePredicate } from '../../../../typegend';

const provider = new Provider(LOCAL_NETWORK_URL);
const baseAssetId = await provider.getBaseAssetId();

const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const receiver = Wallet.generate({ provider });

const inputAddress =
  '0xfc05c23a8f7f66222377170ddcbfea9c543dff0dd2d2ba4d0478a4521423a9d4';

const predicate = new SimplePredicate({
  provider,
  data: [inputAddress],
});

提示:如果你希望在 Predicate 实例化之后才传入 data,或者想使用与构造时不同的 data,就需要新建一个 Predicate 实例(源码也提供了 toNewInstance 便捷方法)。data 是实例化时确定并编码进交易输入的。

第三步:向 Predicate 地址充值

用持有足够资金的普通钱包向 predicate.address 转账即可:

// The amount of coins to send to the predicate
const amountToPredicate = 10_000_000;

// Fund the predicate with some funds from our wallet (sender)
const fundPredicateTx = await sender.transfer(
  predicate.address,
  amountToPredicate,
  baseAssetId,
  {
    gasLimit: 1000,
  }
);

// Wait for the transaction
await fundPredicateTx.waitForResult();

第四步:由 Predicate 校验并发起花费

现在 Predicate 持有资金,我们让它验证一笔转账——把资产转给接收者:

// The amount of coins to send from the predicate, to our receiver wallet.
const amountToReceiver = 200;

// Transfer funds from the predicate, to our receiver wallet
const transferFromPredicateTx = await predicate.transfer(
  receiver.address,
  amountToReceiver,
  baseAssetId
);

// Wait for the transaction
await transferFromPredicateTx.waitForResult();

注意 transfer 方法有两个参数:接收者地址转账金额。当 Predicate 依据其预设条件返回 true 时,花费即成功完成。

花光余额会失败:别忘了交易费

如果试图把 Predicate 持有的全部金额都转出去,会因为没有余额支付交易手续费而失败。SDK 抛出的错误信息形如(见 apps/docs/src/guide/predicates/snippets/cookbook/failure-not-enough-funds.ts):

const errorMessage = `Insufficient funds or too many small value coins. Consider combining UTXOs.\nFor the following asset ID: '${baseAssetId}'.`;

校验失败的情形

回顾我们的 Predicate:只有 input_address 等于硬编码的 valid_address 才会放行。因此,如果传入一个不同的地址,Predicate 将校验失败,SDK 抛出的错误以如下关键词开头(见 apps/docs/src/guide/predicates/snippets/cookbook/failure-returns-false.ts):

const errorMessage = `PredicateVerificationFailed`;

这也对应上文 index 文档所说"若校验未通过,SDK 将抛出验证错误"。

更丰富的交互方式:Predicate 方法全景

Predicate 类继承自 Account 类(见 packages/account/src/account.ts),因而继承了其全部方法。除此之外,Predicate 自带的关键方法与用途可归为三类:查询余额、交易、转账。

查询余额

  • getBalances:返回 Predicate 拥有的全部资产余额;
  • getResourcesToSpend:返回 Predicate 拥有的、可加入某笔交易请求的资源(UTXO)。该方法在 transfercreateTransfer 内部被调用,当你需要把 Predicate 塞进一笔已有的交易请求时,可以主动调用它获取资源(getResourcesToSpend 在 predicate.ts 的实现)。

交易相关方法

  • setData(data):在 Predicate 已实例化后更新 predicate data(即参数)。注意:它只更新 Predicate 实例内部的 data,不会影响已经嵌入了 predicate UTXO 的交易请求——因为每个 predicate UTXO 都携带自己的一份 data 副本。若需修改交易请求内的 predicate data,应随后调用 populateTransactionPredicateData(实现见 predicate.ts#L104-L127),该方法会遍历请求中归属于该 Predicate 地址的输入并写入新的字节码、data 与 witness 索引;
  • sendTransaction:向节点发送交易(Predicate 覆盖实现会关闭依赖估算,见 predicate.ts#L135-L141);
  • simulateTransaction:对 Predicate 调用做干跑(dry-run),不消耗真实资源。典型用途是校验余额是否足以支付交易费(predicate.ts#L149-L154)。

转账相关方法

  • createTransfer:创建包含全部转账细节的交易请求,并通过干跑自动估算成本、以所需 Predicate 资源为请求注资。返回后可先检查/修改请求属性,再提交上链,从而提高成功率;
  • transfer:直接向另一地址发送资金,是最直接的转账入口。

关于 Predicate data 的内部编码:getPredicateData() 会通过 interface.functions.mainencodeArguments(this.predicateData) 将传入的参数数组编码为字节;当没有 data 时返回空字节数组(见 predicate.ts#L161-L168)。

可配置常量(Configurable Constants):同一逻辑、不同配置

Predicate 与合约、脚本一样支持 configurable constants,这让同一个 Predicate 可以适配不同的使用场景。典型例子是白名单资产转移校验:只有当接收方地址在预先批准的白名单中,转账才被执行。

对应的 Sway 实现(见 apps/docs/sway/whitelisted-address-predicate/src/main.sw):

predicate;

configurable {
    WHITELISTED: b256 = 0xa703b26833939dabc41d3fcaefa00e62cee8e1ac46db37e0fa5d4c9fe30b4132,
}

fn main(address: b256) -> bool {
    WHITELISTED == address
}

WHITELISTED 拥有一个代表默认批准地址的默认值。当需要把另一个地址加入白名单时,可在 TS 端更新该常量并让 Predicate 执行转账(见 configurable-set-data.ts):

const configurable = { WHITELISTED: whitelisted.address.toB256() };

// Instantiate predicate with configurable constants
const predicate = new WhitelistedAddressPredicate({
  provider,
  data: [configurable.WHITELISTED],
  configurableConstants: configurable,
});

// Transferring funds to the predicate
const tx1 = await sender.transfer(predicate.address, 200_000, baseAssetId, {
  gasLimit: 1000,
});
await tx1.waitForResult();

const amountToTransfer = 100;

// Transferring funds from the predicate to destination if predicate returns true
const tx2 = await predicate.transfer(recipient.address, amountToTransfer, baseAssetId, {
  gasLimit: 1000,
});
await tx2.waitForResult();

只要更新后的 WHITELISTED 与目标接收方地址一致,Predicate 即校验通过。若默认白名单地址恰好就是要转账的接收方,则无需更新常量,直接基于默认值即可(见 configurable-default.ts)。

务必记住上文的地址推论:这些定制并不会直接修改原始 Predicate。改变常量会改变字节码,进而改变哈希与地址——你实际得到的是"新配置的新 Predicate",而非行为被改写的旧 Predicate。

自定义交易:Predicate 与 ScriptTransactionRequest 组合

Predicate 逻辑与自定义交易配合,能解锁更复杂的 dApp 用例:先实例化一个自定义交易,再把 Predicate 资源追加进去,最后经由一个通过校验的 Predicate 提交交易。

自定义交易可通过 ScriptTransactionRequest 实例塑形。以"必须使用 configurable PIN 才能解锁资金"的 Predicate 为例(源码见 apps/docs/sway/configurable-pin/src/main.sw):

predicate;

configurable {
    PIN: u64 = 1337,
}

fn main(pin: u64) -> bool {
    return PIN == pin;
}

TS 端把它纳入一笔自定义交易(见 custom-transactions.ts):

import { Provider, ScriptTransactionRequest, Wallet } from 'fuels';

// Setup
const provider = new Provider(LOCAL_NETWORK_URL);
const sender = Wallet.fromPrivateKey(WALLET_PVT_KEY, provider);
const receiver = Wallet.generate({ provider });
const assetId = await provider.getBaseAssetId();
const amountToFundPredicate = 300_000;
const amountToReceiver = 100_000;

// Instantiate the predicate using valid predicate data, aka the pin we need
// to send the funds to the receiver
const data: ConfigurablePinInputs = [1337];
const predicate = new ConfigurablePin({ provider, data });

// Fund the predicate, so that we can send these funds via predicate logic
// to the receiver
const fundPredicateTx = await sender.transfer(predicate.address, amountToFundPredicate, assetId);
await fundPredicateTx.waitForResult();

// Instantiate the script request
const request = new ScriptTransactionRequest();

// Adding the OutputCoin that represents the funds that we want to send to the receiver
request.addCoinOutput(receiver.address, amountToReceiver, assetId);

// Estimate the transaction cost and fund accordingly
const { assembledRequest } = await provider.assembleTx({
  request,
  feePayerAccount: predicate,
  accountCoinQuantities: [
    {
      amount: amountToReceiver,
      assetId,
      account: predicate,
      changeOutputAccount: predicate,
    },
  ],
});

// Submit the transaction and await it's result
const predicateTx = await predicate.sendTransaction(assembledRequest);
await predicateTx.waitForResult();

更完整的自定义交易构造说明可参考 Transaction Request 修改指南 对应章节(该部分位于 apps/docs/src/guide/transactions/ 目录下,具体请查阅仓库内 transactions 指南文档)。

部署 Predicate:把字节码存为 blob,降低重复执行成本

对于需要频繁重复执行的 Predicate,官方建议先进行部署以优化成本。部署后,Predicate 的字节码作为 blob 存储在链上;SDK 会生成一个能按需加载该 blob 的"加载器"字节码,用它来执行原始 Predicate。由于上链的字节码更小、重复执行不再重复携带完整代码,反复执行的成本显著降低。

使用 Fuels CLIfuels deploy 命令部署时,会自动完成以下动作:

  1. 使用当前 forc 版本编译 Predicate;
  2. 把编译好的 Predicate 二进制作为 blob 部署上链;
  3. 生成一个更小的、用于加载已部署 Predicate blob 的新 Predicate;
  4. 同时为 Predicate 与加载器生成可在应用中直接使用的 TypeScript 类型。

随后在代码中按生成的类型使用(见 deploying-predicates.ts):

// We can deploy dynamically or via `fuels deploy`
const originalPredicate = new ConfigurablePin({
  provider,
  data: [1337],
});

const { waitForResult: waitForDeploy } = await originalPredicate.deploy(wallet);
await waitForDeploy();

// First, we will need to instantiate the script via it's loader bytecode.
// This can be imported from the typegen outputs that were created on `fuels deploy`.
// Then we can use the predicate as we would normally, such as overriding the configurables.
const loaderPredicate = new ConfigurablePinLoader({
  data: [23],
  provider,
  configurableConstants: {
    PIN: 23,
  },
});

// Now, let's fund the predicate
const fundTx = await wallet.transfer(loaderPredicate.address, 100_000, baseAssetId);
await fundTx.waitForResult();

// Then we'll execute the transfer and validate the predicate
const transferTx = await loaderPredicate.transfer(receiver.address, 1000, baseAssetId);
const { isStatusSuccess } = await transferTx.waitForResult();

注意:此处既可以通过 CLI 部署,也可以动态调用 Predicatedeploy(account) 方法部署(predicate.ts#L367),二者结果一致。

调试 Predicates:先把逻辑写成 Script

目前尚没有直接调试 Predicate 的手段。文档给出的实用替代方案是:

先把 Predicate 当作 Script 编写、测试与调试(Script 拥有更丰富的调试工具),待其行为符合预期后,再把它改回 Predicate。

由于 Predicate 的 main 本质上是一段可被独立执行的纯校验逻辑,这种方法在工程实践中非常有效,能规避"无法单步调试链上校验"的痛点。

多参数与结构体参数:不止一个入参

Predicate 的 main 同样支持多参数与结构体参数。

例如"两个参数不相等才为真"的 Predicate(见 predicate-multi-args/src/main.sw):

predicate;

fn main(arg1: u64, arg2: u64) -> bool {
    return arg1 != arg2;
}

TS 端按数组顺序传参即可(见 multi-args.ts):

const predicate = new PredicateMultiArgs({ provider, data: [20, 30] });

又如期望传入结构体的 Predicate(见 predicate-main-args-struct/src/main.sw):

predicate;

struct Validation {
    has_account: bool,
    total_complete: u64,
}

fn main(received: Validation) -> bool {
    let expected_has_account: bool = true;
    let expected_total_complete: u64 = 100;

    received.has_account == expected_has_account && received.total_complete == expected_total_complete
}

TS 端把结构体作为对象传入 data 数组(见 struct-arg.ts):

const predicate = new PredicateMainArgsStruct({
  provider,
  data: [{ has_account: true, total_complete: 100 }],
});

总结:Predicate 编程心智模型

围绕"Predicates"这套机制,可以提炼出如下心智模型:

  1. 它是规则而非逻辑:Predicate 是纯函数、无副作用、不读链上状态,只凭参数下结论;
  2. 先校验后记账:校验在链下完成,通过后才上链,这是其"便宜、不堵网络"的根源;
  3. 地址派生自字节码:向地址存款与普通地址无异;花费必须提供原始字节码 + predicate data,二者共同决定校验结果;
  4. 改配置 = 新 Predicate:configurable constants 修改会派生出新地址,不会改写旧行为;
  5. 失败由 SDK 报错:校验失败(如参数不匹配、余额不足付手续费)会以 PredicateVerificationFailed 或余额不足等错误抛出;
  6. 调试先走 Script:现阶段把校验逻辑写成 Script 调试,成熟后再迁回 Predicate。

围绕 Predicates 的更多细粒度操作,可继续阅读本仓库中 apps/docs/src/guide/predicates/ 目录下的系列文档:实例化 Predicates与 Predicates 交互的方法向 Predicates 发送并花费资金带 Configurable Constants 的 PredicatePredicate 与自定义交易以及部署 Predicates

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

项目优选

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