把比特币当成"数字黄金"的人,往往忽略了一个事实:真正在链上完成转账的不是什么加密信封,而是一小段运行在堆栈上的程序。我最近翻公开课笔记,第九讲正好落在 Bitcoin Script(比特币脚本)上,听完最大的感受是——不理解脚本系统,就没法真正理解比特币"可编程货币"到底可编程在哪,也看不懂后来各路智能合约平台的设计取舍。
这篇笔记不是照抄 PPT,而是我把课程内容消化之后重新梳理的版本。适合这么几类人看:刚读完白皮书但被交易结构卡住的入门者;想搞明白 P2PKH 和 P2SH 区别的开发者;以及研究合约平台时想追根溯源的人。我会从"转账为什么是代码"讲起,再把标准脚本模板一个个拆开,最后聊清楚"非图灵完备"到底是缺陷还是刻意设下的安全边界。
1. 转账为什么是一段"代码":脚本运行的底层逻辑
1.1 UTXO 不是余额,而是一堆"锁"
很多人对比特币的第一印象是"我有多少个币",但比特币账本里根本没有"余额"这个概念。账本里存的是一堆 UTXO(Unspent Transaction Output,未花费交易输出),每一个 UTXO 都像一张锁定的支票,上面写着两件事:面额多大,以及"谁能花掉它"。
"谁能花掉它"在比特币里不是用简单的账户名表达的,而是用一段程序来表达。这段程序就是输出脚本,也叫 scriptPubKey。当你发起一笔交易,说要花掉某个 UTXO,你要做的不是出示身份证,而是提供一段解锁脚本(scriptSig),证明你满足对方当初设置的条件。验证者把两段脚本拼在一起执行,能跑通,这钱就是你的;跑不通,交易直接无效。
所以比特币的转账模型本质上不是"从 A 账户扣钱打到 B 账户",而是"A 用脚本给钱上了一把锁,B 用另一段脚本证明自己有钥匙"。这个认知直接决定了你后面读所有比特币代码时的思维框架。
1.2 把两段脚本拼在一起执行:以 P2PKH 为例的堆栈全过程
比特币脚本是一种基于栈的脚本语言,指令从左到右执行,数据先压入栈顶,操作符再从栈顶弹出数据进行处理,处理完把结果压回栈顶。这个执行模型有点像小学时用的计算器后缀表达式:你先写数字,再写运算符。
最经典的 P2PKH(Pay to Public Key Hash)脚本是这个样子:
解锁脚本(放在交易的输入里):
text复制<签名> <公钥>
锁定脚本(放在 UTXO 里,也就是收款地址对应的脚本):
text复制OP_DUP OP_HASH160 <20字节公钥哈希> OP_EQUALVERIFY OP_CHECKSIG
节点验证时做的事很简单:把两段脚本首尾拼接成一条完整的指令流:
text复制<签名> <公钥> OP_DUP OP_HASH160 <20字节公钥哈希> OP_EQUALVERIFY OP_CHECKSIG
然后从头到尾按栈式规则执行。我把每一步栈的变化列出来,这样最直观:
| 步骤 | 当前指令 | 栈内容变化 |
|---|---|---|
| 1 | <签名> |
[签名] |
| 2 | <公钥> |
[签名, 公钥] |
| 3 | OP_DUP |
复制栈顶公钥,栈变为 [签名, 公钥, 公钥] |
| 4 | OP_HASH160 |
弹出顶部公钥,计算 RIPEMD160(SHA256(公钥)),压回,栈为 [签名, 公钥, 公钥哈希] |
| 5 | <20字节公钥哈希> |
把脚本里写的哈希常量压栈,栈为 [签名, 公钥, 公钥哈希, 脚本哈希] |
| 6 | OP_EQUALVERIFY |
弹出两个哈希并比较,相等则继续执行,不等则立即验证失败 |
| 7 | OP_CHECKSIG |
弹出公钥和签名,用公钥对签名做 ECDSA 验签,压回布尔值 |
最后,脚本执行器检查栈顶是否为真,且整个栈只剩下这一个元素,验证通过。否则拒绝这笔交易。
这里有个特别容易误解的地方:OP_EQUALVERIFY 和 OP_EQUAL 虽然只差一个后缀,但行为完全不同。OP_EQUAL 比较完会把 true 或 false 压回栈中,比较失败并不立即终止;而 OP_EQUALVERIFY 一旦比较失败,整个脚本立刻判定无效。P2PKH 用 VERIFY 版本,就是为了让"哈希不匹配"这种错误直接在早期暴露,省得后面多做无谓的验签计算。
另外一个关键点:脚本验证不是某一个人做的,而是接收这笔交易的每个全节点独立执行的。这意味着脚本必须是确定性的、可终止的,不然全网络都会遭殃。这一点后面还会展开讲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 五个标准交易模板挨个解剖:从 P2PK 到 P2WPKH
2.1 最早的 P2PK 与后来居上的 P2PKH
比特币最早的转账脚本其实不是 P2PKH,而是 P2PK(Pay to Public Key),锁定脚本简单到令人发指:
text复制<公钥> OP_CHECKSIG
花的时候只要提供一个签名即可。这种写法是最朴素的"锁",验证过程就是"用你给的公钥验你给的签名,验过就放行"。它的缺点是地址会很长,因为要把完整的 65 字节公钥写进脚本里,作为对比特币地址的基础来说并不方便。
P2PKH 的意义在于加了一层哈希:锁定脚本里不暴露公钥,只暴露公钥的哈希。这样做的实际好处有几个。第一,地址短,20 字节哈希比 65 字节公钥短得多,方便人类抄写和传播。第二,抗量子威胁的思路更清晰——在签名被公开之前,攻击者拿不到公钥,也就无法从公钥反推私钥;虽然这个保护在真正花费的那一刻就失效了,但至少 UTXO 在静态存放期多了一层保障。第三,从隐私和习惯上讲,把一个又长又丑的公钥字符串压缩成二维码友好的地址,用户体验好太多了。
P2PKH 还有个不太起眼但影响深远的特性:它把"地址格式"和"脚本模板"绑定在一起了。地址以 1 开头的意味着它背后的脚本是 P2PKH,地址以 3 开头意味着它背后是 P2SH,以 bc1 开头则对应隔离见证地址。所以当人们说"往这个地址打币",本质上是说"构造一个匹配该脚本模板的 UTXO"。
2.2 多签与 P2SH:把复杂条件藏到哈希背后
P2PKH 解决了"一把钥匙开一把锁"的问题,但实际业务中经常需要更复杂的花钱条件,比如"3 个人里至少 2 个人签名才能花"。这种需求可以直接用裸多签脚本写:
text复制OP_2 <公钥A> <公钥B> <公钥C> OP_3 OP_CHECKMULTISIG
意思很直白:提供 2 个签名,且这 2 个签名分别对应对着的 3 把公钥之一,校验通过。
但裸多签有个致命问题:脚本反正要写到 UTXO 里,多签参与方的公钥全都要暴露。一个 2-of-3 的裸多签脚本动辄两三百字节,转账费居高不下。P2SH(Pay to Script Hash)就是为解决这个问题出现的:锁定脚本里不再写完整的多签条件,只写一个赎回脚本的哈希:
text复制OP_HASH160 <20字节脚本哈希> OP_EQUAL
真正的赎回脚本(redeemScript)由花钱方在解锁时自己提供。验证流程分两步走:第一步先检验"你给的赎回脚本哈希跟我锁定的哈希是否一致";第二步,把这个赎回脚本切换成一个新的执行上下文,把调用者此前压入的签名、公钥等数据当作栈的初始状态,继续执行赎回脚本里的逻辑。
P2SH 的设计精妙之处在于,你可以把任意复杂的脚本藏在一个 20 字节哈希后面,全节点在验证前不需要知道完整脚本内容,只有真正花钱的人才需要把完整脚本亮出来。这就为各种创新脚本打开了后门——不需要通过共识升级,只要构造一个符合 P2SH 的协议,社区接受,就能在比特币上玩出新花样。
实际上 P2SH 有个配套的强约束:标准节点不接收非标准形式的裸脚本输出,但任意脚本的哈希锁进 P2SH 是允许的。所以后来大量应用,包括多重签名钱包和各类二层协议,都选择 P2SH 作为载体。
2.3 隔离见证下的 P2WPKH 与 P2WSH
P2WPKH(Pay to Witness Public Key Hash)是隔离见证引入的版本,它的锁定脚本极度简化:
text复制OP_0 <20字节公钥哈希>
这里 OP_0 表示版本号 0,后面的 20 字节就是公钥哈希。签名和公钥不再放进解锁脚本,而是挪到了见证区(witness)。这一段见证数据不参与计算交易 ID,只参与验证。
为什么要这么设计?最直接的原因是交易延展性。在旧版 P2PKH 里,签名放在 scriptSig 中,而 scriptSig 经过序列化后参与交易 ID 的哈希计算。ECDSA 签名本身存在多种合法 DER 编码方式,攻击者可以在不改动签名有效性的情况下改动签名在 scriptSig 里的编码,从而改变整笔交易的 txid。这给二层协议造成了极大困扰——通道里明明广播了一笔承诺交易,别人却能通过改签名字节伪造出一笔不同 txid 的等价交易,让通道状态变得混乱。
隔离见证把签名挪出交易 ID 的计算范围后,这个问题被釜底抽薪。除此之外,见证数据本身不参与 txid 计算,也让交易体积的计算方式发生了变化:见证部分有独立的计费折扣(按更小权重计算),因此 P2WPKH 的实际链上手续费往往比同等 P2PKH 低。
P2WSH 则是 P2SH 的见证版本,锁定时存的是 32 字节的 SHA256 哈希(注意,比 P2SH 的 20 字节 RIPEMD160 哈希长,安全性更高),解锁时在见证区提供 witnessScript。整体思路和 P2SH 一脉相承,但借隔离见证的东风解决了延展性和费用问题。到现在,新钱包默认地址基本都以 bc1 开头,背后就是 P2WPKH 或 P2WSH 模板。
我整理了一张速查表,方便对照:
| 模板 | 锁定脚本 | 解锁内容 | 地址形式 |
|---|---|---|---|
| P2PK | <公钥> OP_CHECKSIG |
签名 | 早期公钥形式 |
| P2PKH | OP_DUP OP_HASH160 <哈希> OP_EQUALVERIFY OP_CHECKSIG |
签名 + 公钥 | 1... |
| 裸多签 | OP_m <公钥...> OP_n OP_CHECKMULTISIG |
m 个签名 + 空元素 | 无法直接用地址 |
| P2SH | OP_HASH160 <脚本哈希> OP_EQUAL |
解锁数据 + 赎回脚本 | 3... |
| P2WPKH | OP_0 <公钥哈希> |
见证区存签名 + 公钥 | bc1q... |
| P2WSH | OP_0 <SHA256哈希> |
见证区存解锁数据 + 见证脚本 | bc1q... |
3. 非图灵完备的真相:为什么脚本是对的,循环是错的
3.1 脚本给"可编程"划定的边界
公开课里反复强调一个词:non-Turing complete,非图灵完备。很多人一听就皱眉头,觉得"不能做循环的程序语言都是残废"。但放在区块链语境下,这个结论需要反过来读。
比特币脚本缺少什么?最典型的是循环。脚本里有条件分支(OP_IF / OP_ELSE / OP_ENDIF),但没有 JUMP 之类的向后跳转指令,因此不存在"执行多少步取决于运行时数据"的可能。理论上,一段脚本能执行的指令总数在它被构造出来那一刻就有明确上限。
脚本执行器还会额外设一道防线:非标准操作码默认禁用,部分操作码即使存在也从标准交易的白名单里被剔除。这意味着矿工和节点只会接受一小撮被社区广泛认可的脚本操作,其他花活一律不认。普通用户不用担心哪天收到一个输出脚本是"递归程序"的 UTXO,因为它根本进不了标准区块。
这种设计带来的直接好处是"可预测"。每一笔交易花多少 CPU、验证多少步,大家心里都有数。节点之间同步区块时,不会因为某个恶意脚本跑出天文数字的运算量而卡死。对于一个每天处理海量交易的分布式系统来说,这比"语言表达能力强"重要得多。
3.2 如果加了循环会带来什么灾难
不妨做个思想实验:假设脚本支持 OP_BEGINLOOP OP_ENDLOOP 这样的循环指令,会出现什么?
一笔交易被打包进区块之后,全网络所有全节点都要验证它。攻击者构造一个"循环 10 亿次 SHA256"的脚本,然后支付极低的手续费。单看这一笔交易,没有节点会拒绝执行,可它会让全网每个验证节点在这个 UTXO 上消耗数分钟甚至更久的 CPU。攻击者不需要挖矿算力,只需要反复广播这种恶意交易,就能把全网的区块验证速度拖垮,形成事实上的拒绝服务攻击。
以太坊之所以需要引入 gas 机制,本质上就是为了缓解这个问题:用每步操作的 gas 费限制执行成本。但"限制成本"不等于"没有成本",gas 计算的复杂度本身又会引入新的攻击面,经典的 DoS 漏洞很多都出在"某个操作的实际计算成本被低估了"。比特币选择的是一个更粗暴也更可靠的路线:语言层面就不给你写出循环的机会。这样就不需要复杂的成本计量模型,执行边界天然清晰。
从这个角度回头看,所谓的"图灵完备"在区块链上从来不是免费的午餐。它意味着更强的表达能力,也意味着更复杂的资源计量和更微妙的攻击面。比特币脚本宁愿保留表达能力上的残缺,也要换取验证的确定性和安全性,这是非常清醒的工程取舍。
3.3 脚本史上最典型的坑:CHECKMULTISIG 的多余弹栈与交易延展性
讲脚本的历史包袱,有两个例子非常有意思。
第一个是 OP_CHECKMULTISIG 的"多弹一个元素"问题。这个操作码在执行时会从栈顶依次弹出 n + 1 个数据:先是 n 个签名,再是一个"哑元素",之后才是公钥数量等参数。本质上这是一个 off-by-one 的编程失误,但在共识历史上已经无法修改(改掉会硬分叉,所有旧节点会拒绝新节点认为合法的区块)。所以现在的使用规范是:多签交易在解锁脚本开头必须手动压入一个空元素,把这个 bug 填平。任何从零开始写多签脚本的人,几乎都会在这里栽一次。
第二个是交易延展性,这个我在前面隔离见证部分提过。旧版 P2PKH 的签名位于 scriptSig,而 scriptSig 又是 txid 计算的一部分;ECDSA 签名的 DER 编码允许填充冗余字节,导致同一个签名可以被改写成另一份同样合法但字节完全不同的数据。于是攻击者可以截获一笔广播中的交易,改写签名字节后重新广播,产生一笔"看起来相同但实际上 txid 不同"的交易。对于普通转账这不是大问题,但对二层通道协议就是灾难,因为通道状态切换依赖具体 txid 的确认。后来的隔离见证把见证数据移出 txid 计算范围,根除了这类问题。
这些坑告诉我们一个道理:共识代码里任何一个小细节都背负着历史包袱,改一处就要牵扯全网。设计脚本系统时,对各种边界情况多想一步,能省掉后来者无数麻烦。
4. 让交易变得有条件的钥匙:时间锁与 OP_RETURN 的另类用途
4.1 CLTV 与 CSV:让一笔钱"到点才能花"
脚本能做的不只是验签,还能给资金加上时间维度。两个最常见的操作码是 OP_CHECKLOCKTIMEVERIFY(简称 CLTV)和 OP_CHECKSEQUENCEVERIFY(简称 CSV)。
CLTV 的语义是:从栈顶弹出一个时间值,检查它是否小于等于当前交易的 nLockTime,如果不满足则脚本执行失败。通俗地说,它给某一笔 UTXO 加了一个"最早可花费时间"。以 CLTV 为例,一个典型的 HTLC(哈希时间锁合约)输出脚本长这样:
text复制OP_HASH160 <哈希值> OP_EQUALVERIFY
OP_DUP OP_HASH160 <收款人公钥哈希> OP_EQUALVERIFY OP_CHECKSIG
OP_IF
<收款人公钥> OP_CHECKSIG
OP_ELSE
<锁定时间> OP_CHECKLOCKTIMEVERIFY OP_DROP
<退款人公钥> OP_CHECKSIG
OP_ENDIF
简化理解就是:如果谁能提供哈希原像(比如知道某个秘密),并且签名匹配,就能立即花掉这钱;否则要等时间一到,退款方才能拿回资金。闪电网络的资金托管、跨链原子交换,本质上都是 CLTV 和哈希锁的组合。
CSV 则基于交易输入的 sequence 字段作用,语义类似但作用粒度更细,可以针对"这笔 UTXO 在当前交易里必须等多少个区块或者多长时间才能被花"做约束。闪电网络通道的承诺交易中,撤销旧状态后的惩罚期就常用 CSV 实现。
4.2 OP_RETURN:可证明燃烧、存证与协议载体
OP_RETURN 是一个反直觉的操作码:以它开头的输出脚本,直接被节点判定为"不可花费"。它不是把币销毁,而是构造了一个永远无法被解锁的 UTXO,与之相伴的是一小段数据(早期限制 80 字节,后来根据版本有扩展)。
这个设计最初被当作"燃烧证明"使用,比如把一笔币打到一个谁也不知道私钥的地址,向全网证明自己销毁了多少资产。但它的价值远不止此:因为你可以在 OP_RETURN 里塞任意字节,它成了最早的链上数据存储方案。很多资产协议(比如早期的染色币、后续的各种 Token 协议)都选择把资产发行与转账信息写入 OP_RETURN,借用比特币网络的不可篡改性为外部系统做锚定。
要注意的是,OP_RETURN 的数据不进入 UTXO 集合,节点不会为它维护状态,所以它适合存"指纹"和"凭证",不适合存大段业务数据。真把所有业务数据都堆上链,成本高不说,还违背了链上轻量化的设计初衷。我见过有人把整份合同文本塞进 OP_RETURN,结果却被协议节点裁剪掉一半,这就是没搞清楚它定位的教训。
4.3 这些操作码如何组合出真正的"应用"
单个操作码看起来都很简单,但组合起来就能表达相当复杂的业务逻辑。我的体会是,理解比特币脚本最好的方式,就是把它当成"积木式锁":验签是一块积木,哈希锁是一块积木,时间锁是一块积木,多签是一块积木。你想让资金按照什么规则流动,就把对应的积木按顺序拼成一段脚本。
比如要实现一笔"条件支付":A 和 B 打赌,约定赢家拿到钱。脚本可以写成——如果 B 能在截止时间前提供某个秘密,B 直接拿走;如果超时,A 拿回。这就是一个小型去中心化合约,不需要任何第三方仲裁。这种能力在比特币诞生时就存在,只是一直被"币圈只谈涨跌"的舆论掩盖了。
如果你第一次接触脚本编程,我建议别急着写复杂合约,先从改造 P2PKH 开始:把 OP_DUP OP_HASH160 换成 OP_HASH160,实现一个"知道哈希原像就能取走"的简单哈希锁;再加一个 CLTV,让它变成"到点才能取"。亲手在测试网上跑通之后,很多抽象概念都会落地。
5. 学完脚本之后,再看智能合约平台
5.1 为什么比特币才是最早的"可编程货币"
现在一提智能合约,大多数人想到的是以太坊。但严格说起来,比特币脚本才是最早的链上可编程逻辑——它允许链上表达"资金在什么条件下可以被转移",只是表达能力的边界更窄而已。
这个区别很本质:比特币走的是"无状态验证"路线。每一笔交易要验证什么、需要哪些数据,全部包含在交易自身和它引用的 UTXO 里,节点之间不需要维护一个巨大的全局状态。好处是并行度高、确定性极强、攻击面小;代价是你很难表达"跨多笔交易、依赖历史状态"的复杂应用。
以太坊式的账户模型则引入了一个全局状态机:合约有存储、有余额、有可变的持久化数据,代码可以通过循环来实现复杂逻辑。表达能力上确实碾压比特币脚本,但代价也随之而来——状态越复杂,越容易出漏洞,重入攻击、整数溢出这些经典事故都是全局可变状态带来的。
5.2 从脚本到状态机的设计分岔
更准确地说,比特币脚本和以太坊虚拟机不是同一个层面的东西。比特币脚本关心的是"这笔钱当前能不能花",它不关心过去、不关心未来,只关心当下;而以太坊合约关心的是"整个账本状态如何变迁",它有自己的存储空间,可以记录数据,可以在多个交易之间保持连续性。
这就导致了完全不同的应用形态。比特币脚本适合做"一次性条件支付":托管、多签、时间锁、哈希锁,这些都是把复杂规则压缩进一笔交易里。而以太坊则能做持续的、多阶段的应用:代币发行、借贷协议、去中心化交易所,这些都需要状态在多个区块之间持续演进。
不能说谁优谁劣,只能说取舍不同。比特币脚本保守但稳固,它牺牲了表达能力换取了极强的安全边界;以太坊虚拟机开放但复杂,它用 gas 和存储模型为表达能力兜底。学完比特币脚本再看以太坊的 EVM,你会很自然地问一句:"这个操作如果遇到死循环怎么办?""这个状态变更如果被重放怎么办?"——这些问题意识,正是从脚本系统的设计哲学里长出来的。
5.3 给后来者的学习建议
如果你也被这套系统吸引,想深入实践,我推荐一条路线。
先到比特币核心代码的 src/script/ 目录里,把 interpreter.cpp 翻一遍。它是脚本解释器的核心实现,你会在里面找到 OP_DUP、OP_HASH160、OP_CHECKSIG 这些指令的真实落地代码。读代码最大的收获不是看懂语法,而是看到"共识规则"和"普通业务逻辑"之间的那道分界线。
然后打开 src/policy/policy.cpp,看 IsStandard 函数。它定义了什么脚本是标准脚本、什么脚本只是"技术上允许但正常节点不转发"。你会发现很多花哨的脚本其实就是被这个函数挡在门外的。
最后,一定要在 regtest 模式(本地回归测试网络)下亲手构造几笔交易。建议用命令行工具手动组装:先做一笔 P2PKH,再做一笔 P2SH 多签,再做一笔带 CLTV 的时间锁交易。不要直接用钱包,因为钱包会把所有细节藏起来;手动拼交易,你才能真正理解"解锁脚本""锁定脚本""见证数据"这三者之间的关系。这个过程中你大概率会踩到 CHECKMULTISIG 那个多余空元素的坑——踩过一次之后,这辈子都不会忘了。
