HOME/Articles/

"没有中间商赚差价"

Article Outline

"没有中间商赚差价"

晚上好, 这里是 ljzn.

最近在工作中遇到到了一个问题, 如何计算一笔比特币交易的手续费? 这个问题本来不需要过多操心, 在交易费用很低的情况下, 只要保证费率不低于最低限度就可以了. 但是大家也知道, 现在 BTC 的手续费高得吓人, 而且费用给低了很难被打包确认. 所以相对精准的计算手续费就成了一件很重要的事情.

手续费的计算方式是 交易体积 乘以 费率. 对于非隔离见证交易, 这里的交易体积就是签名后的交易序列化之后的体积. 而对于隔离见证交易, 签名被移动到了 witness 结构里, 而 witness 结构的费率是交易的其它部分的四分之一. 详细的我们下文会说. 这里先吐槽一下隔离见证, 隔离见证一直鼓吹的一点是它可以降低手续费, 事实上是 "个子不长尺子长", 直接通过修改体积计算的规则, 用"虚拟体积" vsize 来替代真实体积, 达到降低手续费的目的.

吐槽完毕. 来讲讲手续费计算为什么这么麻烦. 因为涉及到了几个变量, 一个是签名, 一个是找零. 在得到签名之前, 我们是不知道签名的长度究竟是多少的, 根据 bitcoin wiki 里面所说, 签名的长度有可能是 73, 72, 71, 概率分别是 25%, 50%, 25%. 为了安全起见, 我们统一使用 73 bytes, 以免遇到手续费不足被节点拒收的情况.

再说找零, 打个比方, 消费者拿 100 元去买 98 元的东西, 正常找零是 2 元. 这时候中间商说要收手续费, 1 元, 那么找零就变成了 1 元. 中间商再涨价, 手续费 2 元, 找零就没了. 中间商黑心至极, 手续费提到 3 元, 这一笔交易直接就没法进行了, 除非消费者再加钱. 我们前面也提到了, 比特币手续费等于 交易体积 乘以 费率. 这里消费者又拿出一张 5 元的纸币, 交易体积是增加了 1 个 input 的体积, 因此手续费又会提升 (1 个 input 的体积 乘以 费率), 提升的这部分手续费, 能不能被 5 元覆盖到呢, 如果超出了 5 元, 消费者又需要加钱, 循环往复.

为避免这种入不敷出的情况, 我们引入一个概念: gain(增益). 不知道大伙有没玩过炉石,昆特牌这类的卡牌游戏, 里面有一种效果就是给牌面增加点数, 叫做增益. 类比到刚才的例子上, 我们定义 5 元纸币的增益等于 5 - 它所产生的 input 的手续费. 这样, 如果一张纸币(在比特币里的 UTXO)的增益为负数, 我们就不应该使用它. 我们把能够满足增益等于 0 的 UTXO 面值称为 最小可花费面值. 有些特殊情况, 我们要打的是"效果牌" ---- 不关心这个 UTXO 的面值, 关心的是它的其它特性, 比如转账 USDT 的时候, 需要一个 input 来表示发出 USDT 的地址, 这里就不需要去要求其增益为正数了.

说完 Input 说说 Output. 和输入一样, 交易的输出也是要计算体积的, 在刚开始构造交易的时候, 我们知道这笔交易时要发送给谁, 例如上面例子中的 98 元. 不知道的是是否要找零, 其实就两种情况, a. 有找零, b.无找零. 我们再引入一个概念: cost(花销), 每个输出的花销等于 输出中包含的金额 加上 它所产生的 output 的手续费. 首先呢, 假设无找零, 按照 gains - costs - barrier > 0 这个要求, 在钱包中寻找 UTXO. 这里的 barrier 表示基础开销, 即除了 inputs 和 outputs 之外的部分产生的手续费. 找到足够的 UTXO 之后, 再看看 gains - costs - barrier 的值是否大于 (最小可花费面值 加上 1 个 output 的手续费), 如果大于, 那么就加上一个找零的 output; 如果不大于, 说明找零没有意义, 要么找回来了也花不出去, 要么找零的金额还不够支付因它而增加的手续费的.

是时候拿几个真实的交易来试验一下了. 首先看看我们的交易体积计算方式有没有问题.

这笔交易 为例, 这笔交易是一笔非隔离见证交易. 尽管输出中包含隔离见证地址, 但是输入里面没有.

对于这种传统交易, 用的是 1 开头的地址. 交易序列化的结构是:

  4 字节的版本号;
  inputs 个数;
  inputs 内容;
  outputs 个数;
  outputs 内容;
  4 字节的 locktime;

inputs 个数是 2, 转化为 Variable Length Integer 编码, 长度是 1.

inputs 内容是两个 p2pkh 交易的解锁脚本. 每个解锁脚本内容是 OPx [签名] OPy [公钥] 公钥我们知道长度是 32, 签名我们上面说了, 按 73 来算, 加上两个操作符的长度 2, 再加通过 Variable Length String 编码, 得到的长度是 108. 每个 input 里面还包含了 sequence(序列) 和 outpoint(连接到其来源), 长度分别是 4 和 36. 所以一个 p2pkh input 的体积是 108+4+36 = 148.

outputs 个数是16, 转换编码后长度是 1.

outputs 的内容就比较多了, 有 p2pkh, p2wpkh, p2shwpkh 这三种锁定脚本, 脚本编码后长度分别是 26, 23, 24. 每个 output 还要加上表示金额的 4 个字节.

最后根据这种方法计算出来这笔交易的体积是 831, 和实际的相差了 2, 是因为 2 个签名那里我们都取了最长的可能值. 总体是准确的.

这是传统交易, 再来看看隔离见证交易. 对于 inputs 里包含隔离见证地址的交易, 需要使用一种新的序列化结构, 因为是 BIP141 方案提出的, 所以姑且就叫 BIP141 结构, 是这样的:

  4 字节的版本号;
  1 字节的 0;
  1 字节的 1;
  inputs 个数;
  inputs 内容;
  outputs 个数;
  outputs 内容;
  witness;
  4 节的 locktime;

可以看到这里多了一个 witness 结构. 对于 inputs 的类型支持也变成了三种: p2pkh, p2wpkh, p2shwpkh, 解锁脚本的体积分别是 108, 24, 0, 对应的 witness 体积分别是 0, 108, 108.

关于解锁脚本的体积, 如果使用的是未压缩的公钥, 那么还要多 32 字节. 对于隔离见证地址, 只允许使用压缩后的公钥. 但 p2pkh 地址是允许使用未压缩公钥的.

我们找来测试网上的这笔交易. 这笔交易有 3 个 input, 3 个 output, 混合了 3 种地址. 而且 p2pkh 地址使用的公钥是未压缩的. 按照我们上面的方法, 计算得到交易体积是 613, 实际是 610, 说明估算是准确的.

我们一开始提到了, 隔离见证交易的手续费计算机制和传统交易不同, 采用的是 vsize, vsize 等于 base_size 加上 四分之一的 witness_size. 根据官方的建议, 这里使用 round up (近一)来转换小数. 这样, 我们计算出来上面这笔交易的 vsize 是 449, 比实际的多 2.

以上就是根据输入输出的个数和类型来估算交易体积的方法. 别忘了我们最初的目的: 已知一部分输出, 以及钱包里的 UTXO 集合, 构造出一个手续费合理的交易.

过程类似昆特牌, 钱包里的 UTXO 都是我们的手牌, 目标输出就是对手. 就拿这一笔交易 来举例, 这笔交易有 1 个 p2shwpkh 类型的输出. 可以按这样的流程构造交易.

  1. 取出一个 UTXO 添加到输入里, 计算它的增益 gain(U1). 再计算全部输入的总增益减去各项开支(交易体积 x 费率, 输出的金额总和), 我们把这一结果称为盈余(surplus). 如果盈余是非负数, 进入下一步; 如果不是, 重复第一步.

  2. 这时这笔交易已经是合法的了(输入金额大于输出金额加最低手续费), 但我们还要判断是否可以找零. 盈余减去因找零输出而增加的手续费, 得到的就是找零的金额.

  3. 如果上一步得到的找零的金额大于等于最小可接受的找零金额, 那么就添加这个找零到输出里面.

    通过上面的规则, 我们可以这样去验证它的正确性. 先把上述这笔交易的输入作为 UTXO 放到钱包里, 然后这笔输出作为我们的目标输出. 再根据区块浏览器上看到的这笔交易所使用的费率, 计算得出其是否需要有找零, 以及找零的金额, 看看和区块浏览器上面的一不一样.

    大概步骤就是这样了, 区块链方面的编程有一个好, 啥都能在区块浏览器上找到, 自己写的东西有没有错对比一下就知道了.