GitHub
深入技术
P2TR

P2TR

P2TR: Pay To Taproot, 意为支付到 Taproot。由于是隔离见证的第 1 个版本, 所以也叫 V1_P2TR

锁定脚本

P2TR 锁定脚本的操作码模板:

OP_1 <Taproot 输出公钥>

以下是一个 P2TR 锁定脚本的示例:

OP_13cb650a3809269365cfea9874982c00b2f61951203a6f3b3686848f1e1b08d46

OP_1 是隔离见证的版本字节, 表示该锁定脚本是 P2TR

Taproot 输出公钥 是一个 32 字节的公钥, 由内部公钥和脚本树的默克尔根计算得到。

  • 内部公钥是用户私钥经过 secp256k1 曲线生成, 实际上就是用户公钥。但在计算 Taproot 输出公钥时, 只会使用公钥的 x 坐标。

  • Taproot 允许多个锁定脚本, 任意一个锁定脚本的解锁即可花费 UTXO。 在有多种锁定脚本的情况下, 将多个锁定脚本通过 MAST 构造成一个脚本树, 脚本树的默克尔根参与计算 Taproot 输出公钥。在花费时, 可以选择其中一个锁定脚本并提供解锁脚本, 同时还需要提供默克尔证明。假设使用上图中 Script_A 来花费 UTXO , 除了解锁脚本外, 还需要其默克尔证明 BC

多种锁定脚本可以是完全自定义的脚本, 也可以是标准脚本, 例如:

// Alice 可花费
script_A = 'OP_1 <Alice_pubkey> OP_CHECKSIG'
// Bob 可以在 30 天后花费
script_B = '<30 days> OP_CHECKLOCKTIMEVERIFY OP_DROP <Bob_pubkey> OP_CHECKSIG'
// Alice, Bob, Carol 任意 2 人可以花费 (P2MS)
script_C =
  'OP_2 <Alice_pubkey> <Bob_pubkey> <Carol_pubkey> OP_3 OP_CHECKMULTISIG'

锁定脚本经过 taggedHash 运算后, 成为脚本树的子节点。将所有子节点构造成一个默克尔树, 得到默克尔根。内部公钥和默克尔根拼接后经过 taggedHash 运算计算出 tweak 值, 简称 t

t=TaggedHash(TapTweak,P+Root)\mathtt {t = TaggedHash('TapTweak', P + Root)}

内部公钥加上 t 乘以椭圆曲线 secp256k1 的基点 G 即可得到 Taproot 输出公钥 Q

Q=P+t×G\mathtt {Q = P + t \times G}

Q 是椭圆曲线的一个点, 但在实际中, 只会用到其 x 坐标, 因此 Q 实际指的是点 Qx 坐标。

Q 进行 bech32m 编码即可得到 Taproot 地址。 当向 Taproot 地址转账时, 对其执行 bech32m 解码可得到地址的 Taproot 输出公钥, Taproot 输出公钥最终会被存入 UTXO 锁定脚本中。

脚本树可以为空。 在比特币钱包中, 默认生成的 Taproot 地址都是在没有脚本树的情况下:

Taproot 地址生成器

从公钥生成 P2TR 地址
私钥
压缩公钥
Step 1: X坐标
Step 2: 计算 Taproot 公钥
Step 3: 字节分组
Step 4: 添加版本前缀
Step 5: Bech32m 编码

解锁脚本

要解锁 P2TR, 有两种方式:

  • 秘钥路径 - Key Path
  • 脚本路径 - Script Path

秘钥路径

只有持有内部公钥对应的私钥才能使用秘钥路径解锁 P2TR, 解锁脚本只有一个签名数据。

以下是一个秘钥路径解锁 P2TR 的交易示例:

02000000000101c1d468b687c165337bcf3ca07194b142f09d4f74e244044a6a8fee9d5539f0ab0200000000ffffffff025798000000000000160014f90371a81f05e7f13091c523ac4ab2f438f9e30272003000000000002251203cb650a3809269365cfea9874982c00b2f61951203a6f3b3686848f1e1b08d460140ad06bd890367e518991fa7337b834032d824586b61d95cf84c0a53ee2c58cb37b53e324c8f85609da8f90da48f35af10121420f112dfc91ea6ed5fccfa0c5f8a00000000

该交易只有一个输入, 对应的 Witness 字段为

[
  {
    "stackItems": "01",
    "0": {
      "size": "40",
      "item": "ad06bd890367e518991fa7337b834032d824586b61d95cf84c0a53ee2c58cb37b53e324c8f85609da8f90da48f35af10121420f112dfc91ea6ed5fccfa0c5f8a" // 签名
    }
  }
]

包含的元素只有一个, 即 Schnorr 签名数据。

需要注意的是, 秘钥路径花费时, 不需要执行脚本。只需要验证签名和锁定脚本中的 Taproot 公钥是否匹配即可证明交易的有效性。

import { Transaction } from 'bitcoinjs-lib'
import { verifySchnorr } from 'tiny-secp256k1'
 
const verifyTaprootSignature = () => {
  // txid: ffda7de7dd1466343e1da5566fdb7db32476d0b0f2e1be7d5dc783bf555d2e00
  const tx = Transaction.fromHex(
    '02000000000101c1d468b687c165337bcf3ca07194b142f09d4f74e244044a6a8fee9d5539f0ab0200000000ffffffff025798000000000000160014f90371a81f05e7f13091c523ac4ab2f438f9e30272003000000000002251203cb650a3809269365cfea9874982c00b2f61951203a6f3b3686848f1e1b08d460140ad06bd890367e518991fa7337b834032d824586b61d95cf84c0a53ee2c58cb37b53e324c8f85609da8f90da48f35af10121420f112dfc91ea6ed5fccfa0c5f8a00000000'
  )
 
  // 要花费的 utxo
  const utxo = {
    // 锁定脚本
    script: Buffer.from(
      '51203cb650a3809269365cfea9874982c00b2f61951203a6f3b3686848f1e1b08d46',
      'hex'
    ),
    // 金额
    amount: 3185699
  }
 
  // 从锁定脚本中提取 Taproot 公钥
  // 3cb650a3809269365cfea9874982c00b2f61951203a6f3b3686848f1e1b08d46
  const taprootPubkey = utxo.script.subarray(2)
 
  // 计算签名哈希
  // 422a8fcae1eacd3a5bdaeaac83c15b8ec4ec5236f8adb32d6756b816730dd85e
  const sigHash = tx.hashForWitnessV1(
    0,
    [utxo.script],
    [utxo.amount],
    Transaction.SIGHASH_DEFAULT
  )
 
  // 提取签名
  // ad06bd890367e518991fa7337b834032d824586b61d95cf84c0a53ee2c58cb37b53e324c8f85609da8f90da48f35af10121420f112dfc91ea6ed5fccfa0c5f8a
  const signature = tx.ins[0].witness[0]
 
  // 验证签名
  const isValid = verifySchnorr(sigHash, taprootPubkey, signature)
 
  console.log(`isValid: ${isValid}`) // isValid: true
}
 
verifyTaprootSignature()

脚本路径

另一种花费方式是通过脚本路径。假设脚本树中有三个锁定脚本

import { script } from 'bitcoinjs-lib'
import { OPS } from 'bitcoinjs-lib/src/ops'
 
// Alice 可以立即花费
const ScriptA = script.compile([
  OPS.OP_1,
  Buffer.from(AlicePublicKey, 'hex'),
  OPS.OP_CHECKSIG
])
 
// Bob 可以在 30 天后花费
const ScriptB = script.compile([
  script.number.encode(30),
  OPS.OP_OP_CHECKLOCKTIMEVERIFY,
  OPS.OP_DROP,
  Buffer.from(BobPublicKey, 'hex'),
  OPS.OP_CHECKSIG
])
 
// Alice, Bob, Carol 任意 2 人可以花费
const ScriptC = script.compile([
  OPS.OP_2,
  Buffer.from(AlicePublicKey, 'hex'),
  Buffer.from(BobPublicKey, 'hex'),
  Buffer.from(CarolPublicKey, 'hex'),
  OPS.OP_3,
  OPS.OP_CHECKMULTISIG
])

可选择的花费方式有:

  • 拥有内部私钥可直接选择秘钥路径花费
  • 没有内部私钥时, 只能选择脚本路径花费方式, 从脚本树中选择任意一个可满足条件的锁定脚本
    • 选择 ScriptA, 需提供默克尔证明 BC
    • 选择 ScriptB, 需提供默克尔证明 AC
    • 选择 ScriptC, 需提供默克尔证明 AB

解锁 P2TRWitness 字段格式为:

<解锁脚本> <锁定脚本> <控制块>

控制块

控制块是内部公钥和默克尔证明的组合。

假设现在选择 ScriptA 花费, 需要使用 Alice 的私钥签名交易得到签名数据, 以及 ScriptA 本身和控制块, 最后将这三个元素组合成 Witness 字段:

<Alice签名> <ScriptA> <控制块>

Copyright © 2024 HeapUp