很多人看到比特币交易,第一反应是看地址和金额。我自己早年也一样,直到后来自己动手拆解交易、做离线签名工具,被“脚本”绕得晕头转向,才彻底想明白:地址只是“锁定规则”的外壳,真正决定一笔钱能不能花、由谁花、满足什么条件才能花的,是交易里那两段不起眼的“比特币脚本”。
比特币脚本是一套运行在虚拟栈上的验证规则集。它没有循环,没有图灵完备的能力,操作码也有限,但它用非常克制的设计,保证了所有节点对“这笔交易是否合法”能得出完全一致的结论。这篇文章适合刚接触 Bitcoin 底层、想搞懂 UTXO 验证过程、或者正在写钱包/交易工具但经常被脚本字段卡住的开发者。读懂脚本之后,再回头看区块浏览器里的 ScriptSig、ScriptPubKey、Witness,你会觉得它们一下子变得非常直白。
1. 重新认识比特币脚本
1.1 脚本不是在“跑程序”,是在“核验凭证”
比特币采用的是 UTXO 模型。每一笔交易的输出会附带一段锁定脚本,记录“未来谁能花这笔钱”。将来有人要花这个输出时,必须再提交一段解锁数据。矿工和全节点把解锁数据与锁定脚本放到一起执行,整个执行过程就是一次核验:栈顶上最终留下一个有效值,这笔交易才算通过;如果中途任何一步失败,整笔交易直接无效。
所以别把脚本当成普通编程语言。它更像一个“自动门禁”:你不需要写复杂的业务逻辑,只需要把证据交给门禁,门禁会告诉你放行还是拒绝。验证的结果只有成功或失败,没有中间状态。这种设计不是能力不足,而是刻意为之:脚本越简单,节点执行越确定,攻击面越小,整个网络越容易达成共识。
一段脚本通常会包含两种成分。一种是“数据”,比如公钥、签名、哈希、时间锁数值;另一种是“操作码”,比如复制栈顶元素、计算哈希、比较数据、验签等。数据负责描述条件和证据,操作码负责执行验证。两者拼在一起,就构成了一套可独立运行的验证程序。
1.2 堆栈模型与脚本的运行方式
脚本的载体是一个栈,所有操作都围绕栈顶数据展开。可以把栈想象成一叠扑克牌:往栈顶放一张牌就是“压入数据”,从顶上拿掉一张就是“弹出数据”;有些操作码会复制牌面,有些会修改最上面的牌,有些会把两张牌合并后放回。
最常见的执行顺序是这样的:先把一系列数据依次压入栈,然后执行操作码。操作码从栈顶取所需数量的元素,经过计算后再把结果压回栈。举例来说,OP_HASH160会取走栈顶元素,计算它的 SHA256 再算一次 RIPEMD160,然后把哈希值压回栈顶。OP_EQUALVERIFY会弹出最上面两个元素,比较它们是否相等,如果不相等,脚本立刻失败。
在早期比特币实现里,验证器会把解锁脚本直接拼接在锁定脚本后面执行。SegWit 之后,解锁数据被挪到了 Witness 字段里,实际执行逻辑做了不少调整,但核心心智模型没有变:解锁数据 + 锁定规则 = 通过或不通过。你只要能看懂栈的变化,就能看懂整个验证过程。
2. 先认识这些操作码,脚本才不会像天书
2.1 高频操作码速查
脚本在区块浏览器里通常以操作码形式展示,比如OP_DUP、OP_HASH160、OP_EQUALVERIFY、OP_CHECKSIG。刚接触的人看到这些名字会头皮发麻,其实它们都是很机械的动作,背下几个高频的就行。
| 操作码 | 作用 | 典型使用场景 |
|---|---|---|
| OP_DUP | 复制栈顶元素 | P2PKH 锁定时复制公钥,便于后续算哈希和验签 |
| OP_HASH160 | 对栈顶元素计算 SHA256 后再算 RIPEMD160 | 把公钥转成公钥哈希,和地址关联 |
| OP_EQUALVERIFY | 比较栈顶两个元素,相等则继续,不等则失败 | 验证解锁公钥的哈希是否匹配锁定脚本里的目标哈希 |
| OP_CHECKSIG | 用栈顶公钥验证签名,结果入栈 | P2PKH、P2WPKH 等最常见的单签验证 |
| OP_CHECKMULTISIG | 验证 m-of-n 多重签名 | 多签钱包、托管场景 |
| OP_IF / OP_ELSE / OP_ENDIF | 条件分支控制 | 合约类脚本、时间锁组合脚本 |
| OP_CHECKLOCKTIMEVERIFY | 检查交易的 nLockTime 是否满足脚本要求 | 锁定资金直到某个区块高度或时间 |
| OP_CHECKSEQUENCEVERIFY | 检查输入 nSequence 是否满足脚本要求 | 相对时间锁、通道类应用 |
| OP_RETURN | 标记输出为不可花费 | 在链上存任意数据,常见于备注、存证 |
OP_DUP的好理解之处在于“复制”。OP_HASH160则是让公钥变成更短、更好用的哈希值,这个哈希值就是地址的核心。OP_CHECKSIG是最后的大招,它从栈顶取出公钥和签名,验证签名是否对当前交易有效,然后把一个布尔值压回栈。
另外还有个容易混淆的点:OP_EQUALVERIFY和OP_EQUAL不同。OP_EQUALVERIFY比较完会把两个元素都弹出,如果不等则直接失败;OP_EQUAL则只把比较结果留下,比如留下true或false。很多自定义脚本里会用OP_EQUAL配合后续逻辑,但标准支付脚本里基本见不到裸用OP_EQUAL的情况。
2.2 常见脚本类型对照
理解了操作码,再看实际交易里的脚本类型就轻松多了。目前最常见的就是下面这几种。
| 类型 | 锁定脚本特征 | 解锁方式 |
|---|---|---|
| P2PKH | OP_DUP OP_HASH160 <20字节公钥哈希> OP_EQUALVERIFY OP_CHECKSIG | 提交签名 + 公钥 |
| P2SH | OP_HASH160 <20字节脚本哈希> OP_EQUAL | 提交能匹配哈希的 RedeemScript,再按脚本要求解锁 |
| P2WPKH | OP_0 <20字节公钥哈希> | Witness 里提交签名 + 公钥 |
| P2WSH | OP_0 <32字节脚本哈希> | Witness 里提交 WitnessScript,再按脚本要求解锁 |
| P2TR | OP_1 <32字节公钥> | 可用密钥路径直接签名,也可走脚本路径触发 Tapscript |
从地址上也能直观区分:传统 P2PKH 地址以1开头,P2SH 地址以3开头;隔离见证地址多数以bc1q开头,Taproot 地址以bc1p开头。不过这只是外观,真正要看懂交易,还是得看锁定脚本本身。
P2PKH 是“支付到公钥哈希”,它把地址和脚本关联起来,用户只需要公开哈希而不直接暴露完整公钥,降低了公钥提前暴露带来的风险。P2SH 则允许把一段复杂脚本先哈希后作为解锁条件,发送方只看到哈希,不需要知道里面的复杂逻辑,等实际花费时再把原始脚本交出来。SegWit 系列进一步把解锁数据从 ScriptSig 挪到 Witness 里,既解决了交易延展性问题,也让 Witness 部分的字节不再影响交易 ID 的计算。Taproot 是后起之秀,它引入密钥路径花费和脚本路径花费,让复杂脚本在链上看起来和普通单签几乎没有区别,隐私性更好。
2.3 为什么有些脚本会被网络拒绝
很多人在测试或研究时会写一些自定义脚本,发现广播不出去,第一反应是脚本写错了。其实不一定,也许是“非标准交易”。节点在默认配置下,只转发标准脚本类型。简单说,标准性规则是节点自己加的转发门槛,不是共识规则本身。
标准性规则会检查很多东西。比如 ScriptSig 必须是纯数据,不能在里面塞操作码;公钥或签名必须满足长度限制;多重签名的公钥数量不能超过约定上限;OP_RETURN数据长度要控制在标准范围;数字编码要尽量短等。这些规则的目的不只是“限制自由”,更是防止有人用异常脚本骚扰网络,或让节点陷入不必要的计算开销。
如果你确实要测试非标准脚本,可以考虑自己起一个 regtest 环境,或者使用允许特殊打包的测试工具。只要矿工愿意把交易放进区块,节点还是会按共识规则验证它。但想依赖普通节点内存池转发,往往会被“非标准交易”拒之门外。这一点在实际开发踩坑时非常常见,先查标准性问题,往往能省下大量排查时间。
3. 从P2PKH一笔一笔推演验证过程
3.1 锁定脚本与解锁脚本的组合关系
P2PKH 是最经典、也最值得从底层推演一遍的脚本。它的锁定脚本长这样:
OP_DUP OP_HASH160 <20字节公钥哈希> OP_EQUALVERIFY OP_CHECKSIG当收款方拿到这笔钱后,将来要花掉它时,需要提交一段解锁脚本:
<签名> <公钥>比特币验证时,会把解锁脚本和锁定脚本按顺序拼接起来执行。如果把两段脚本合在一起看,完整的执行序列是:
<签名> <公钥> OP_DUP OP_HASH160 <公钥哈希> OP_EQUALVERIFY OP_CHECKSIG整个过程实际上验证了两件事:第一,提交的公钥经过哈希运算后,是否等于锁定脚本里写的那个公钥哈希;第二,提交的签名是否能用这个公钥验证通过。两个条件同时成立,资金解锁条件才算满足。
3.2 栈上发生了什么:逐步推演
我用栈的变化一步步看这个过程。初始状态下,栈是空的,脚本开始执行后,先把签名和公钥压入栈:
[签名, 公钥]第一个操作码是OP_DUP,它把栈顶公钥复制一份,栈变成:
[签名, 公钥, 公钥]接着是OP_HASH160,它取走栈顶公钥,计算公钥哈希,再把结果压回栈。此时栈里有三样东西:
[签名, 公钥, 公钥哈希]然后是压入锁定脚本里写好的目标公钥哈希,栈变成:
[签名, 公钥, 公钥哈希, 目标公钥哈希]OP_EQUALVERIFY把后两个元素做比较。如果相等,两个哈希都被弹出,验证继续;如果不等,整个脚本直接失败。假设相等,栈回到:
[签名, 公钥]最后执行OP_CHECKSIG。它从栈顶取出公钥,再取出签名,验证签名是否为当前交易的有效签名。如果验签成功,栈顶留下一个true,脚本验证通过。如果验签失败,留下false,交易无效。
这段推演值得自己在纸上画一遍。我当初看的时候觉得琐碎,后来自己手动拆交易才发现,栈的顺序错一个、操作码多一个少一个,结果天差地别。尤其是OP_DUP为什么要先复制一份公钥:因为OP_HASH160会消耗栈顶的公钥,如果不先复制,后面验签就没有公钥可用了。这种细节,看代码远不如推栈来得直观。
3.3 为什么P2PKH可以沿用一个至今
P2PKH 这套结构已经存在了很多年,但它至今仍是理解比特币交易的最佳入口。它的设计有一个很实用的优点:地址只是公钥哈希的一种编码,用户不需要在收款时暴露自己的完整公钥。ECDSA 公钥一旦暴露,理论上会降低一些抗量子计算的安全性,所以能不暴露就不暴露,哈希值刚好充当了“隔离层”。
另一个原因是兼容性。P2PKH 地址格式简单,钱包实现成熟,离线签名、硬件钱包、冷热分离等场景都能很方便地支持。即使后来出现了 SegWit、Taproot,许多钱包仍然把 P2PKH 作为默认地址类型之一,或者用 P2SH 包裹 SegWit 地址来兼容旧钱包。
从历史角度看,P2PKH 的脚本结构也很适合作为学习范本。它不涉及复杂的条件分支,也没有多个公钥的排列组合,但包含了“哈希校验”和“签名校验”这两个最核心的元素。把 P2PKH 吃透,再去看 P2SH、多签和 Taproot,只需要在原有心智模型上叠加新概念。
4. 亲手解码一笔交易的脚本
4.1 交易二进制结构速览
理解脚本不能只停留在抽象层面,最好能拿一段真实的交易十六进制数据,手动拆开看看。一个传统比特币交易的二进制结构大致是这样的:
版本号 4字节 输入个数 可变长度整数 输入列表: 前一笔交易ID 32字节 输出索引 4字节 ScriptSig长度 可变长度整数 ScriptSig数据 若干字节 Sequence 4字节 输出个数 可变长度整数 输出列表: 金额 8字节 ScriptPubKey长度 可变长度整数 ScriptPubKey数据 若干字节 锁定时间 4字节交易中比较容易被新手忽略的是“可变长度整数”这个概念。它用来表示后面的数据长度,比如输入个数、脚本长度等。常见情况下,小于 0xFD 的数值直接用 1 字节表示;更大的数字会使用更复杂的 CompactSize 编码。自己写解析工具时,这一块是最容易踩坑的,因为很多人会默认脚本长度就是一个固定字节。
从 SegWit 开始,交易结构在版本号和输入个数之间加入了 Marker 和 Flag 两个字节,后面每个输入还会附带 Witness 数据。所以看到一段交易十六进制,先判断它是不是带 Witness 的交易,再决定解析方式,否则字段就会错位。
4.2 脚本十六进制怎么“掰开揉碎”
我在实际解析脚本时,习惯先把十六进制脚本按“操作码 + 数据”分段。以最常见的 P2PKH 锁定脚本为例:
76a914 <20字节公钥哈希> 88ac这里面的76就是OP_DUP,a9是OP_HASH160,14是“压入 20 字节数据”的指令。这里的14是十六进制,十进制是 20,所以它后面一定要跟 20 字节的数据。最后88是OP_EQUALVERIFY,ac是OP_CHECKSIG。只要按这个方式拆,一段脚本就能从“天书”变成“清单”。
再看 P2SH 锁定脚本:
a914 <20字节脚本哈希> 87a9依然是OP_HASH160,14表示压入 20 字节,87是OP_EQUAL。P2SH 的特点是锁定脚本里只有脚本哈希,真正执行的那段复杂脚本不在这里,而是在花费时的 RedeemScript 里。
如果看到隔离见证的锁定脚本,比如 P2WPKH:
0014 <20字节公钥哈希>最前面的00表示 Witness 版本 0,后面的14表示压入 20 字节数据。Taproot 则是:
5120 <32字节公钥>这里的51其实是OP_1,也就是 Witness 版本 1,后面的20表示压入 32 字节数据。懂了这个规律之后,你就会发现脚本并没有想象中那么神秘,无非就是操作码、长度前缀和数据三者的排列。
4.3 用比特币核心命令练手
如果你的环境里有比特币核心钱包,最直接的方式是用命令行工具解码交易。常用命令有这几个:
# 获取原始交易数据,第二个参数 true 表示直接返回十六进制 bitcoin-cli getrawtransaction <交易ID> true # 解码一笔十六进制交易 bitcoin-cli decoderawtransaction <交易hex> # 单独解码一段脚本 bitcoin-cli decodescript <脚本hex>需要注意,getrawtransaction默认只能查询本地区块数据,如果想查询任意历史交易,需要节点开启txindex=1,否则往往只能查到内存池里的交易,或者要用-txindex重新同步。没有本地节点的话,也可以在任意区块浏览器里找到交易的 Raw Hex,再复制出来自己切割字段。虽然这个过程繁琐,但练习一次之后,对脚本的理解会非常扎实。
5. 脚本开发里那些常见的坑
5.1 多签总比别人多一个OP_0
写多重签名脚本时,非常容易遇到一个经典问题:明明签名数量、顺序都正确,脚本就是不通过。原因很可能在于OP_CHECKMULTISIG有一个历史遗留的“吃元素”行为。
按照脚本规则,OP_CHECKMULTISIG在验签时会从栈上多弹出一个元素。这个行为最早是个 bug,但后来因为太多脚本已经按这个行为生成,为了兼容,网络约定所有实现都要保留这个“Bug 行为”。所以构造多签解锁脚本时,必须在所有签名前面多加一个无意义元素,通常是OP_0。如果不加这个元素,验签结果就会始终失败。
这也是为什么你在很多多签交易的 ScriptSig 里会看到开头有一个OP_0,然后才是签名1、签名2。不是开发者没事干多写一个,而是缺它不可。初次接触会非常困惑,记住这个“幽灵元素”的规矩就对了。
5.2 P2SH的redeemScript不是scriptPubKey
另一个高频误区是把 P2SH 的锁定脚本和真实解锁脚本搞混。P2SH 的锁定脚本只保存一段脚本的哈希,真正的复杂脚本要在花费时才暴露出来,这段脚本叫 RedeemScript。验证时,节点会先检查解锁脚本里的 RedeemScript 是否和锁定脚本里的哈希一致,再把 RedeemScript 当作新的锁定脚本来执行。
这个设计的一大好处是,收款方只需要提供一个 P2SH 地址,发送方不需要搞懂对方地址背后到底是什么合约逻辑。无论是多签、时间锁还是其他自定义条件,对发送方来说都只是一个哈希。
对于开发者来说,最容易翻车的地方是忘记做“哈希匹配”。你可能已经写好了 RedeemScript,也把签名放进去了,但忘记检查 RedeemScript 的哈希是否和你构造的 P2SH 地址匹配。结果就是脚本执行时发现有两个完全不同的脚本哈希,直接失败。这个问题非常隐蔽,因为错误并不发生在验签环节,而是在最开始的哈希比对环节。
5.3 时间锁操作码的编码方式
时间锁在脚本里也很常见。使用OP_CHECKLOCKTIMEVERIFY时,操作码要求栈顶有一个数字,这个数字要么是区块高度,要么是 Unix 时间戳,具体取决于交易的 nLockTime 类型。如果栈顶数字小于当前交易的 nLockTime,脚本就会失败。换句话说,这个操作码规定的是“到期前不能花”。
关键坑在于数字编码。比特币脚本里的数字是小端序表示的,而且默认要求尽可能短的编码。比如你想锁定到区块高度 500000,需要把 500000 先转成十六进制0x0007A120,再按小端序写成20 A1 07 00。如果直接把大端序的字节放进去,或者补了一堆无意义的前导零,标准性检查就会拒绝交易。很多新手在写时间锁脚本时,第一次都会栽在这个编码上。
OP_CHECKSEQUENCEVERIFY的语义略有不同,它检查的是当前输入的 Sequence 字段。它要求交易版本至少为 2,且被检查输入的 Sequence 不能等于 0xFFFFFFFF。这个操作码很适合做相对时间锁,因为它跟 UTXO 被确认的时间相关,而不是跟整个交易的锁定时间相关。
5.4 标准性规则不是教你做事,是保护网络
还有一个非常容易让新手误解的点:标准性规则不等于共识规则。默认比特币节点只会转发“标准交易”,但矿工可以把非标准交易直接打包进区块。只要区块里的交易通过共识验证,它就是合法历史的一部分。
标准性规则包含很多细节。比如传统脚本里要求 ScriptSig 必须全是压入数据,不允许在解锁脚本里执行操作码;签名和公钥编码必须是标准格式,公钥长度、签名长度都要在合理范围内;OP_RETURN的数据长度也有限制。这些规则的核心诉求是降低节点被垃圾数据或畸形计算拖垮的风险。
所以开发者在做自定义脚本时,不要默认“只要在测试网能广播,主网就能广播”。测试网的节点通常也会启用标准性检查,问题只是主网环境对标准性的要求更严格。比较好的做法是先在 regtest 环境完全放开手脚测试逻辑,再用标准交易模板往测试网上发,避免把精力浪费在非标准脚本的广播问题上。
6. 如果只准备做钱包开发,还该再深挖什么
6.1 签名哈希与脚本的关系
脚本里最核心的验签操作OP_CHECKSIG,它验证的签名不是凭空对一句话签名,而是对一个“交易摘要”签名。这个摘要是用 SIGHASH 标志决定的。最常见的SIGHASH_ALL会覆盖所有输入和输出,签名一旦生成,任何输出金额或收款地址的改动都会让验签失败。这也是交易保护自己不被篡改的重要手段。
其他几个 SIGHASH 类型则需要谨慎使用。SIGHASH_NONE表示签名者不承诺输出,其他人可以修改输出;SIGHASH_SINGLE表示签名者只承诺与当前输入索引对应的那个输出;SIGHASH_ANYONECANPAY可以和其他类型组合,表示签名者只承诺当前输入,别人可以往交易里追加输入。这些类型在闪电网络、CoinJoin 等场景中非常重要,但普通支付里不要乱用,否则容易让对方拿到修改空间。
在实际拼装签名时,签名后面需要附带一个 SIGHASH 类型字节,一般就是01。如果你在拼接签名时漏掉这个字节,或者用错了类型,验签几乎必然失败。最典型的表现是:单独验证签名时是好的,放进脚本就失败。我排查过几次这个问题,最后都出在 SIGHASH 类型字节上。
6.2 离线钱包脚本拼接的检查清单
如果你正在写离线交易工具,我建议在拼脚本之前先过一遍下面的检查项。这些都是我实际踩过坑之后总结出来的。
首先,明确锁定脚本类型。P2PKH、P2SH、P2WPKH 的处理方式完全不同,不能混用。其次,确认 ScriptSig 或 Witness 里的数据顺序,签名、公钥、RedeemScript 的顺序不能颠倒。再次,检查所有长度前缀是否正确。脚本里每段数据都有对应长度,长度算错一个字节,整个脚本就会错位,而且很难肉眼发现。
还要记得检查签名编码。现在大部分节点都要求使用 DER 编码签名,并且要求签名的 S 值为低值,也就是所谓的 Low-S 要求。如果你用一个高 S 值的签名,同一把私钥签出来的签名可能被节点标记为非标准交易。此外,交易版本要认准,用了OP_CHECKSEQUENCEVERIFY就必须让交易版本不低于 2。
最后,如果是多签,确认开头是否有OP_0;如果是 P2SH,确认 RedeemScript 哈希是否匹配;如果是 SegWit,确认 witness 是否放对了字段。脚本这种东西,表面上看起来只是一串十六进制,但栈上差一个元素、编码差一个字节,都会让整笔交易翻车。我的习惯是:不管代码写得再顺手,也一定先把脚本反汇编成操作码,在纸上手工推一遍栈,再用测试网交易验证一遍。用测试网多试几次,比在主网上交完手续费之后才发现问题要划算得多。