Bitcoin Core 离线签名实战:用 PSBT 在离线钱包与在线节点间安全完成签名转账
本篇基于 Bitcoin Core 仓库中的 离线签名教程 编写,讲解如何用两台分别运行在线与离线实例的 Bitcoin Core,借助 PSBT(Partially Signed Bitcoin Transactions)让私钥永远不接触任何网络设备,完成"接收—建单—签名—广播"全流程;读完你可以完整掌握 exportwatchonlywallet、restorewallet、send、walletprocesspsbt、sendrawtransaction 这条离线签名链路的使用方法,并理解每个 RPC 在源码层的实际行为。
概述:为什么需要离线签名
私钥一旦存放在联网设备上,就存在被恶意软件、键盘记录器或网络攻击者窃取的风险。Bitcoin Core 的离线签名方案通过物理隔离(air-gap)大幅降低这种机会:
offline主机:与所有网络(互联网、Tor、WiFi、蓝牙等)完全断开,甚至不需要同步区块链数据;online主机:常规在线节点,拥有已同步的区块链。
整个工作流围绕 PSBT 展开:在线的 watch_only_wallet(只读钱包)负责追踪交易、构造未签名交易;私钥保留在离线的 offline_wallet 中,只对 PSBT 文件签名;最后由在线节点广播已签名并 finalize 的 PSBT。PSBT 机制本身的格式说明见 PSBT 文档。
教程示例基于 Bitcoin Core v32.0、
signet测试网络。在mainnet上使用时,只需去掉命令中的-signet参数。
前置要求
- 两台主机均已安装并构建 Bitcoin Core(
./build/bin/bitcoin-cli可用); jq(可选):教程用jq -r '.psbt'从 JSON RPC 响应中抽取字段,没有它也可以手工复制 JSON 值。
第一步:在离线主机上创建并准备 offline_wallet
1. 创建带密码短语加密的钱包
在离线机器上创建钱包 offline_wallet,并立即用 passphrase 加密它。这个钱包持有私钥,必须永远保持离线:
[offline]$ ./build/bin/bitcoin-cli -signet -named createwallet \
wallet_name="offline_wallet" \
passphrase="** enter passphrase **"
{
"name": "offline_wallet"
}
使用
passphrase加密wallet.dat至关重要:即使未授权的人拿到了离线主机,也无法读取钱包内容。钱包加密的更多细节见 Managing the Wallet — 加密钱包一节。
createwallet 同时接受位置参数与 -named 命名参数两种写法;教程采用 -named 形式以明确各参数含义(钱包名、可选的 passphrase、disposable、avoid_reuse 等)。
2. 导出 watch-only 钱包文件
使用 exportwatchonlywallet 将当前钱包以"只读"格式导出为 watch_only_wallet.dat:
[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" -named exportwatchonlywallet \
destination=/path/to/watch_only_wallet.dat
从源码可以看到该命令的确切语义(exportwatchonlywallet 实现):导出的文件包含当前钱包的公开描述符(public descriptors)、已有交易和地址簿数据,因此可以在另一台节点上用 restorewallet 导入。一个关键细节是:使用硬派生(hardened derivation)的描述符在导出时只会包含有限数量的已派生公钥——因为硬派生需要私钥才能继续派生,而导出文件中没有私钥;不含硬派生的描述符则没有这个限制。这对实践的意义是:离线钱包后续新派生的地址,可能不会自动出现在 watch-only 钱包中,需要重新导出或按地址接收。
watch_only_wallet.dat 之后要通过物理介质(如 U 盘)转移到在线机器上。
第二步:在在线主机上创建 watch_only_wallet
将 U 盘中的文件导入在线机器,用 restorewallet 恢复出只读钱包 watch_only_wallet。它的私钥是禁用的,职责有三:追踪并验证 incoming 交易、花费时创建未签名 PSBT、广播已签名并 finalize 的 PSBT:
[online]$ ./build/bin/bitcoin-cli -signet -named restorewallet \
wallet_name="watch_only_wallet" \
backup_file=/path/to/watch_only_wallet.dat
{
"name": "watch_only_wallet"
}
从 restorewallet 的源码 可以看到该命令接受三个参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
wallet_name |
STR | 是 | 恢复后钱包的名称 |
backup_file |
STR | 是 | 用于恢复的备份文件路径(此处即 watch_only_wallet.dat) |
load_on_startup |
BOOL | 否 | true 加入启动加载列表,false 移除,不传则保持原状 |
源码中的帮助文本还提示:如果节点开启了 -blockfilterindex=1(block filter index),恢复时的重扫描会显著更快——watch-only 钱包导入后需要重扫链上交易,这是在线主机上耗时最长的一步。返回结果为 {"name": ...},如有恢复相关告警会附带 warnings 数组。
第三步:给 offline_wallet 充值
关键点:offline_wallet 与在线 watch_only_wallet 共享同一组公钥(源自同一份描述符),因此二者生成的地址完全相同。交易可以用任意一个钱包创建,但只有持有私钥的 offline_wallet 能添加有效签名。
1. 生成收款地址
用在线 watch_only_wallet 生成地址(离线钱包生成结果相同):
[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" getnewaddress
tb1qtu5qgc6ddhmqm5yqjvhg83qgk2t4ewajg0h6yh
2. 从水龙头领取 signet 币
访问 signet 水龙头(如 signetfaucet.com),填入上一步的地址即可收到少量测试币。
3. 用 listunspent 确认收款
根据节点连接状况,交易可能需要几秒到几分钟才会同步到本地节点,反复执行直到出现输出即可:
[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" listunspent
[
{
"txid": "0f3953dfc3eb8e753cd1633151837c5b9953992914ff32b7de08c47f1f29c762",
"vout": 1,
"address": "tb1qtu5qgc6ddhmqm5yqjvhg83qgk2t4ewajg0h6yh",
"label": "",
"scriptPubKey": "00145f2804634d6df60dd080932e83c408b2975cbbb2",
"amount": 0.01000000,
"confirmations": 4,
"spendable": true,
"solvable": true,
"desc": "wpkh([306c734f/84h/1h/0h/0/0]025932ccee7590158f7e08bb36290d135d30a0b045163da896e1cd7645ec4223a9)#xytvyr4a",
"parent_descs": [
"wpkh([306c734f/84h/1h/0h]tpubDCJnY92ib4Zu3qd6wrBXEjG436tQdA2tDiJU2iSJYjkNS1darssPWKaBfojhjUF5vMLBcxbN2r93pmFMz2zyTEZuNx9JDo9rWqoHhATW3Uz/0/*)#7mh08dkg"
],
"safe": true
}
]
几个值得注意的字段:
"spendable": true且"solvable": true:该输出既已被本钱包掌控,钱包也知道如何为其签名(注意 watch-only 钱包实际只能"构造"待签交易);"desc"显示这是wpkh(P2WPKH,即 SegWit 原生地址)描述符,父级描述符为84h/1h/0h的标准 BIP84 派生路径;scriptPubKey中00145f2804...的5f2804634d6df60dd080932e83c408b2975cbbb2就是该地址对应的 hash160,后面分析 PSBT 时会再见到它。
第四步:创建并导出未签名 PSBT
1. 确定收款地址
教程示例把资金发到 tb1q9k5w0nhnhyeh78snpxh0t5t7c3lxdeg3erez32;若不需要后续继续测试,也可以把币发回水龙头。
2. 用 send 构造未签名 PSBT 并落盘
send 是 Bitcoin Core 的新一代发送 RPC(替代旧版 sendtoaddress 的用法)。在 watch-only 钱包上调用时,它不会提交交易,而是返回一个已填充输入但未签名的 PSBT(这正是离线流程想要的)。用 jq -r '.psbt' 抽出 base64 字符串写入 funded_psbt.txt,便于通过物理介质搬到离线机器:
[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" send \
'{"tb1q9k5w0nhnhyeh78snpxh0t5t7c3lxdeg3erez32": 0.009}' \
| jq -r '.psbt' \
>> /path/to/funded_psbt.txt
[online]$ cat /path/to/funded_psbt.txt
cHNidP8BAHECAAAAAWLHKR9/xAjetzL/FCmZU5lbfINRMWPRPHWO68PfUzkPAQAAAAD9////AoA4AQAAAAAAFgAULajnzvO5M38eEwmu9dF+xH5m5RGs0g0AAAAAABYAFMaT0f/Wp2DCZzL6dkJ3GhWj4Y9vAAAAAAABAHECAAAAAY+dRPEBrGopkw4ugSzS9npzJDEIrE/bq1XXI0KbYnYrAQAAAAD+////ArKaXgAAAAAAFgAUwEc4LdoxSjbWo/2Ue+HS+QjwfiBAQg8AAAAAABYAFF8oBGNNbfYN0ICTLoPECLKXXLuyYW8CAAEBH0BCDwAAAAAAFgAUXygEY01t9g3QgJMug8QIspdcu7IiBgJZMszudZAVj34IuzYpDRNdMKCwRRY9qJbhzXZF7EIjqRgwbHNPVAAAgAEAAIAAAACAAAAAAAAAAAAAACICA7BlBnyAR4F2UkKuSX9MFhYCsn6j//z9i7lHDm1O0CU0GDBsc09UAACAAQAAgAAAAIABAAAAAAAAAAA=
send 的第一个参数是 JSON 对象 {"地址": 金额, ...},可以包含多个收款人;watch-only 钱包没有私钥,所以其产物只能是待签 PSBT——这就是"谁持私钥谁签名、谁联网谁广播"职责分离的落点。
第五步:在离线主机上解码与分析 PSBT
把 funded_psbt.txt 带入离线机器,先"解码"再"分析",是签名前必做的人工安全审查:
[offline]$ ./build/bin/bitcoin-cli -signet decodepsbt $(cat /path/to/funded_psbt.txt)
{
...
}
[offline]$ ./build/bin/bitcoin-cli -signet analyzepsbt $(cat /path/to/funded_psbt.txt)
{
"inputs": [
{
"has_utxo": true,
"is_final": false,
"next": "signer",
"missing": {
"signatures": [
"5f2804634d6df60dd080932e83c408b2975cbbb2"
]
}
}
],
"estimated_vsize": 141,
"estimated_feerate": 0.00100000,
"fee": 0.00014100,
"next": "signer"
}
analyzepsbt 的输出直接告诉你当前交易"缺什么、下一步该谁做":
"next": "signer":交易还需要签名者处理;"missing.signatures"中的5f2804634d6df60dd080932e83c408b2975cbbb2与前面listunspent里scriptPubKey的 hash160 完全一致——即缺少"对应该公钥哈希的私钥"所提供的签名;estimated_vsize(141 vB)、estimated_feerate与fee(0.00014100 BTC)让你离线就能确认手续费与交易大小是否符合预期,在签名之前拦截任何被篡改的金额或手续费,这是离线签名安全模型的重要一环。
第六步:解锁、签名并 finalize PSBT
1. 解锁离线钱包
用 walletpassphrase 输入创建时设置的密码短语解锁钱包。第二个参数是解锁超时(秒),超时后钱包自动重新锁定:
[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" walletpassphrase "** enter passphrase **" 60
2. 用 walletprocesspsbt 完成签名与 finalize
walletprocesspsbt 会依次执行"补充 UTXO 信息 → 用钱包私钥签名 → finalize(组装 witness 与 scriptSig)"三步,输出中同时给出可继续流转的 PSBT 与 finalize 后的裸交易 hex。取 .hex 字段(finalize 后的交易十六进制)存入 final_psbt.txt:
[offline]$ ./build/bin/bitcoin-cli -signet -rpcwallet="offline_wallet" walletprocesspsbt \
$(cat /path/to/funded_psbt.txt) \
| jq -r .hex \
>> /path/to/final_psbt.txt
从 RPC 参数表 可以看到 walletprocesspsbt 完整支持五个参数:psbt、sign(默认 true)、sighashtype、bip32derivs、finalize(默认 true)。本流程使用默认值即可:既签名又 finalize。若日后想保留可继续添加签名的中间 PSBT(例如多签场景),把 finalize 设为 false 并取 .psbt 字段即可。
第七步:广播签名后的交易
在在线节点上用 sendrawtransaction 广播(注意此时不带 -rpcwallet,这是节点级 RPC):
[online]$ ./build/bin/bitcoin-cli -signet sendrawtransaction $(cat /path/to/final_psbt.txt)
c2430a0e46df472b04b0ca887bbcd5c4abf7b2ce2eb71de981444a80e2b96d52
返回的十六进制串即新交易的 txid。至此,私钥自始至终未接触网络,而交易已上链。
确认余额与交易历史
用在线 watch_only_wallet 核对余额变化:
[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" getbalances
{
"mine": {
"trusted": 0.00085900,
"untrusted_pending": 0.00000000,
"immature": 0.00000000
},
"lastprocessedblock": {
"hash": "0000003065c0669fff27edb4a71928cb48e5a6cfcdf06f491a83fd86822d18a6",
"height": 159592
}
}
trusted: 0.00085900 与"收到 0.01000000、支出 0.00900000 + 手续费 0.00014100"完全对账:0.01000000 − 0.00900000 − 0.00014100 = 0.00085900。
也可以列出钱包相关交易明细:
[online]$ ./build/bin/bitcoin-cli -signet -rpcwallet="watch_only_wallet" listtransactions
[
...
]
流程小结与安全要点
| 阶段 | 主机 | 命令 | 产物 |
|---|---|---|---|
| 建钱包(加密) | offline | createwallet offline_wallet |
持有私钥的钱包 |
| 导出公钥数据 | offline | exportwatchonlywallet |
watch_only_wallet.dat |
| 导入只读钱包 | online | restorewallet |
可追踪交易/建单的 watch-only 钱包 |
| 收款确认 | online | getnewaddress / listunspent |
收款地址与 UTXO |
| 建未签名交易 | online | send |
funded_psbt.txt(base64 PSBT) |
| 审查交易 | offline | decodepsbt / analyzepsbt |
确认金额、手续费、缺签信息 |
| 签名 + finalize | offline | walletpassphrase + walletprocesspsbt |
final_psbt.txt(hex 交易) |
| 广播 | online | sendrawtransaction |
txid |
| 对账 | online | getbalances / listtransactions |
余额与交易明细 |
安全要点归纳:
- 私钥永不联网:签名全程在离线主机完成,网络攻击面被物理切断;
- 钱包文件加密:
createwallet时的 passphrase 是防止离线主机失窃后的最后防线; - 签名前必须 analyzepsbt:在离线机器上核对收款地址、金额、手续费,防止在线机器被入侵后伪造交易;
- 注意硬派生限制:从源码帮助文本可知,含硬派生的描述符导出的 watch-only 文件只带有限已派生公钥,需要新地址时应重新导出(见 exportwatchonlywallet 实现);
- mainnet 迁移:删除全部命令中的
-signet,并把水龙头步骤换成真实充值即可,流程不变。
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