GitHub
深入技术
P2SH

P2SH

P2SH - Pay to Script Hash, 意为支付到脚本哈希。

这是一种可自定义脚本的标准模板, 自定义脚本被称为赎回脚本(Redeem Script)

脚本哈希

脚本哈希是对自定义脚本(赎回脚本)进行 Hash160 运算的结果, 大小为 20 字节。

假设赎回脚本是 6f93598893578893588851

Hash160计算器

16进制数据
结果

Hash160 结果为 da5a92e670a66538be1c550af352646000b2367d, 该值即为脚本哈希, 被存储在 UTXOScriptPubKey 中。

锁定脚本

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

OP_HASH160 <脚本哈希> OP_EQUAL

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

OP_HASH160da5a92e670a66538be1c550af352646000b2367dOP_EQUAL

其中 da5a92e670a66538be1c550af352646000b2367d 为脚本哈希。

计算脚本哈希是一个不可逆的操作, 因此无法从脚本哈希中计算出赎回脚本。这在一定程度提升了自定义脚本的安全性。

解锁脚本

P2SH 解锁脚本由两部分组成, 前半部分是自定义的解锁脚本, 后半部分是赎回脚本。

OP_3OP_5OP_46f93598893578893588851

6f93598893578893588851 是赎回脚本, 转换成 ASM 格式为:

OP_3DUPOP_ADDOP_9OP_EQUALVERIFYOP_ADDOP_7OP_EQUALVERIFYOP_ADDOP_8OP_EQUALVERIFYOP_1

脚本执行

P2SH 脚本执行分为两部分:

  • 标准脚本执行 - 依次拼接 ScriptSigScriptPubKey 后执行, 主要用来验证脚本哈希的正确性。
  • 赎回脚本执行 - 检查栈顶元素是否为 OP_1, 若是则移除栈顶元素, 并执行赎回脚本。

脚本执行模拟

模拟脚本在栈中的执行过程
原始交易数据
交易输入
解锁脚本
锁定脚本
Stack
  • 赎回脚本可以是任何有效的脚本,包括非标准脚本。只要在执行结束时栈上有且只有一个 OP_1, 则解锁脚本有效。

  • 赎回脚本最大为 520 个字节。

P2SH-P2MS

P2SH 最常见的应用场景是多签, 即将 P2MS 作为赎回脚本。

  • P2MS 输出的锁定脚本包含多个原始公钥信息, 无疑会增加交易的大小, 从而增加交易费用。而使用 P2SH-P2MS 可以用多个公钥信息构建赎回脚本, 交易输出中只需存储赎回脚本哈希, 从而减小交易大小, 降低交易费用。

  • P2MS 最多支持 3 个公钥, 而由于单个压缩公钥大小为 33 字节字节, P2SH-P2MS 赎回脚本最多支持 15 个公钥。

花费时, P2MS 解锁脚本只需要提供需要的多个签名即可。而 P2SH-P2MS 除了必须的多个签名外, 还需要提供完整的赎回脚本。 因此在花费时, P2SH-P2MS 会比 P2MS 需要更多的交易费用。

实践

构建自定义赎回脚本

import { payments, script } from 'bitcoinjs-lib'
import { OPS } from 'bitcoinjs-lib/src/ops'
import { toHex } from 'uint8array-tools'
 
const redeem = script.compile([
  OPS.OP_3DUP,
  OPS.OP_ADD,
  OPS.OP_9,
  OPS.OP_EQUALVERIFY,
  OPS.OP_ADD,
  OPS.OP_7,
  OPS.OP_EQUALVERIFY,
  OPS.OP_ADD,
  OPS.OP_8,
  OPS.OP_EQUALVERIFY,
  OPS.OP_1
])
 
const p2sh = payments.p2sh({ redeem: { output: redeem } })
 
console.log(toHex(p2sh.output!)) // a914da5a92e670a66538be1c550af352646000b2367d87
console.log(toHex(p2sh.redeem?.output!)) // 6f93598893578893588851

p2sh.output 为锁定脚本, p2sh.redeem?.output 为赎回脚本

以标准脚本作为赎回脚本, 如 P2MS:

const p2sh = payments.p2sh({
  redeem: payments.p2ms({
    m: 2,
    pubkeys: [
      Buffer.from(
        '020727931f73a3557750012196a95c2c711eedea3b981311bdaae0d77830147f5e',
        'hex'
      ),
      Buffer.from(
        '03c843204c50447f6981eb9eb703d74f888dce775cc853ceba2d24b17758ac949c',
        'hex'
      ),
      Buffer.from(
        '03cf0f461d623c3a7c73d3d540ae07e6308d56f1630551be0b7aa7b86632ce4f60',
        'hex'
      )
    ]
  })
})
 
console.log(toHex(p2sh.output!)) // a914f71179c135963f43de2d173dbf8a0faa72def6ba87
console.log(toHex(p2sh.redeem?.output!)) // 5221020727931f73a3557750012196a95c2c711eedea3b981311bdaae0d77830147f5e2103c843204c50447f6981eb9eb703d74f888dce775cc853ceba2d24b17758ac949c2103cf0f461d623c3a7c73d3d540ae07e6308d56f1630551be0b7aa7b86632ce4f6053ae
Copyright © 2024 HeapUp