剖析比特币 P2SH 交易

  • Tiny熊
  • 发布于 7小时前
  • 阅读 63

探讨 比特币 P2SH 的设计理念、地址生成机制,以及交易验证

通过 [[剖析比特币交易生命周期.md]],我们深入了解了 P2PKH(Pay-to-PubKey-Hash)交易的完整流程,从地址生成、交易构造到脚本验证的每一个细节。P2PKH 作为比特币最基础的交易类型,虽然简单高效,但只能实现"单一公钥控制"的支付方式。

当我们想要实现多重签名钱包、时间锁或其他复杂的支付条件时,P2PKH 就显得力不从心了。P2SH(Pay-to-Script-Hash)应运而生,它让比特币的支付方式变得更加灵活和强大。

这篇文章将继续探讨 P2SH 的设计理念、地址生成机制,以及交易验证的完整过程。

P2SH 产生的背景

P2PKH 的局限性

在 [[剖析比特币交易生命周期.md]] 中,我们详细分析了 P2PKH 交易。P2PKH 的锁定脚本非常简单:

OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG

这种方式只能实现"单一公钥控制"的支付,但在实际应用中,我们有时需要更复杂的支付条件,例如在 多重签名的场景下,公司账户需要 3 个高管中的 2 个签名才能转账,这个时候锁定脚本会变成:OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG

如果 Alice 想给 Bob 公司的多签钱包转账,Alice 知道 Bob 公司 的 3 个公钥(每个 33 字节),构造这个复杂的锁定脚本(约 100+ 字节)交易体积大,我们知道手续费是按交易的字节大小收取的,这就使得支付给多签的交易手续费很高

另外: 由于锁定脚本直接暴露在区块链上,任何人都能看到这些公钥,在花费前就已公开,隐私性比较差,如果 Bob 想升级多签方案,假设从 2/3 升级到 3/5, 所有付款方都需要更新地址,带来不便。

P2SH 的解决方案

P2SH (Pay to Script Hash)通过一个巧妙的设计解决了上述问题:将复杂的锁定脚本哈希化,付款方只需要支付到这个哈希值

P2SH 核心思想:

传统 P2PKH:
付款方 → 锁定到公钥哈希 → 收款方提供公钥和签名解锁

P2SH:
付款方 → 锁定到脚本哈希 → 收款方提供完整脚本和解锁数据

如果使用 P2SH 发起交易 Alice 只需要知道一个脚本哈希(20 字节),构建的锁定脚本为OP_HASH160 <scriptHash> OP_EQUAL(23 字节), 只有 Bob 在花费时才提供完整的多签脚本。

我们用一个表格 对比一下 P2PKH 与P2SH 的主要区别:

特性 P2PKH P2SH
锁定脚本长度 至少 25 字节,多公钥显著增大 23 字节(固定)
付款方需要知道 公钥哈希 脚本哈希
复杂度由谁负担 付款方 收款方
隐私性 公钥提前暴露 脚本花费时才公开
灵活性 只支持公钥 任意脚本

P2SH 于 2012 年通过 BIP 16 引入比特币,为实现复杂支付条件提供了更好的方案。接下来我们看看 P2SH 地址是如何生成的。

P2SH 地址生成

P2SH 地址的生成过程与 P2PKH 类似,但关键区别在于:P2PKH 对公钥哈希编码,P2SH 对脚本哈希编码

P2SH 地址生成流程

我们以 2-of-3 多签钱包为例,完整流程如下:

步骤 1:构造赎回脚本(Redeem Script)

赎回脚本定义了资金的解锁条件,对于 2-of-3 多签:

redeemScript = OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG

具体字节表示:

0x52                          # OP_2 (需要 2 个签名)
0x21 <pubkey1_33_bytes>            # 第一个公钥
0x21 <pubkey2_33_bytes>            # 第二个公钥  
0x21 <pubkey3_33_bytes>            # 第三个公钥
0x53                          # OP_3 (总共 3 个公钥)
0xae                          # OP_CHECKMULTISIG

步骤 2:计算脚本哈希

对赎回脚本执行 HASH160(SHA256 + RIPEMD160):

scriptHash = RIPEMD160(SHA256(redeemScript))

结果是 20 字节的哈希值,例如:

scriptHash = 89abcdefabbaabbaabbaabbaabbaabbaabbaabba

步骤 3:添加版本前缀

根据网络类型添加版本字节:

网络 版本字节 地址前缀
主网 0x05 3
测试网 0xc4 2
version = 0x05                    # 主网 P2SH
data = version || scriptHash      # 拼接:0x05 + 20 字节

步骤 4:计算校验和

version + scriptHash 执行双重 SHA256,取前 4 字节:

checksum = SHA256(SHA256(version + scriptHash))[:4]

步骤 5:Base58Check 编码

将三部分拼接后进行 Base58 编码:

address = Base58(version + scriptHash + checksum)

最终得到的地址格式:

主网:3xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (以 3 开头)
测试网:2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (以 2 开头)

P2SH 地址多签示例

继续以2-of-3 多签地址为例:

公钥:
pubkey1 = 02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5
pubkey2 = 03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb
pubkey3 = 0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798

赎回脚本:
52 21 02c6047f... 21 03774ae7... 21 0279be66... 53 ae

脚本哈希:
scriptHash = HASH160(redeemScript) = 89abcdef...

P2SH 地址(主网):
3E8ociqZa9mZUSwGdSmAEMAoAxBK3FNDcd

P2SH 更具有灵活性,还可以构建例如时间锁的地址:

赎回脚本(2026年1月1日后可用):
04 69554880           # <timestamp> (1767196800)
b1                    # OP_CHECKLOCKTIMEVERIFY
75                    # OP_DROP
76 a9 14 <pubKeyHash> 88 ac  # 标准 P2PKH 部分

P2SH 地址:
3FxYq8i7Ym5qLqXqKqLqXqKqLqXqKqLqXq

P2SH 交易构造

之前一样,交易包含两部分:作为输入的 UTXO 解锁(构造解锁脚本)和 输出的 UTXO 的锁定(构造锁定脚本)。

先看第 2 部分:

构造输出锁定脚本

了解 P2SH 地址是如何生成的,只需要对地址进行 Base58 解码,然后获取到赎回脚本的 Hash。 解析过程如下:

  // 1. Base58 解码
  const decoded = base58.decode(address);
  // 2. 提取各部分
  const version = decoded[0];           // 第 1 字节
  const scriptHash = decoded.slice(1, 21);  // 第 2-21 字节
  const checksum = decoded.slice(21, 25);   // 第 22-25 字节

  // 3. 验证校验和...

从地址解析出 scriptHash 后,就可以构造锁定脚本了:

scriptPubKey = OP_HASH160 <scriptHash> OP_EQUAL

字节表示:

a9                    # OP_HASH160
14                    # 推送 20 字节
<scriptHash_20_bytes> # 脚本哈希
87                    # OP_EQUAL

总长度固定为 23 字节,无论赎回脚本多复杂。P2SH 甚至比最简单的 P2PKH 还短 2 字节!

假设 Alice 给 Bob 的多签 P2SH 地址支付了 0.01 BTC (下文称这笔交易为 A),这笔支付的交易的输出,是这样的:

{
  "value": 1000000,  // 0.01 BTC
  "scriptPubKey": "a914 abcdef... 87"  // OP_HASH160 <scriptHash> OP_EQUAL
}

这笔 UTXO 被锁定到脚本哈希 abcdef...,只有 Bob 提供正确的赎回脚本才能解锁。

如果 Alice 给 Bob 的多签 P2SH 的交易输入来自于 P2PKH 的 UTXO ,交易的输入的构造就和上篇文章一样,因此这里,以 Bob 要花费这笔 UTXO 介绍如何构造解锁 P2SH 的脚本。

构造输入的解锁脚本

上篇文章一样,解锁脚本scriptSig 包含对完整交易的签名,因此需要先将交易结构构造出来:

构造交易结构

假设 Bob 要给 Tom 支付 0.001 BTC, Bob 先选择交易 A 作为 Input 输入,输入中的 scriptSig 先留空,输出是用 Tom 的地址和找零地址【可选】构造来输出 UTXO 。

将输入和输出按如下方式拼接一起:

P2SH 交易结构

在签名时,scriptSig 会临时替换为引用交易(即交易 A)UTXO 的赎回脚本(redeemScript),再加上 sighash flag 来构建签名原像,原像两次 hash 之后得到 tx_hash,最后私钥对 tx_hash 进行签名。

准备签名

由于 Bob 是多签地址,需要两个私钥 prikey1prikey2 进行签名(2-of-3 中的 2 个):

sig1 = sign(privkey1, tx_hash)
sig2 = sign(privkey2, tx_hash)

拿到全部签名之后,就可以构造 scriptSig 了。

构造 scriptSig

P2SH 的 scriptSig 格式:

scriptSig = <sig1> <sig2> <redeemScript>
redeemScript = OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG

scriptSig 需要完整提供赎回脚本,并且提供的签名顺序必须与赎回脚本中的公钥顺序对应。

最终完整的交易是以下交易格式的序列化:

{
  "version": 2,
  "inputs": [
    {
      "previous_output": {
        "txid": "abc123...",
        "vout": 0
      },
      "scriptSig": "00 <sig1> <sig2> <redeemScript>", // OP_0 开头是为了修复历史 bug
      "sequence": 0xffffffff
    }
  ],
  "outputs": [
    {
      "value": 100000,  // 0.001 BTC  
      "scriptPubKey": " tom 锁定脚本"
    },
    {
      "value": 990000,  // 0.0099 BTC  找零
      "scriptPubKey": " bob 找零 "
    }
  ],
  "locktime": 0
}

矿工执行验证

矿工在收到交易后,从交易中提取 scriptSig 和 scriptPubKey

image.png

scriptSig : <sig1> <sig2> <redeemScript>
redeemScript : <sig1> <sig2> OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG 
scriptPubKey : OP_HASH160 <scriptHash> OP_EQUAL

P2SH 的验证过程比 P2PKH 复杂,会分为两个阶段:先验证脚本哈希匹配,再验证赎回脚本的执行结果。

第一阶段:验证脚本哈希

验证脚本哈希执行 scriptSig 与 scriptPubKey 的拼接脚本:

<sig1> <sig2> <redeemScript> OP_HASH160 <scriptHash> OP_EQUAL

此时 <redeemScript> 被视为一个完整的数据序列。栈执行过程:

步骤 操作 栈状态
1 推送 sig1 [sig1]
2 推送 sig2 [sig1, sig2]
3 推送 redeemScript [sig1, sig2, redeemScript]
4 OP_HASH160 [sig1, sig2, HASH160(redeemScript)]
5 推送 scriptHash [sig1, sig2, HASH160(redeemScript), scriptHash]
6 OP_EQUAL [sig1, sig2, true]

这一步主要是验证:

HASH160(redeemScript) == scriptHash

如果哈希不匹配,验证失败,直接终止执行,如果匹配,进入第二阶段执行。

第二阶段:执行赎回脚本

比特币客户端在执行 P2SH 时,有一个特别的处理,在第一阶段执行完 scriptSig 时【第 3 步】,会保留一个栈的副本,栈中包含 [sig1, sig2, redeemScript]。在第一阶段匹配无误后,会继续基于这个栈的副本执行:

  1. 从栈中取出 redeemScript(在第一阶段被推入)
  2. 反序列化为可执行的脚本
  3. 执行 redeemScript 脚本:
<sig1> <sig2> OP_2 <pubkey1> <pubkey2> <pubkey3> OP_3 OP_CHECKMULTISIG

栈执行过程:

步骤 操作 栈状态
初始 scriptSig 残留 [sig1, sig2]
1 OP_2 [sig1, sig2, 2]
2 推送 pubkey1 [sig1, sig2, 2, pk1]
3 推送 pubkey2 [sig1, sig2, 2, pk1, pk2]
4 推送 pubkey3 [sig1, sig2, 2, pk1, pk2, pk3]
5 OP_3 [sig1, sig2, 2, pk1, pk2, pk3, 3]
6 OP_CHECKMULTISIG [true]

OP_CHECKMULTISIG 详解:

从栈中取出:

  • n = 3(公钥数量)
  • pubkeys = [pk1, pk2, pk3]
  • m = 2(需要的签名数量)
  • signatures = [sig1, sig2]

验证逻辑:

for each signature in signatures:
  for each pubkey in pubkeys:
    if verify(pubkey, signature, tx_hash):
      match_count++
      break

return match_count >= m

如果至少 2 个签名有效,OP_CHECKMULTISIG 推入 1(true)。

最终验证:

  • 第一阶段:脚本哈希匹配 ✓
  • 第二阶段:赎回脚本执行成功 ✓
  • 结果:交易有效

以下是交易执行的动态图:

总结

我们在这篇文章介绍了 P2SH 交易如何构建与验证,可以看出来 P2SH 是比特币协议中一个强大的设计,实现很多的优化,例如:

  • 降低成本:固定 23 字节的锁定脚本,大幅减少付款方的手续费
  • 增强隐私:复杂的赎回条件在花费前不会暴露在区块链上
  • 支持复杂的脚本:支持多重签名、时间锁等各种复杂支付条件

    P2SH 的灵活性,让比特币在保持协议简洁性的同时,获得了强大的扩展能力,也为闪电网络等二层方案 、 SegWit(隔离见证)等实现提供了基础。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。