挂起、放行、撤销:LND 持有型发票三条命令的幂等边界与事件流 图 1
挂起、放行、撤销:LND 持有型发票三条命令的幂等边界与事件流 · 图 1

机制篇之外,接口细节才是集成的门槛

持有型发票先锁款、后由收款方决定放不放行的机制,本栏目已有专篇。真正把系统接起来时,问题会具体到命令与 RPC 层面:发票怎么创建、结算与撤销的重复调用会不会报错、状态变化怎么被程序感知。以下依据 LND v0.19.0-beta 源码,把这三段接口行为讲清楚。

挂起、放行、撤销:LND 持有型发票三条命令的幂等边界与事件流 图 2
挂起、放行、撤销:LND 持有型发票三条命令的幂等边界与事件流 · 图 2

幂等表与集成守则

把规则整理成守则:结算方向可以放心重试——「已结算的发票再结算会成功」这一设计让网络抖动不必惊动人工;撤销方向必须先看状态、或至少分辨失败原因——这里的失败很可能不是通道故障,而是「钱已放行」的业务事实,吞掉错误分支就会漏记。原子交换里这个分叉并不罕见:两边几乎同时提交结算时,一方的成功与另一方的报错指向同一事实,业务层要把两个分支收敛到同一条账务结论。

事件流的重连纪律

SubscribeSingleInvoice 先送当前状态、再送变化,这是重连场景的生命线:消费者不必每次断线后全量对账,因为重连的第一条事件自带发票现状,后续才是增量。写消费代码时不要把第一条事件当重复丢弃。查询侧配合 LookupInvoiceV2——付款哈希、付款地址、set ID 三种引用都能定位一张发票,多路径与 blinded 场景下手里只剩 set ID 时尤其有用。

addholdinvoice:哈希进门,预像留白

lncli addholdinvoice 的参数形式是 hash 与可选的 amt——第一个位置参数是预像的哈希,这是它与普通 addinvoice 最本质的差别:普通发票由 LND 自己生成预像与哈希,持有型发票则要求调用方带着现成的哈希来开单,预像留在调用方手里,等将来结算时再揭晓。AddHoldInvoiceRequest 消息里 memo、value 与 value_msat(两者互斥)、description_hash、expiry、cltv_expiry 等字段与普通发票同款,还支持不带金额的发票——金额留零时,付款方可以在支付时自行指定要付的聪数。这一点在做跨链交换时要格外小心:无金额的持有型发票把金额裁量交给了来款。

settleinvoice 与 cancelinvoice:两个相反的幂等方向

结算命令 settleinvoice 接一个 32 字节的十六进制预像。invoices.proto 注释写明:SettleInvoice 结算一张已接受的发票;如果发票已经结算过,这个调用会成功。撤销命令 cancelinvoice 接付款哈希,注释写得更细:取消一张当前开放的发票;如果发票已被撤销,调用成功;如果发票已经结算,调用会失败。两条规则合起来是一个不对称设计——结算操作可以重复(幂等友好,网络重试安全),撤销操作遇到已结算会明确报错(钱已经放了行,撤销方必须知道)。集成方在写重试逻辑时,对 cancelinvoice 的失败分支必须显式处理,别把错误当网络故障吞掉。

事件流与查询口

程序关心的是状态什么时候变。SubscribeSingleInvoice 返回一条服务器到客户端的单向流,注释写明它通知指定发票的状态迁移,并且订阅建立的初始时刻总会先发出当前状态——这意味着重连后的消费者不会丢起点。查询侧 LookupInvoiceV2 允许用付款哈希、付款地址或 set ID 三种方式引用一张发票,对 AMP 与 blinded 场景尤其有用。

风险提示

还有一个运营细节:状态机只有四个状态且没有内置超时机制,超时策略要自己做——发票长期停在 OPEN、已锁款迟迟不结算,都应在业务侧设置告警时限。时限的取值要与时间锁参数对齐,留出结算与确认所需的时间余量。

对账口径也值得写进接入文档:持有型发票的结算证据是预像与锁款交易的对应关系,两者都要落到节点台账;只记录结算成功回执而丢了预像来源,事后争议时很难自证。

持有型发票把放行决定权留在收款方,也把预像保管责任留在收款方:预像丢失意味着已锁的款项无法结算,撤销已结算发票的失败分支处理不当会造成账务错乱。闪电与链上资金存在现实损失风险,本文不构成投资建议。