读比特币脚本,最直接的入手点是把一笔交易的输入脚本和输出脚本拼起来,在同一个栈上从左到右跑一遍。输出脚本(scriptPubKey)是锁定条件,输入脚本(scriptSig)或见证数据(witness)是解锁材料。验证通过的标准很窄:执行结束后栈上恰好剩下一个非零值。栈空了、栈顶是 0、或者剩下不止一个元素,都算失败。
常见的误解是把锁定脚本当成"地址的另一种写法"。地址只是锁定脚本的编码结果,脚本本身才是真正的条件。理解脚本,就是理解"花这笔钱需要满足什么",而不是记住地址前缀。
最标准的样本:P2PKH 怎么一步步跑完
P2PKH 是传统地址(以 1 开头)背后的脚本,也是读脚本最好的起点。它的锁定脚本是:
OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG
解锁脚本需要提供签名和公钥:
矿工验证时,先把解锁脚本压入栈,再执行锁定脚本。过程如下:
- 压入
,栈:[sig] - 压入
,栈:[sig, pubKey] OP_DUP复制栈顶,栈:[sig, pubKey, pubKey]OP_HASH160对栈顶做哈希,栈:[sig, pubKey, hash160(pubKey)]- 压入锁定脚本里的
,栈:[sig, pubKey, hash160(pubKey), pubKeyHash] OP_EQUALVERIFY比较栈顶两个值,相等则消耗掉它们,继续;不等则整个脚本失败。这一步确认"你提供的公钥确实哈希到了锁定脚本指定的那个值"- 此时栈上剩
[sig, pubKey],OP_CHECKSIG弹出这两个值,用公钥验证签名是否有效。有效则压入1,栈:[1]
最后栈上只有一个 1,验证通过。如果签名不对,OP_CHECKSIG 压入 0,脚本失败。
这里的关键判断:OP_EQUALVERIFY 拦住的是"公钥不匹配",OP_CHECKSIG 拦住的是"签名无效"。两个检查各管一段,缺一不可。
P2SH:锁定的是脚本的哈希,不是公钥的哈希
P2SH 解决的是"发送方不想关心收款方用什么复杂脚本"的问题。它的锁定脚本很短:
OP_HASH160 OP_EQUAL
解锁脚本需要提供原始 redeemScript(加上满足该脚本的签名等数据),最后一步把 redeemScript 压栈。执行到 OP_EQUAL 时,验证的是"你提供的 redeemScript 的哈希是否匹配锁定脚本里的 "。匹配之后,节点会再把 redeemScript 当作脚本执行一遍,这才是真正的签名检查或条件检查。
读 P2SH 脚本时,不能只看锁定脚本就以为懂了条件。锁定脚本只告诉你"需要某个脚本的哈希",真正的花费条件在 redeemScript 里。如果你看到以 3 开头的地址,对应的就是这种结构。
SegWit 之后:脚本还在,但放在 witness 里
原生 SegWit(P2WPKH,地址以 bc1q 开头)的锁定脚本更短:
OP_0 <20-byte pubKeyHash>
它看起来不像一段"可执行的条件",更像一个版本号加一段数据。实际的签名和公钥放在交易的 witness 字段里,不在 scriptSig 中。验证节点会根据 OP_0 识别出这是版本 0 的见证程序,然后按对应规则去 witness 里取签名和公钥,用它们去匹配 <20-byte pubKeyHash> 并验证签名。
读 SegWit 脚本时,不要只盯着 scriptPubKey 找 OP_CHECKSIG。条件的一部分被移到了 witness 验证路径中。锁定脚本本身只是告诉节点"用哪套规则来检查"。
Taproot(P2TR,地址以 bc1p 开头)走得更远。它的锁定脚本只是一个被调整过的 x-only 公钥:
OP_1 <32-byte tweaked pubkey>
如果走 key-path 花费,签名直接用这个公钥验证,链上看起来和单签一样。如果走 script-path,才会暴露脚本树的一个分支。对普通读者来说,读到 P2TR 输出时,应该知道"条件可能藏在脚本树的某个叶子里",而不是只看锁定脚本本身。
验证失败时,问题出在哪一层
脚本执行失败不是一种原因。按执行顺序排查,能定位到具体环节:
栈状态错误:执行中途栈元素不够弹出(比如 OP_CHECKSIG 需要两个元素,栈上只有一个),脚本立即失败。这通常意味着解锁脚本提供的数据不完整。
OP_EQUALVERIFY 或 OP_EQUAL 失败:公钥哈希不匹配,或者 redeemScript 的哈希不匹配。说明解锁脚本里放错了公钥或脚本。
OP_CHECKSIG 返回 0:公钥和哈希对上了,但签名验证不通过。签名可能不对应这笔交易的当前状态,或者签名本身被篡改。
执行完栈上剩余多个值:有些脚本写错了,或者解锁脚本多压入了数据,导致最后没有清理干净。比特币要求恰好一个非零值,多一个元素也算失败。
一个实际的判断习惯:如果你在区块浏览器里看到一笔花费被标记为无效,先看 scriptSig 和 scriptPubKey 的拼接结果,再看栈最终剩了什么。大多数问题出在"提供的数据不对",而不是脚本本身写错了。
读脚本时值得养成的两个习惯
先认类型,再看细节。看到 OP_DUP OP_HASH160 ... OP_EQUALVERIFY OP_CHECKSIG 就知道是 P2PKH,看到 OP_HASH160 ... OP_EQUAL 且解锁脚本末尾带一段脚本数据就知道是 P2SH,看到 OP_0 <20 bytes> 就知道是 P2WPKH。类型定了,验证路径的大框架就定了。
区分"锁定"和"花费"。锁定脚本在 UTXO 创建时就固定了,花费脚本是你花这笔钱时提供的。读一个地址对应的条件,读的是锁定脚本;判断一笔花费能不能过,要把解锁脚本和锁定脚本拼起来跑。两者不要混在一起看。
如果你手头有一个具体的脚本字符串,可以先按操作码逐个拆开,标出哪些是压栈的数据、哪些是操作符,然后按栈的变化走一遍。走不通的地方,就是验证会失败的地方。



