- 区块链
- DeFi
- 金融科技
【免费下载链接】bisq
A decentralized bitcoin exchange network
导读
本文深入解读 Bisq(去中心化比特币交易所)BSQ Swap 协议中的一份关键安全规范——bsq-swap-seller-inputs.md。该规范定义了:在买家对最终 swap 交易签名之前,必须如何验证卖家(BTC 卖家)的每一笔交易输入,以及卖家在选币时不得选用哪些输出。读完本文,你将掌握 BSQ Swap 交易中输入描述的传输格式(RawTransactionInput)、"输入描述必须与实际花费输出严格绑定"的安全不变量、卖家签名形式白名单(P2PK/P2PKH/P2WPKH)、DAO 状态下 BSQ 输出的排除规则,以及买家侧校验在源码中的完整实现(ProcessBsqSwapFinalizeTxRequest、TransactionValidation),并了解该规范的兼容性与明确不覆盖的边界。
适用范围与协议上下文
BSQ Swap 是一笔同时包含 BSQ 着色输入与 BTC 输入的单笔 Bitcoin 交易:
- 买家贡献 BSQ 输入,卖家贡献 BTC 输入;
- 卖家收到 BSQ,买家收到 BTC;
- 任何未分配给输出的输入值成为 Bitcoin 矿工费。
本规范约束的是买家(BTC 的买方)视角,且无论买卖双方是 maker 还是 taker 均适用。协议流程中:
- 卖家构建 swap 交易,并连同每个输入的描述信息一起发给买家;
- 买家依据描述验证卖家是否支付了其应承担的矿工费份额、是否未拿走超出应得的找零;
- 验证通过后,买家才对自己的输入签名、广播并回传交易。
关于费用如何分担(矿工费 vs 交易费),详见 docs/bsq-swap-fee-handling.md。卖家对买家输入的校验不在本规范范围内——卖家是自己根据买家的输入描述构建交易,因此买家输入天然与其描述绑定;卖家对买家签名合法性的校验由 docs/specifications/trade/bsq-swap-buyer-signatures.md 规定。
从源码消息结构看,卖家输入描述通过BsqSwapFinalizeTxRequest传输(见 BsqSwapFinalizeTxRequest.java),其字段包括:
tx:卖家构建的(已部分签名)swap 交易序列化字节;btcInputs:卖家各 BTC 输入的描述列表;btcChange:卖家 BTC 找零金额;bsqPayoutAddress:卖家 BSQ 收款地址;btcChangeAddress:卖家 BTC 找零地址。
单个输入描述由RawTransactionInput承载(见 RawTransactionInput.java),对应规范所述的四项要素:
| 字段 | 含义 |
|---|---|
parentTransaction | 被花费输出所在父交易的序列化字节 |
index | 被花费输出在父交易中的输出索引 |
value | 该输出的金额(聪) |
scriptTypeId | 该输出脚本类型 id(用于判断是否为 segwit,例如 P2WPKH/P2WSH) |
安全不变量:输入必须严格花费描述所指的输出
核心不变量:买家签名前,交易的每个卖家输入必须恰好花费其对应描述所指定的输出——相同的父交易 ID、相同的输出索引,且与描述的顺序一一对应。所有使用卖家输入金额或脚本类型的校验,都必须以该输入实际花费的输出为准。
为什么买家自己的签名不足以保护自己
买家的签名会承诺所有输入的outpoint,但不会承诺卖家输入的金额。如果卖家输入可以花费与描述不同的输出,卖家就能构造攻击:
- 描述同一父交易中金额较大的输出,实际却花费该交易中金额较小的输出;
- 买家按描述的大金额校验矿工费与找零,卖家便可以从矿工费(包括买家自己的份额)中"找回"多余的找零。
该攻击的杠杆被进一步放大:taker 可以选择高达maker 本地费率估算两倍的费率(由MinerFeeValidation.checkMinerFeeRateIsInTolerance控制,见 MinerFeeValidation.java 中MAX_FEE_DEVIATION_FACTOR = 2)。当买家是 maker 时,其承担的费率份额会更大。
三条规则
规范将不变量落为三条可执行的规则:
- 父交易 ID 绑定输出:父交易 ID 由描述中的父交易计算得出,而父交易 ID 承诺了其全部输出。因此,只要输入花费的正是描述的 outpoint,其金额与脚本类型就必然等于描述输出。
- 输入数量一一对应:交易中每个描述都必须有一个卖家输入,且这些输入位于买家输入之后。
- 禁止重复花费同一 outpoint:否则同一描述可以列出两次,其金额被重复计数两次。
源码中的落实:ProcessBsqSwapFinalizeTxRequest
买家侧验证集中在 ProcessBsqSwapFinalizeTxRequest.java。其关键步骤:
checkTransaction解析并验证卖家交易(Transaction.verify()),同时拒绝重复花费同一 outpoint 的交易(规则 3 的源码实现);input.validate(btcWalletService)校验描述中 value/scriptTypeId 与父交易对应输出一致(防"描述虚高"的静态校验);checkInputOutpoints逐一比对交易输入的实际 outpoint 与描述 outpoint(getHash()与getIndex()必须同时相等),并且要求交易中的卖家输入数量与描述数量相等(规则 1、2 的源码实现);- 校验卖家输入金额总和是否不低于本地按交易大小与费率计算出的
getSellersBtcInputValue; - 校验卖家找零
btcChange <= expectedChange,确保卖家没有从矿工费中窃取(详细费用公式见下文)。
其中checkInputOutpoints与checkInputSignatures均在 TransactionValidation.java 中实现,并有对应单元测试 TransactionValidationTest.java:
checkInputOutpointsAcceptsInputWhichSpendsTheExpectedOutpoint:花费预期输出则通过;checkInputOutpointsRejectsOtherOutputOfTheExpectedParentTransaction:同一父交易中花费另一个输出则被拒绝;checkInputOutpointsRejectsOutputOfAnotherParentTransaction:花费另一个父交易的输出则被拒绝。
BSQ 输出不得作为卖家输入
规则:卖家只能用 BTC 支付
买家签名前,任何卖家输入都不得花费买家 DAO 状态中记录为未花费 BSQ 输出的输出。为执行该规则,买家的 DAO 状态必须就绪且已同步,否则买家直接拒绝该请求(源码中为daoFacade.isDaoStateReadyAndInSync()检查)。
DAO 状态必须同步的原因(源码注释亦明确):
- 花费硬分叉 3 高度起的lockup 输出(见 docs/specifications/dao/bond-lockup-spend.md),或花费锁定期未过的 unlock 输出,会使整笔交易对 DAO 而言无效;
- 无效交易的全部 BSQ 输入被烧毁、任何输出都不会成为 BSQ 输出,买家将失去自己的 BSQ 找零。
规则按"输入实际花费的输出"评估
如同安全不变量所要求的,该 BSQ 排除规则也必须基于输入实际花费的输出来评估。如果只检查描述输出,攻击者可以描述同一父交易中的 BTC 输出,实际却花费其 BSQ 输出,从而绕过检查。
源码中的实现方式:对每个sellersRawBtcInputs构造TxOutputKey(parentTxId, index),并断言!daoFacade.isUnspentTxOutput(key)。注意 key 的父交易 ID 通过input.getParentTxId(btcWalletService)从序列化的父交易解析而来,这与"父交易 ID 承诺其输出"的原理一致。
什么输出不算 BSQ 输出
BSQ 交易中被 DAO 归类为 BTC 输出的输出,不属于 BSQ 输出,卖家可以花费。典型例子:一笔以 BSQ 支付交易费的交易的 BTC 找零输出。
卖家的镜像规则:选币时排除 BSQ 输出
卖家在选择自己的 BTC 输入时(既用于 swap 交易本身,也用于资金估算)必须应用同样的规则:不选择其 DAO 状态记录为未花费 BSQ 输出的输出(例如发送到卖家某个 BTC 地址的 BSQ)。如果除这类输出外,卖家的 BTC 钱包没有足够 BTC,则卖家视为资金不足(missing funds)。
该逻辑在 BsqSwapCalculation.java 中实现:
getSellersBtcInputsAndChange通过谓词isUnspentBsqOutput(daoFacade)过滤不可用的输出;getSellersSelectableBtcBalance返回可选的 BTC 余额,同样排除未花费 BSQ 输出;- 其迭代式费用估算从
MIN_SELLERS_TX_SIZE = 104vBytes 起步,反复用getVBytesSize(segwit 输入 68 vBytes、非 segwit 输入 149 vBytes、有找零 62、无找零 31)校正所需输入,最多迭代 10 次。
卖家作为 taker 时,处理买家 BSQ 输入消息(BsqSwapTxInputsMessage)的ProcessBsqSwapTxInputsMessage(见 ProcessTxInputsMessage.java)同样先断言 DAO 状态就绪,并用 DAO 的unspentTxOutputMap交叉验证买家的每个 BSQ 输入确实存在、未花费且可花费(isTxOutputSpendable)。测试用例 BsqSwapSellerDaoStateGuardTest.java 验证了 DAO 状态未同步时,seller-as-maker 与 seller-as-taker 均会以 "DAO state is not ready and in sync" 拒绝请求。
卖家签名形式:白名单严格校验
只有三种输出类型、固定签名形式
买家签名前,每个卖家输入必须处于卖家钱包签名的形式,且签名对花费输出的金额与脚本有效。Bisq 钱包只签 P2PK、P2PKH、P2WPKH 三类输出,且一律使用 SIGHASH_ALL,形式固定:
| 输出类型 | 合法形式 |
|---|---|
| P2WPKH | 空 scriptSig,witness 中恰好包含签名与压缩公钥(两项 push) |
| P2PKH | scriptSig 中恰好包含以最短 push 编码的签名与公钥,无 witness |
| P2PK | scriptSig 中恰好包含以最短 push 编码的签名,无 witness |
公钥必须是压缩或非压缩的标准编码,网络不会中继混合编码(hybrid)公钥。买家拒绝其他任何形式。
该白名单正是卖家钱包签名的产物。对照 TradeWalletService.java 的signInput:P2PK/P2PKH 走hashForSignature(..., SIGHASH_ALL, false)+ScriptBuilder.createInputScript,P2WPKH 走calculateWitnessSignature+redeemP2WPKH(空 scriptSig + witness),其余脚本类型直接抛出SigningException("Don't know how to sign...")。
买家侧的镜像校验
买家侧的checkInputSignatures(源码注释逐字对应本规范)通过isSignedInBisqWalletForm校验:
- P2WPKH:
scriptBytes.length == 0、witness 恰好 2 项 push、签名末字节为 SIGHASH_ALL、公钥 push 长度为 33 字节(压缩); - P2PK/P2PKH:无 witness、scriptSig push 数分别为 1/2、所有 push 均为最短编码(
isShortestPossiblePushData)、签名带 SIGHASH_ALL; - 再通过
Script.correctlySpends(..., Script.ALL_VERIFY_FLAGS)用花费输出的金额与脚本实际验签。
为什么只验这三种类型就够了
Script.correctlySpends只为上述类型验证签名,而且即便如此也不覆盖网络的全部规则,例如:
- witness 项数、witness 签名的 ANYONECANPAY 标志;
- scriptSig 的中继规则。
因此,一个被网络拒绝或不中继的卖家输入,会让买家拿到一笔永不确认的交易。若卖家之后能修正该输入,这也会给卖家留下与"未签名输入"相同的主动权(详见下文"未覆盖"部分)。
SIGHASH_ALL 签名的覆盖范围
卖家输入使用的 SIGHASH_ALL 签名覆盖所有输入的 outpoint 与所有输出,但不覆盖买家输入的脚本与 witness。因此买家随后对自己输入签名不会使卖家的签名失效——这正是两阶段签名流程(卖家先签、买家后签)能够成立的前提。测试用例checkInputSignaturesAcceptsValidP2wpkhSignature/checkInputSignaturesAcceptsValidP2pkhSignature/checkInputSignaturesRejectsMissingSignature(TransactionValidationTest.java)分别验证了这两类合法形式通过、缺失签名被拒绝。
这一规则防止买家签出一笔因卖家输入而残缺的交易;但并未剥夺卖家事后决定 swap 是否发生的选项——使用一笔尚未发布的父交易作为输入具有相同的效果,且买家无法察觉(见下文)。
买家签名与交易一致性交叉校验
买家侧在 BuyerCreatesAndSignsFinalizedTx.java 中,用本地数据重新构建完整交易(买家 BSQ 输入、卖家 BSQ 收款、双方找零、买家 BTC 收款等全部取自本地模型,而非直接信任卖家消息),然后:
checkArgument(Arrays.equals(transaction.bitcoinSerialize(), tradePeer.getTx()), "Buyers unsigned transaction does not match the sellers tx");若序列化结果与卖家发来的交易不一致则拒绝签名。换言之,BsqSwapFinalizeTxRequest中仅卖家的 BTC 输入被原样采用,交易其余部分由买家重建并与卖家交易逐字节比对。这保证了:
- 费用与找零金额以本地按费率、交易大小计算的期望值为准;
- 卖家无法通过捏造交易结构窃取买家份额;
- 买家输入的脚本/witness 差异不影响比对(因为卖家部分签名中买家输入为空)。
同时,买家在 BuyerPublishesTx.java 中通过publishAndCommitBsqTx广播交易并在广播结果未知前即回传BsqSwapFinalizedTxMessage给卖家(广播超时按成功处理)——这是本规范明确不覆盖、由协议设计承担的风险(见下文)。
费用校验如何依赖输入绑定不变量
买家对卖家输入的金额校验(ProcessBsqSwapFinalizeTxRequest中的expectedChange计算)依赖 docs/bsq-swap-fee-handling.md 所述的分担模型:
adjustedTxFee = max(0, txFeePerVbyte * 该方vBytes - 该方BSQ交易费)- 买家 BSQ 输入 = bsqTradeAmount + buyersTradeFee;
- 买家 BTC 收款 = btcTradeAmount - adjustedBuyerTxFee;
- 卖家 BTC 输入 = btcTradeAmount + adjustedSellerTxFee;
- 卖家 BSQ 收款 = bsqTradeAmount - sellersTradeFee。
买家校验时用**卖家输入描述(已绑定实际花费输出)**的金额之和减去本地算出的买家 BTC 收款与双方调整后交易费,得到expectedChange,并断言change <= expectedChange——只允许找零小于等于期望值(dust 找零、币选择溢出会落入矿工费),绝不允许大于期望值(那意味着卖家回收了本该付矿工费的钱)。注释明确写道:"By enforcing that it must not be larger than expectedChange we guarantee that peer did not cheat on tx fees."
兼容性
- 所有版本的诚实卖家都用自己的描述构建输入,因此本规范的绑定规则不会拒绝诚实交易;消息结构与持久化数据均未变化。
- 旧版本卖家在选币时不排除 BSQ 输出,可能选中其 BTC 钱包持有的 BSQ 输出。新买家拒绝此类请求,而不是签出一笔改变 DAO 解析方式的交易。
- 买家在处理卖家请求时新增了对DAO 状态就绪且同步的检查。
未覆盖的范围与已知边界
规范明确列出以下不属于本规范解决的攻击面,理解它们有助于正确评估 BSQ Swap 协议的整体信任模型:
- SPV 钱包限制:买家使用 SPV 钱包,无法验证卖家输入是否存在、是否未花费、是否已确认。卖家因此可以拖延或阻止swap 确认——例如使用一笔尚未发布或日后被替换的父交易作为输入;买家钱包将显示一笔永不确认的 swap。
- DAO 状态尚未收录的输出:未被 DAO 状态收录的输出(例如父交易尚未确认)不会被识别为 BSQ 输出——买家校验与卖家选币均无法察觉。
- 广播时序:买家在广播结果未知前就将交易提交至钱包并回传卖家,广播超时按成功处理;因此一笔被网络拒绝的交易仍可能在买家侧被标记为交易完成。
- taker 费率容忍度不调整:taker 选择费率,maker 接受其自身估算一半到两倍之间的费率(
MinerFeeValidation.checkMinerFeeRateIsInTolerance)。在本规范的不变量下,卖家无法从矿工费中拿回任何部分,但接手买家挂单的卖家仍可让买家按最高两倍估算费率支付份额,且该份额随买家输入与找零输出规模增大;多付部分归矿工,卖家按相同费率支付自身份额。该容忍度使费率估算不同的交易者可以相互成交,收紧它会拒绝今天可接受的成交,因此是否调整属于维护者的决策,不随本变更进行。
总结:一份从消息到签名全链路可验证的规范
bsq-swap-seller-inputs.md是 BSQ Swap 双人签名流程中买家侧安全的基石。它通过"输入描述与实际花费输出严格绑定"的安全不变量、DAO 状态下的 BSQ 输出排除、以及 P2PK/P2PKH/P2WPKH 签名形式白名单三组规则,配合仓库中 ProcessBsqSwapFinalizeTxRequest.java、TransactionValidation.java、BsqSwapCalculation.java 等实现与 TransactionValidationTest.java 等测试,确保买家在签名前即可判定:卖家付出了应承担的矿工费、没有从交易费中窃取找零、输入完整可确认,且交易在买家签名后立即可广播。同时,规范也诚实地划定了 SPV 钱包、DAO 收录时序与广播时序等协议层面的固有边界,为审计与后续演进提供了清晰的参照。
- 区块链
- DeFi
- 金融科技
【免费下载链接】bisq
A decentralized bitcoin exchange network
相关推荐
Bisq BSQ Swap 买家签名校验规范:如何防止无效签名与重复输入提交
Bisq BSQ Swap 买家签名校验规范:如何防止无效签名与重复输入提交 导读 本文解读 Bisq 去中心化交易所中 BSQ Swap(BSQ 兑 BTC
区块链DeFi金融科技financial-services 仓库 investment-banking 插件 buyer-list 技能实战:构建卖单并购的买家名单与买家宇宙
financial services 仓库 investment banking 插件 buyer list 技能实战:构建卖单并购的买家名单与买家宇宙 导读
人工智能AI 应用AI 技能/插件AI Agent金融科技如何让十年老电脑重新流畅:低成本翻新与性能优化实战指南
如何让十年老电脑重新流畅:低成本翻新与性能优化实战指南 桌上那台开机要等两分钟、硬盘灯常年不熄的老机器,真的只能进闲鱼吗?未必。这篇低成本旧电脑升级实战指南,给
操作系统固件驱动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考