Bitcoin Core PSBT RPC 默认升级为 v2:createpsbt、converttopsbt 与 psbt_version 参数深度解析
本文基于 Bitcoin Core 发布说明 doc/release-notes-21283.md 展开,核心主题是 PSBT(Partially Signed Transaction,部分签名交易)创建类 RPC 的默认行为变更:createpsbt、walletcreatepsbt、converttopsbt 和 psbtbumpfee 现在默认创建 version 2 的 PSBT,同时这四个 RPC 均新增了可选的 psbt_version 参数,允许调用者显式指定要创建的 PSBT 版本。读完后,你将理解 PSBT v0 与 v2 在结构上的差异、如何在脚本中安全地控制 RPC 输出版本,以及 Bitcoin Core 源码中对该参数的解析与校验链路。
变更概述:从 PSBT v0 默认值到 v2 默认值
原发布说明(release-notes-21283.md)的完整内容如下:
Updated RPCs
createpsbt,walletcreatepsbt,converttopsbt, andpsbtbumpfeewill now default to creating version 2 PSBTs. An optionalpsbt_versionargument is added to these RPCs which allows specifying the version of PSBT to create.
这是一次向后兼容性需要调用方注意的行为变更:
- 默认值改变:此前这些 RPC 不指定版本时产生 v0(即 BIP-174 最初定义的格式)PSBT;升级后默认产生 v2 PSBT。任何对返回 base64 字符串做字节级比对、或依赖旧解析器(只认 v0 全局未签名交易字段)的下游脚本,可能需要同步调整。
- 新增显式参数:
psbt_version作为可选参数加入四个 RPC,取值只允许0或2,传其他值会直接报RPC_INVALID_PARAMETER错误(提示 "The PSBT version can only be 2 or 0")。
PSBT v0 与 v2 的结构差异:为什么 v2 是更好的默认值
在 src/psbt.h 中,PartiallySignedTransaction 类(类定义位于 L1238 起)的序列化逻辑清晰地展示了两个版本的差异:
- v0(
GetVersion() < 2分支):全局区首先写入PSBT_GLOBAL_UNSIGNED_TX标记,随后写入完整的不带 witness 的未签名交易序列化字节流(L1277-L1283)。整个交易(包括所有输入和输出的地址/金额/脚本)都在这一个冗余字段里。 - v2(
GetVersion() >= 2分支):不再写入完整未签名交易,而是写入一组全局字段——交易版本号(PSBT_GLOBAL_TX_VERSION)、可选的回退锁时间(PSBT_GLOBAL_FALLBACK_LOCKTIME)、输入/输出计数(PSBT_GLOBAL_INPUT_COUNT、PSBT_GLOBAL_OUTPUT_COUNT)以及可选的可修改性标志(PSBT_GLOBAL_TX_MODIFIABLE)(L1297-L1315)。各输入、输出的数据分别存放在各自的键值区中。
这带来三个实际好处:
- 无冗余:v0 中全局未签名交易与各输入/输出字段重复存放同一信息,v2 消除了这层冗余,序列化体积更小。
- 支持部分数据即可校验/填充:v2 允许在输入或输出数据不完整的情况下进行合并与更新(例如只带部分输入的 PSBT 可以被钱包补全),而 v0 要求全局未签名交易完整存在。
- 必填字段约束更强:从源码看,v2 在填充输出时对数据完整性有硬性要求——PSBTOutput 的校验逻辑(L1225-L1234) 中明确抛出 "Output amount is required in PSBTv2" 与 "Output script is required in PSBTv2",保证 v2 PSBT 的每个输出都携带金额和脚本。
版本上限由常量 PSBT_HIGHEST_VERSION = 2 定义(src/psbt.h#L97),反序列化时遇到高于该值的版本会直接失败(L1483-L1484),这也解释了为什么 RPC 层只接受 0 和 2 两个取值。
参数在各 RPC 中的具体位置与默认值
psbt_version 在四个 RPC 中的参数位置(按位置传参时)不同,这一点在 src/rpc/client.cpp 的命名参数→位置参数映射表中可以逐条确认(L211-L335 区域):
| RPC | 参数位置(位置传参索引) | 默认值 | 源码位置 |
|---|---|---|---|
createpsbt |
第 5 个参数(索引 5) | 2 |
src/rpc/rawtransaction.cpp#L1711 |
converttopsbt |
第 3 个参数(索引 3) | 2 |
src/rpc/rawtransaction.cpp#L1764 |
walletcreatefundedpsbt |
第 6 个参数(索引 6) | 2 |
src/wallet/rpc/spend.cpp#L1738 |
psbtbumpfee |
第 1 个参数(索引 1) | 2 |
src/rpc/client.cpp#L335 |
注意两点:
- 发布说明中写作
walletcreatepsbt,而当前仓库中实际注册的钱包端 RPC 名为walletcreatefundedpsbt(RPCMethod 定义),其参数文档为{"psbt_version", RPCArg::Type::NUM, RPCArg::Default(2), "The PSBT version number to use."}。 - 客户端使用命名参数(
-named)调用时,src/rpc/client.cpp中的映射会自动把psbt_version落到上表对应的索引位置,因此命名与位置两种调用方式等效。
源码实现链路:解析、校验与构造
以 createpsbt 为例(src/rpc/rawtransaction.cpp#L1700-L1745),处理流程为:
- 构造底层未签名交易:
ConstructTransaction(...)根据 inputs/outputs/locktime/version 参数生成CMutableTransaction; - 解析版本参数:默认
psbt_version = 2,若request.params[5]非 null 则覆盖; - 合法性校验:
if (psbt_version != 2 && psbt_version != 0) throw JSONRPCError(RPC_INVALID_PARAMETER, "The PSBT version can only be 2 or 0")(L1733-L1735); - 构造并序列化:
PartiallySignedTransaction psbtx(rawTx, psbt_version),注意PartiallySignedTransaction的构造函数本身就带uint32_t version = 2的默认值(src/psbt.h#L1269),随后ssTx << psbtx走前述 Serialize 逻辑输出 base64。
converttopsbt 的链路基本一致(L1775-L1812):先解码原始交易 hex,清除所有输入的 scriptSig/scriptWitness,然后同样以 uint32_t psbt_version = 2 起步做参数读取与 0/2 校验,再构造 PartiallySignedTransaction。walletcreatefundedpsbt 则在 FundTransaction 完成选币找零后,以同样的方式解析第 6 个参数并构造 PSBT(src/wallet/rpc/spend.cpp#L1786-L1796),随后调用 wallet.FillPSBT(psbtx, {.sign = false, ...}) 填充钱包数据但不签名。
这条链路说明:版本参数在进入序列化之前就被完整校验,非法版本不会留下任何中间状态;而 v2 输出金额的必填校验则发生在输出填充阶段(FillPSBT 触发 PSBTOutput 校验时),对离线钱包这类"只知道地址、不知道金额"的使用者,v2 会直接报错而非静默生成不合规 PSBT。
实际操作:如何使用 psbt_version 参数
以下示例均可在节点本地通过 RPC(如 bitcoin-cli)验证:
# 1) 创建 PSBT(默认即 v2,显式传 2 等效)
createpsbt '[{"txid":"myid","vout":0}]' '[{"address":"bc1q...":0.1}]' 0 2 2
# 2) 需要与旧版解析器互操作时,显式回退到 v0
createpsbt '[{"txid":"myid","vout":0}]' '[{"data":"00010203"}]' 0 2 0
# 3) 命名参数写法(位置不敏感,推荐脚本使用)
-named createpsbt inputs='[{"txid":"myid","vout":0}]' outputs='[{"address":"bc1q...":0.1}]' psbt_version=0
# 4) 原始交易 hex 转 PSBT,第三参数为 psbt_version
converttopsbt "rawtransactionhex" false true 2
converttopsbt "rawtransactionhex" false true 0
# 5) 钱包创建带资金 PSBT(psbt_version 是第 6 个位置参数)
walletcreatefundedpsbt "[]" '[{"address":"bc1q...":0.5}]' 0 '{"add_inputs":true,"fee_rate":2}' true 2 0
# 6) PSBT 提费重签,psbt_version 为第 1 个位置参数(options 之后)
psbtbumpfee "<psbt_base64>"
psbtbumpfee "<psbt_base64>" 0
需要遵守的约束:
- 传
1、3或负数都会触发RPC_INVALID_PARAMETER,错误信息为 "The PSBT version can only be 2 or 0"; - 注意区分 PSBT 版本 与 交易版本号:
createpsbt等 RPC 返回对象中若含psbt_version字段,文档明确写着 "The PSBT version number. Not to be confused with the unsigned transaction version"(src/rpc/rawtransaction.cpp#L1077),后者由version参数(交易版本)控制,两者互不影响; walletcreatefundedpsbt与createpsbt的version参数默认值分别为DEFAULT_WALLET_TX_VERSION和 2,均指交易版本,勿与psbt_version混淆。
升级迁移建议
从源码结构与默认值设计看,Bitcoin Core 的态度是让 v2 成为长期默认,v0 仅作为兼容开关保留。调用方迁移时建议:
- 先验证下游解析能力:对返回 base64 解码后检查全局键——v2 会出现
PSBT_GLOBAL_VERSION(值 2)及TX_VERSION/INPUT_COUNT/OUTPUT_COUNT键;v0 则只有PSBT_GLOBAL_UNSIGNED_TX。 - 显式传参固定行为:在升级节点版本前的过渡期,脚本中显式传
psbt_version=0可锁定旧格式;升级后移除该参数即切换到 v2 默认值。 - 离线签名/填充场景:若对端是较老的钱包实现,
converttopsbt ... 0与psbtbumpfee ... 0是保持兼容的最低成本手段。
小结
本次变更(release-notes-21283.md)用一行可选参数完成了 PSBT 默认格式从 v0 到 v2 的平滑迁移:四个创建类 PSBT 的 RPC 默认输出 v2(更小、无冗余、约束更强),同时通过 psbt_version 参数保留了 v0 的显式回退能力。实现层面,版本参数在 src/rpc/rawtransaction.cpp 与 src/wallet/rpc/spend.cpp 中统一按 "默认 2、仅允许 0 或 2、非法即抛错" 的模式处理,最终由 src/psbt.h 中 PartiallySignedTransaction 的序列化逻辑区分输出 v0 的全局未签名交易字段或 v2 的计数/版本字段,相关行为亦可由 src/test/psbt_tests.cpp 与 src/test/fuzz/psbt.cpp 中的测试用例进一步印证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00