1. 从一笔转账说起:比特币脚本到底在解决什么问题
我刚接触比特币脚本的时候,心里一直有个疑问:为什么比特币不直接用账户余额这种简单的模型,非要搞一套“脚本”出来?直到我把某高校区块链课程里“BTC-比特币脚本”这一讲反复看了三遍,又亲手拆了几个真实的交易数据,才真正想明白——比特币脚本本质上就是一套运行在区块链上的“自动验货机”,它用堆栈这种极其复古的方式,解决了“谁有权花掉这笔钱”这个最核心的问题。
我不知道你有没有过这种体验:第一次看到比特币交易的输入输出结构时,感觉自己好像懂了,但真要手写一笔原始交易、自己去拼装脚本的时候,就发现处处是坑。这门课的内容对我来说最大的价值,就是它没有停留在“UTXO是什么”这种概念层面,而是把脚本的执行过程掰开揉碎了讲,包括操作码怎么弹栈、压栈,签名校验又是怎么一步步完成的。这篇文章就是基于那份课堂笔记的复盘整理,我会结合自己后来实操解析交易数据的经验,把比特币脚本的完整逻辑链条梳理清楚。
如果你是刚入门区块链技术,或者已经看过一些概念但始终对脚本执行流程模模糊糊的开发者,这篇文章应该能帮你省下不少自己踩坑的时间。我会尽量做到让完全没接触过比特币底层代码的人也能顺着思路走下来。
2. 为什么说比特币脚本是“账本上的自动验货机”
2.1 从UTXO模型理解脚本存在的必要性
比特币没有账户,也没有余额数值,它只有一条不断增长的交易链。我打个比方:比特币的账本就像一排自助储物柜,每个柜子(UTXO)里存放着一定数量的比特币,而每个柜子上都挂了一把带程序的锁。你要打开一个柜子,前提是得满足这把锁里写好的验证条件。
这个“带程序的锁”,就是锁定脚本(ScriptPubKey)。而解锁用的钥匙,就是解锁脚本(ScriptSig)。当一笔交易要花费某个UTXO时,比特币网络会把这两段脚本拼接在一起执行,如果最终结果是“真”,柜子就打开了,柜子里的比特币就可以被转移到新的柜子里去。
从课程笔记里我看到一个特别重要但容易被忽视的论断:脚本系统之所以设计成这种模式,是为了让验证过程完全去信任化。你不需要信任对方是个好人,也不需要第三方来裁定,所有节点都只需要运行同一个脚本引擎,就能独立判断这笔交易是否合法。这句话我觉得值得所有区块链开发者反复咀嚼。
2.2 脚本语言的设计哲学:不图灵完备,反而成了优点
很多刚接触开发者脚本的人会问:为什么比特币脚本不用类似JavaScript这样的通用语言?课程里给出的答案非常直接:比特币脚本刻意设计成非图灵完备的,没有循环、没有复杂的跳转逻辑,每次执行都是在有限步数内结束。
我当时理解这个设计时,想到一个很贴切的类比:通用编程语言像是一把瑞士军刀,什么都能做;而比特币脚本更像一把固定尺寸的内六角扳手,它只做一件事,但这件事做得极其可靠。比特币脚本最大的诉求不是功能丰富,而是任何人都能在任何时间、任何地点,用同样的方式执行同一段脚本,得到一模一样的结果。如果脚本语言支持循环,就可能出现死循环攻击,恶意用户构造一笔永远执行不完的交易,把整个网络拖垮。
非图灵完备带来的直接好处有两个:一是执行终止性可保证,二是执行开销可预测。这意味着节点可以安全地运行任何传入的脚本,不需要担心资源被无限消耗。这个设计选择在几十年前可能被视为“低级”,但放在区块链这个特殊的执行环境下,反而成了最大的安全保障。
2.3 堆栈机模型的直观理解
比特币脚本是一种基于堆栈的编程语言。堆栈大家都熟,就是那种后进先出的数据结构。你往栈顶压入数据,然后从栈顶弹出数据执行操作。
我听这门课的时候,脑子里第一次清晰地浮现出脚本执行画面:两个脚本拼接后,程序计数器从头开始逐个读取操作码,每次遇到数据就压入栈顶,遇到操作码就从栈顶弹出相应数量的元素,处理后把结果再压回栈顶。整个过程就像流水线上的机械臂,精准、机械、毫无感情。
堆栈机的好处是它非常容易实现和验证。每个节点用同样的输入,运行同样的脚本,最后只要比对栈顶元素是否为非零值,就能达成共识。这里我不由得感慨:一个看起来极其简单的模型,恰恰是比特币能够在全球范围内做到规则统一的基础。
3. 操作码拆解:比特币脚本的核心指令集
3.1 常用操作码分类解析
想要真正读懂比特币脚本,必须对操作码有基本认识。课程笔记里虽然没有把所有操作码全部列出来,但对几类最常用的做了重点说明,我自己又查了官方文档做了补充整理,按照功能可以分成以下几类:
数据操作类
- OP_0、OP_1NEGATE、OP_1~OP_16:直接压入对应的数值或空数组。
- OP_PUSHDATA1、OP_PUSHDATA2、OP_PUSHDATA4:后续跟着的数据长度不同,用于压入不同大小的数据块。
- OP_DROP:弹出栈顶元素并丢弃。
- OP_DUP:复制栈顶元素,再次压入。
算术运算类
这类操作码在早期比特币脚本中支持加法、减法甚至乘法,但在后来的版本更新中,为了防止潜在的攻击面,很多被禁用了。现在还常用于标准交易的主要是OP_ADD(加法)、OP_SUB(减法)等。需要注意,这类操作码在处理时通常要求元素按“最小编码”规则解释成整数。
密码学校验类
- OP_HASH160:先计算SHA-256,再计算RIPEMD-160,得到20字节的哈希。
- OP_SHA256:计算一次SHA-256。
- OP_CHECKSIG:校验签名,签名与公钥都从栈中弹出。
- OP_CHECKSIGVERIFY:与OP_CHECKSIG类似,但校验失败则直接使脚本无效。
- OP_CHECKMULTISIG:多重签名校验,支持多个公钥中任意M个签名通过即视为有效。
流程控制类
- OP_IF、OP_ELSE、OP_ENDIF:条件分支。
- OP_VERIFY:如果栈顶为假,则整个脚本失败。
- OP_RETURN:标记交易输出为“可证明不可花费”,常用于携带数据或作废UTXO。
这些操作码组合起来,就是比特币世界里各种交易模式的语法基础。我把常见操作码整理成了一张速查表,方便后续实操时对照使用。
| 操作码 | 作用 | 使用场景 |
|---|---|---|
| OP_DUP | 复制栈顶元素 | P2PKH解锁脚本 |
| OP_HASH160 | 双重哈希得到公钥哈希 | P2PKH、P2SH |
| OP_EQUALVERIFY | 比较栈顶两个元素是否相等,不等则失败 | P2PKH |
| OP_CHECKSIG | 验证签名是否匹配公钥 | 几乎所有交易 |
| OP_CHECKMULTISIG | 多重签名验证 | 多签钱包、托管场景 |
| OP_RETURN | 使输出无法花费 | 数据存证、NFT早期形态 |
3.2 OP_CHECKSIG的执行细节探究
理解OP_CHECKSIG是理解整个比特币脚本的关键。课程笔记提到,这个操作码会从栈中弹出公钥和签名,然后验证签名是否由对应的私钥生成。
但我发现很多人(包括最初的我)会误解:到底验证的是哪段数据的签名?答案是:签名签署的是被花费交易的特定部分,即“签名哈希”(sighash)。这个哈希涵盖了交易输入、输出、版本号、锁定时间等关键字段,确保签名一旦生成,交易内容就不能被任何第三方篡改。
课程里特意强调了一个细节:签名中包含了锁定脚本的副本。在旧版本中,这是为了确保花费者知道UTXO上绑定的是什么条件。但在后来的软分叉升级中,这个逻辑做了调整,以减轻交易延展性问题。这个地方原理比较复杂,我建议读者先记住结论:验证签名的数据范围不是整笔交易的全部字节,而是经过特定序列化规则处理的签名哈希,理解这点对后面看交易广播和签名工具都有帮助。
3.3 脚本拼接的顺序与执行原则
比特币脚本执行时,并不是先单独执行解锁脚本,再单独执行锁定脚本,而是把解锁脚本和锁定脚本拼接起来作为一个整体执行。拼接顺序是:先执行解锁脚本,后执行锁定脚本。
课程笔记里有一个让我印象很深的比喻:解锁脚本相当于“钥匙”,锁定脚本相当于“锁芯”。钥匙先插入锁芯,然后整个验证机制开始转动。从技术实现角度讲,解锁脚本的执行结果会留在堆栈中,为随后的锁定脚本提供输入数据。
这对理解脚本的安全边界至关重要:解锁脚本可以携带任何数据,但最终能否花费UTXO,完全取决于锁定脚本的校验逻辑是否被通过。也就是说,解锁脚本里的“垃圾数据”再多也没关系,只要锁定脚本只关心它需要的那个数,其他数据都会被当作无效信息忽略。
4. 从P2PKH到P2SH:标准交易脚本逐项拆解
4.1 P2PKH:最经典的一锁一钥模式
P2PKH(Pay-to-PubKey-Hash)是比特币最基础的标准交易类型,接收方地址本质上就是公钥的哈希。它的锁定脚本长这样:
OP_DUP OP_HASH160 <20字节公钥哈希> OP_EQUALVERIFY OP_CHECKSIG与它配套的解锁脚本非常简单,就是两个元素:
<签名> <公钥>当我第一次自己拼装这组脚本时,踩了一个很蠢的坑:把公钥和公钥哈希搞混了。解锁脚本需要的是完整的公钥,而锁定脚本里预先写死的是公钥哈希。执行流程是这样的:拼接后的脚本从解锁脚本开始,签名先压栈,公钥压栈,然后遇到OP_DUP,复制公钥,接着OP_HASH160把复制的公钥哈希化,此时栈里是:签名、公钥、公钥哈希。随后OP_EQUALVERIFY将公钥哈希与锁定脚本里预置的哈希比对,一致则继续。最后OP_CHECKSIG验证签名与公钥是否匹配。
这个过程精妙的地方在于:你暴露的是公钥哈希(也就是地址),只有到了真正花费时才暴露公钥。而公钥一旦暴露,私钥的安全就完全依赖椭圆曲线算法的数学难题了。这也是为什么很多人建议比特币地址不要重复使用——重复使用会在链上暴露公钥,虽然目前没有实际攻击手段,但密码学的安全边界总想留得更宽一些。
4.2 P2SH:将复杂条件封装成一枚“定值硬币”
P2SH(Pay-to-Script-Hash)的出现解决了一个大痛点:复杂的锁定脚本会让交易变得很大,而且发送方需要知道完整的接收条件。P2SH的思路是把复杂的脚本先哈希化,得到一个20字节的ScriptHash,锁定脚本只写这个哈希值。
P2SH的锁定脚本很简洁:
OP_HASH160 <20字节脚本哈希> OP_EQUAL而要花费P2SH交易的UTXO时,解锁脚本需要提供两部分:
<签名> <公钥> <赎回脚本>这里的赎回脚本(RedeemScript)就是当初被哈希化的原始脚本。执行的时候,节点先对赎回脚本做哈希,与锁定脚本中的哈希比对,确认一致后,再把赎回脚本当作一段子程序继续执行。换句话说,P2SH的验证过程包含了二次脚本执行:先验证哈希匹配,再执行赎回脚本完成真正的条件校验。
我读课程笔记时最大的收获是理解了P2SH为什么被称作“可编程的地址”。它让多重签名钱包的实现变得异常简单:只需要把多签脚本做成RedeemScript,再把它的哈希给接收方就行。所有复杂的条件都被藏在了一个简短的哈希后面,链上数据量也大幅减少。
4.3 P2WPKH与隔离见证:脚本模型的一次重要演进
课程把隔离见证(SegWit)的脚本模型单独拿出来讲,我认为是有深意的。从脚本角度看,隔离见证最核心的变化是:解锁脚本里的签名数据被移出了原始交易结构,放到了单独的见证字段中。
P2WPKH的锁定脚本写得很简洁,只有:
OP_0 <20字节公钥哈希>它没有OP_DUP,也没有OP_HASH160,因为P2WPKH依赖的是隔离见证的特定验证规则,而不是传统堆栈脚本逻辑。但验证思路其实一脉相承:仍然需要公钥哈希匹配,仍然需要签名有效。
为什么说这是一次重要的演进?因为直接把签名数据从交易序列化字段中剥离,顺带解决了交易延展性问题,同时也为脚本系统引入了一种新的版本化机制。课程笔记里强调,未来新的脚本类型都可以通过类似“版本号+程序”的方式部署,这是比特币脚本从繁琐走向模块化的重要一步。
5. 手写实战:从零构造一笔合法交易
5.1 工具链选择与准备
光看脚本不实操,就像只背了游泳动作不下水一样。为了把这部分吃透,我建议你自己动手构造一笔测试网上的P2PKH交易。不需要依赖复杂的钱包,直接用命令行工具和脚本语言就能搞定。
我自己的实操环境用的是Linux系统,安装了Bitcoin Core节点,并且开启了测试网同步。日常开发中,如果只是做交易构造和脚本验证,也可以用一些在线工具库,但论可控性和学习效果,我还是推荐直接操作节点的方式。下面是基本的准备步骤:
- 安装Bitcoin Core,修改配置切换到测试网同步。
- 使用命令行工具创建钱包,生成一个新的地址。
- 从水龙头获取一些测试币(这个可以自行搜索测试币水龙头)。
- 同步区块,确保节点看到这笔测试币。
测试网的好处在于,你的每一步操作都不会造成真实资金损失,可以放心大胆反复试验。
5.2 构造交易与签名哈希的生成细节
当你已经拿到一个UTXO后,就要开始构造一笔转出交易。这个过程中最复杂的一步就是计算签名哈希。我记得第一次操作时,在这上面整整耗了一天。
核心逻辑是这样的:你需要把交易的所有输入输出按照规定的序列化格式拼成一个“预签名交易”,然后把待花费UTXO的锁定脚本替换成赎回脚本或原始锁定脚本,再进行双重哈希,得到的就是要签名的摘要。
如果你使用Bitcoin Core,可以通过命令完成签名。但为了理解原理,我建议你至少手动构造一次原始交易,然后再用命令完成签名和广播。这个过程能帮助你彻底理解为什么网上很多人警告“不要随便使用在线签名工具”——因为签名哈希覆盖的交易字段一旦被改动,签名就会失效,这本身就是一种保护机制。
5.3 实测数据拆解:一次P2PKH交易的全过程
为了让你看得更直观,我放一个测试网上的简化版例子。某次实操中,我构造了一笔转出交易,输入UTXO对应的锁定脚本就是P2PKH。构造完成后,我把交易数据以十六进制字符串导出,然后手工拆解了关键字段:
- 版本号:占4字节,表示交易版本。
- 输入数量:通常是变长整数,表示这笔交易引用了几个UTXO。
- 前一笔交易ID:32字节,标识被花费的UTXO来源。
- 输出索引:4字节,表示UTXO在上一笔交易中的位置。
- 解锁脚本长度和解锁脚本:包含签名和公钥。
- 序列号:4字节,时间锁相关的字段。
- 输出数量和输出:包含接收地址的锁定脚本和金额。
- 锁定时间:4字节,一般为0。
我把这些字段和对应的脚本拼在一起,本地验证脚本执行结果,确认返回真之后才广播出去。整个过程实测下来比较稳,但其中有一个容易忽略的点:输入的解锁脚本里签名和公钥的顺序不能写反。先压入的是签名,后压入公钥。如果反了,OP_CHECKSIG的语义就完全不同了,验证必然失败。
这类细节,只有自己动手写一遍才能真正记住。
6. 多重签名与其他高级脚本的玩法
6.1 经典的多重签名脚本结构
多重签名是比特币脚本系统里最早被广泛使用的非P2PKH标准脚本之一。它的典型逻辑是:一笔资金需要M个私钥中的至少N个签名才能花费。最经典的配置是2-of-3,也就是三个公钥中任意两个签名即可动用资金。
多重签名脚本放进P2SH的赎回脚本里,结构如下:
OP_2 <公钥A> <公钥B> <公钥C> OP_3 OP_CHECKMULTISIG其中opcode对应的参数一目了然:OP_2是需要的签名数量,OP_3是公钥总数,OP_CHECKMULTISIG负责验证签名数量是否达标。
这里有一个非常著名的历史遗留问题:OP_CHECKMULTISIG从堆栈中会多弹出一个无关元素,导致在使用时必须额外提供一个空数据占位。这个问题被戏称为“多余元素缺陷”。如果你用某些脚本工具手动构造多签交易,一定要记得在解锁脚本的开头加上一个OP_0,否则脚本会验证失败。这个教训我是实际踩过的,所以多说一句。
从应用场景来看,多重签名在托管、企业资金管理、去中心化组织资金管理等领域应用广泛。理解它的脚本底层逻辑,有助于看懂这些上层应用到底做了什么事。
6.2 OP_RETURN与链上数据存证
OP_RETURN是一个很有意思的操作码。它的作用是让输出被标记为“不可花费”,任何后续交易都无法引用这个输出作为输入。实际上,这是为了让节点可以安全地在交易中附带任意数据,而不用假装它是一个UTXO。
课程笔记里提到,有些早期的做法是把数据藏在一个HTLC中,然后通过巨额手续费惩罚让这个输出永远不被花费,这种做法既浪费又不符合规范。OP_RETURN的出现解决了这个问题——它明确地告诉网络:“这串数据不是钱,只是信息”。通过这样的机制,比特币区块链可以承载时间戳存证、文本记录、甚至小型媒体文件的哈希指纹。
很多NFT项目在早期就是用OP_RETURN来记录元数据或者唯一标识的。虽然现在的区块链生态有了更多复杂协议,但理解OP_RETURN依然对理解链上数据可验证性具有帮助意义。
6.3 时间锁脚本:给资金加一个“定时开关”
时间锁也是脚本系统里被低估的一类功能。课程笔记在讲高级脚本时用了不少篇幅解释OP_CHECKLOCKTIMEVERIFY(CLTV)和OP_CHECKSEQUENCEVERIFY(CSV)这两个操作码。
简单理解:
- CLTV:锁定到某个绝对区块高度或时间点,在此之前资金无法花费。
- CSV:锁定到某个相对时间,即从UTXO被打包确认后开始计时,经过多少区块或多少秒之后才能花费。
这两类操作码通常被用在支付通道、合约执行等场景中。比如A和B约定了一笔以7天为期的托管交易,到期前B无法取走资金,到期后B可以单方面完成提取。这个逻辑如果从脚本层面去理解,就是锁定脚本里多了一个高度或时间条件的判断,这一点在理解金融合约类应用时比较关键。
7. 课程笔记中容易被忽略的细节与常见理解误区
7.1 误解一:地址就是公钥
我在无数场合看到有人把比特币地址和公钥混为一谈。课程里虽然没有专门列一个“常见误区”清单,但讲解P2PKH脚本时其实已经把这个关系讲透了:地址是从公钥哈希编码而来的字符串,而公钥是实际参与签名验证的数据。P2PKH的锁定脚本中存储的是公钥哈希,不是公钥,更不是地址。
之所以要设计成“地址=公钥哈希”的模式,是为了降低公钥提前暴露的风险。这就像你不会把家里的钥匙模型拍照发到朋友圈,而是只给访客看一个门牌号,让他到了门口再核对钥匙。
7.2 误解二:解锁脚本是一段“真正的代码”
有初学者会把解锁脚本理解为“某段程序源码”,但实际上它更像是一份“数据+指令”的结合体。锁定脚本才是真正写满了校验规则的代码;解锁脚本更多时候只是按照锁定脚本的预期,把必要的数据压入栈中。
如果锁定脚本是一个数学证明题,那么解锁脚本就是答案。题目问的是“你知道私钥对应的公钥是什么吗”,解锁脚本就回答“我知道,公钥在此,签名可以证明我确实拥有对应私钥”。这种一问一答的机制,决定了整个比特币交易验证的核心模式。
7.3 误解三:脚本执行不可回滚
还有一个常见的误区是认为“脚本执行一旦开始就不能失败”。实际上,脚本验证失败的那一刻,整个交易就会被判定为无效,不会写入区块。节点在执行过程中遇到任何不满足校验的操作码,都会直接判定为“假”。也就是说,执行失败不是回滚,而是整个交易的合法性质疑,最终该交易会被网络拒绝。
理解这一点对于设计钱包或构造交易很有帮助:在广播前,一定要先在自己的节点上模拟执行脚本,确保验证结果是“真”,否则可能因为少了一个OP_0或者签名顺序不对,白白浪费手续费和时间。
8. 常见问题与排查技巧实录
8.1 问题一:签名验证总是失败
我刚开始构造交易时,遇到最多的问题就是签名验证失败。排查下来,绝大多数原因是签名哈希的计算范围与节点实际验证的范围不一致。常见的坑包括:
- 输入的UTXO锁定脚本与实际链上的不符。
- 签名的附加哈希类型(SIGHASH类型)不匹配。
- 公钥格式不是压缩公钥但脚本中用了压缩格式标记。
排查技巧很简单:把构造好的交易用节点自带的解码工具做一次完整的JSON格式解码,然后逐字段与自己的构造逻辑比对。看起来费时间,但一旦养成这个习惯,问题基本都能快速定位。
8.2 问题二:建了很多P2SH却不知道怎么花
很多人在测试网领了P2SH地址的测试币,等到想转出时,突然发现自己根本不知道该往解锁脚本里塞什么。这个问题是因为缺少了对RedeemScript的理解。
解决方案就一句话:花费P2SH的UTXO时,必须在解锁脚本里附带完整的RedeemScript。如果你把这个RedeemScript忘了,节点在执行时根本无法完成二次脚本验证。我在测试时专门试过不带RedeemScript的情况,结果是毫无意外的“验证失败”。
8.3 问题三:不理解OP_CHECKMULTISIG多余元素问题
这个问题前面已经提过,但因为它足够隐蔽,值得放入排查清单。任何手动构造多签交易的人,十有八九都会在这上面翻车。简单回顾一下:OP_CHECKMULTISIG的实际执行逻辑会从栈里多弹出一个元素,所以在解锁脚本中必须手动加上一个无效占位符OP_0。这个设计缺陷从比特币诞生就存在,一直没有被修复,因为修复它会导致所有历史多签交易失效率变化,引发硬分叉风险。
所以,如果你看到一段多签交易的解锁脚本以OP_0开头,别觉得奇怪,这恰恰是正确写法。
9. 这个脚本世界还能继续扩展什么
我个人在实际操作中的体会是,比特币脚本虽然看着古老,但它定义了一套极其清晰的“验证范式”,后续无数区块链项目的智能合约设计,都在这套范式里找到了影子。像以太坊那种更强大的虚拟机,其实要做的事情和比特币脚本一样:定义规则、验证状态、达成共识。区别只是表达力和资源隔离策略不同。
最后再分享一个小技巧:如果某天你要深入研究一种新的脚本类型(比如支持智能合约的Taproot脚本),不要急着找高深资料,先从解锁脚本与锁定脚本拼接执行的视角去分析它,再对照标准交易脚本做差异对比,会事半功倍。这套方法论,比死记任何一条操作码都要实用得多。