锁币有期限,解锁不赊账:lockunspent 的内存锁与持久锁 图 1
锁币有期限,解锁不赊账:lockunspent 的内存锁与持久锁 · 图 1

钱包自动选币是效率,也是风险:两个流程同时花同一枚币,轻则交易被拒,重则把不相关的工作流搅在一起。Bitcoin Core 钱包为此提供了一把手动闸——lockunspent,把指定输出从选币候选池里摘出去。但很多使用者不知道的是,这把锁有两种质地:一种随进程熄灭,一种写进数据库。锁错级别,重启一次就前功尽弃。本文基于 31.0 源码与 RPC 文档把两种锁讲透。

接口形态:布尔在前,清单在后

lockunspent 的参数顺序容易写反:第一个布尔参数是 unlock 方向——false 表示锁定、true 表示解锁;第二个是对象数组,每项为 txid 加 vout。第三个可选布尔参数 persistent 决定锁的质地:默认 false,锁只存在于内存;true 则把锁定状态写进钱包数据库。RPC 文档对内存锁的宿命写得干脆:节点停止或故障时,内存锁随进程退出而消失。

锁币有期限,解锁不赊账:lockunspent 的内存锁与持久锁 图 2
锁币有期限,解锁不赊账:lockunspent 的内存锁与持久锁 · 图 2

两种锁的语义分工

内存锁适合一次性编排:一段脚本先锁住某些 UTXO、构造并签好一笔 PSBT、广播成功后调用解锁——整个生命周期不过几分钟,没必要在数据库里留痕;进程崩了锁自动清零,恰好符合「要么整单完成、要么什么都没发生」的预期。

持久锁则用于跨重启的长期隔离:比如把某批地址对应的小额 UTXO 长期冻结在观察用途上,或者防止无人值守的定时脚本选中它们。代价是要有明确的解锁纪律——锁记录躺在钱包文件里,直到有人显式解锁才消失。而解锁的语义是不赊账的:文档写明解锁会同时清除持久与非持久两把锁,对同一个输出不存在「只解其中一种」的部分成功。

listlockunspent 负责回读,返回当前被锁定的 txid 与 vout 清单。写自动化脚本的标准姿势是「查询—锁定—执行—解锁」四步闭环,异常分支里兜底解锁,防止半路失败留下无人认领的锁。

与选币逻辑的联动

锁在哪里生效?钱包选币器凑金额时会跳过被锁输出。这带来一个容易踩的坑:锁太多会把「明明有钱却报余额不足」逼成现实——余额统计与可选币集合是两回事,锁定项在 listunspent 里仍可见,但发起交易时凑不出手(排障清单见 钱包说余额不足却明明有钱?这九处逐一核对,选币约束的框架见 钱包余额不是一个数字)。给钱包做大额归并前,先 listlockunspent 看一眼有没有历史脚本留下的残留锁,是一条便宜的保险。

与租约锁的对照

闪电网络侧的 lncli wallet leaseoutput 是另一套哲学:锁定自带到期时间,多任务同时出资时按租约抢占,天然免疫「忘了还」的故障(租约机制的完整拆解见 lncli wallet leaseoutput:给 UTXO 上租约)。Bitcoin Core 的 lockunspent 没有到期概念——它更朴素,也更依赖使用者的闭环纪律。两种工具指向同一条原则:凡是被并行使用的钱包,选币权必须显式仲裁;而两套锁的语义并不互通——Core 的锁管不到闪电的出资选币,闪电的租约也不约束 Core 的交易构造,双栈钱包的并发纪律要各记各的账。

实操清单

一、锁定永远要决策第三参数:脚本内短期用默认内存锁,跨重启需求显式持久锁;二、用完即解锁,别赌「反正重启会清」——持久锁不会自己消失,而一次误重启会把内存锁静默清空,两种失败方向恰好相反,测试时要把「重启后查 listlockunspent」列为固定回归步骤;三、部署定时任务前扫一遍 listlockunspent,把历史残留纳入告警;四、涉及多签或硬件签名器的流程,锁定与解锁放进同一事务边界,失败分支也要执行解锁。锁本身不花手续费、不上链,它的成本全部体现在流程正确性上。

风险提示:本文描述节点与钱包的功能行为,不构成投资建议;自动化花销资产仍需自行管理密钥与备份风险。