首页
/ Bitcoin Core 离线签名实战:用 PSBT 在离线钱包与在线节点间安全完成签名转账

Bitcoin Core 离线签名实战:用 PSBT 在离线钱包与在线节点间安全完成签名转账

2026-09-06 23:00:11作者:何将鹤

本篇基于 Bitcoin Core 仓库中的 离线签名教程 编写,讲解如何用两台分别运行在线与离线实例的 Bitcoin Core,借助 PSBT(Partially Signed Bitcoin Transactions)让私钥永远不接触任何网络设备,完成"接收—建单—签名—广播"全流程;读完你可以完整掌握 exportwatchonlywalletrestorewalletsendwalletprocesspsbtsendrawtransaction 这条离线签名链路的使用方法,并理解每个 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 派生路径;
  • scriptPubKey00145f2804...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 与前面 listunspentscriptPubKey 的 hash160 完全一致——即缺少"对应该公钥哈希的私钥"所提供的签名;
  • estimated_vsize(141 vB)、estimated_feeratefee(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 完整支持五个参数:psbtsign(默认 true)、sighashtypebip32derivsfinalize(默认 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 余额与交易明细

安全要点归纳:

  1. 私钥永不联网:签名全程在离线主机完成,网络攻击面被物理切断;
  2. 钱包文件加密createwallet 时的 passphrase 是防止离线主机失窃后的最后防线;
  3. 签名前必须 analyzepsbt:在离线机器上核对收款地址、金额、手续费,防止在线机器被入侵后伪造交易;
  4. 注意硬派生限制:从源码帮助文本可知,含硬派生的描述符导出的 watch-only 文件只带有限已派生公钥,需要新地址时应重新导出(见 exportwatchonlywallet 实现);
  5. mainnet 迁移:删除全部命令中的 -signet,并把水龙头步骤换成真实充值即可,流程不变。
登录后查看全文
热门项目推荐
相关项目推荐