8.BTC-脚本

比特币的脚本语言,通常直接称为 Script,是比特币协议的核心组成部分之一。它是一种有意设计得非常简单、功能受限的编程语言,主要负责处理交易的验证和授权。

  1. 基于堆栈(Stack-based):Script 语言没有变量。所有的操作都是通过一个“堆栈”来完成的。数据被推入(push)堆栈顶部,操作码(Opcodes)会处理堆栈顶部的一个或多个元素,然后将结果推回堆栈。
    • 可以把它想象成一摞盘子:你只能在最上面放盘子(push),也只能从最上面拿盘子(pop)进行操作。
  2. 逆波兰表示法(Reverse Polish Notation, RPN):指令(操作码)跟在数据后面。例如,要计算 2 + 3,在 Script 中会写成 2 3 OP_ADD。计算机会先将 23 推入堆栈,然后 OP_ADD 指令会取出这两个数字,相加后将结果 5 推回堆栈。
  3. 非图灵完备(Non-Turing Complete):这是 Script 最重要的特性之一。故意移除了循环(loops)和复杂的流程控制
    • 为什么这么设计? 为了安全性和可预测性。图灵完备的语言(如以太坊的 Solidity)功能强大,但也可能出现无限循环等问题,导致程序耗尽资源或被攻击(例如著名的 DAO 攻击)。比特币的设计哲学是优先保证系统的稳定和安全,因此 Script 的每个执行步骤都必须是可预测且有限的,从而防止交易验证过程被滥用或卡死。
  4. 无状态(Stateless):每个脚本的执行都是完全独立的,不依赖于任何链下(off-chain)或之前的状态(除了当前正在处理的交易数据)。输入什么,就得到什么结果,保证了验证的一致性。

脚本的工作原理

每一笔比特币交易的输出(UTXO - Unspent Transaction Output)都包含一个锁定脚本(Locking Script),在技术上称为 scriptPubKey。这个脚本定义了花费这笔钱需要满足的条件。

当某人想要花费这笔钱时,他们需要创建一个新的交易,并在输入中提供一个解锁脚本(Unlocking Script),技术上称为 scriptSig

  • 锁定脚本 (scriptPubKey):位于交易输出中。它设置了花费这笔资金的条件(比如,需要提供某个公钥对应的签名)。
  • 解锁脚本 (scriptSig):位于交易输入中。它提供了满足锁定脚本条件的证明(比如,具体的数字签名和公钥)。

验证过程如下:

  1. 首先执行解锁脚本 (scriptSig)。它通常会把一些数据(例如数字签名和公钥)推到堆栈上。
  2. 紧接着,在同一个堆栈上执行锁定脚本 (scriptPubKey)。
  3. 如果两个脚本执行完毕后,堆栈顶部最终的结果是 TRUE(非零值),那么验证通过,这笔资金就可以被花费。否则,交易无效。

组合执行过程: scriptSig + scriptPubKey -> TRUE ?

![[Pasted image 20250627155752.png]]

P2PK(Pay-to-Public-Key)

P2PK 通过锁定脚本(scriptPubKey)和解锁脚本(scriptSig)的组合来工作。

  • 锁定脚本 (scriptPubKey) 当发送者想要用 P2PK 方式支付比特币时,他们会创建一个交易输出(UTXO),其中的锁定脚本非常简单: <接收方的公钥> OP_CHECKSIG
  • <接收方的公钥>:这里是接收方完整的公钥(例如,一个65字节或33字节的字符串)。
  • OP_CHECKSIG:这是一个操作码,它的作用是验证交易签名。它会检查解锁脚本中提供的签名是否是由与这个公钥相匹配的私钥生成的。 这个脚本的含义是:“要想花费这笔钱,你必须提供一个由这个公钥对应的私钥所生成的有效签名。
  • 解锁脚本 (scriptSig) 当接收方(现在是花费者)想要花费这笔被 P2PK 锁定的资金时,他们需要提供一个解锁脚本,内容更加简单: <你的签名>
  • <你的签名>:使用你的私钥对当前这笔花费交易进行签名后得到的数据。
  • 验证过程 当节点验证这笔花费交易时,它会把解锁脚本和锁定脚本拼接起来执行: <签名> <公钥> OP_CHECKSIG
  1. 解锁脚本<签名> 推入堆栈。
  2. 锁定脚本<公钥> 推入堆栈。
  3. OP_CHECKSIG 操作码被执行,它会从堆栈中取出公钥和签名,然后验证该签名对于这笔交易是否有效。
  4. 如果验证通过,OP_CHECKSIG 会在堆栈上留下一个 TRUE 值,交易被确认为有效。如果失败,则交易无效。

P2PKH(Pay-to-Public-Key-Hash)

当你向一个以 1 开头的经典比特币地址发送比特币时,你使用的就是 P2PKH 交易。P2PKH不锁定到公钥本身,而是锁定到公钥的哈希值上。

P2PKH 同样通过锁定脚本和解锁脚本的组合来验证交易。

  1. 锁定脚本 (scriptPubKey)

这是 P2PKH 的“锁”,被放置在交易的输出(Output)中。它规定了花费这笔资金的谜题。

OP_DUP OP_HASH160 <接收方的公钥哈希> OP_EQUALVERIFY OP_CHECKSIG

  • OP_DUP: 复制(Duplicate)堆栈顶部的数据。在这里,它将用来复制花费者提供的公钥。
  • OP_HASH160: 对堆栈顶部的数据执行一次 SHA256 哈希,再执行一次 RIPEMD160 哈希,最终得到一个20字节的哈希值。
  • <接收方的公KEY哈希>: 这是接收方比特币地址中包含的核心信息,是一个20字节的哈希值。这个值被硬编码在锁定脚本中,是谜题的关键部分。
  • OP_EQUALVERIFY: 比较(Equal)堆栈顶部的两个值是否相等,如果相等则移除它们继续执行;如果不等,脚本立即失败(Verify)。
  • OP_CHECKSIG: 检查(Check)交易签名(Signature)是否有效。

这个锁定脚本整体的意思是:“花费者必须提供一个公钥,这个公钥的哈希值必须等于我这里指定的 <公钥哈希>。并且,你还要提供一个用该公钥对应的私钥生成的有效签名。

  1. 解锁脚本 (scriptSig)

这是花费者提供的“钥匙”,被放置在交易的输入(Input)中,用来解开上面的谜题。

<你的签名> <你的公钥>

  • <你的签名>: 使用你的私钥对这笔花费交易进行签名后得到的数据。
  • <你的公钥>: 你完整的公钥。
  1. 验证全过程(堆栈演示)

当节点验证交易时,它会先执行解锁脚本,再执行锁定脚本。我们来看看堆栈(Stack)的变化:

步骤 执行的指令 堆栈内容(底部 -> 顶部) 解释
1 <签名> [<签名>] 解锁脚本将签名推入堆栈。
2 <公钥> [<签名>] [<公钥>] 解锁脚本将公钥推入堆栈。
3 OP_DUP [<签名>] [<公钥>] [<公钥>] 锁定脚本开始执行,复制栈顶的公钥。
4 OP_HASH160 [<签名>] [<公钥>] [<公钥哈希A>] 对复制的公钥进行哈希运算,得到哈希值A。
5 <公钥哈希B> [<签名>] [<公钥>] [<公钥哈希A>] [<公钥哈希B>] 将锁定脚本中预设的公钥哈希B推入堆栈。
6 OP_EQUALVERIFY [<签名>] [<公钥>] 比较A和B是否相等。如果相等,则两者都从堆栈中弹出;如果不等,验证失败。
7 OP_CHECKSIG [TRUE] 用堆栈上剩下的公钥和签名来验证交易。如果签名有效,推入TRUE

P2SH(Pay-to-Script-Hash)

当你向一个以 3 开头的经典比特币地址(或以 bc1q... 开头的现代隔离见证地址,但结构更复杂)发送比特币时,你很可能是在使用 P2SH 或其后续技术。

P2PKH 的模型是“这笔钱属于这个公钥(的所有者)”。而 P2SH 的模型则是“这笔钱属于任何能够满足这个脚本(合约)条件的人”。它允许我们将一笔资金锁定到一个复杂脚本的哈希值上,而不是一个简单公钥的哈希值。这相当于将一个复杂的“智能合约”压缩成一个简短、标准的地址。

P2SH 引入了一个两阶段的验证过程,并增加了一个名为“赎回脚本 (Redeem Script)”的新概念。

  1. 赎回脚本 (Redeem Script)

这是 P2SH 的核心。定义了复杂花费条件的真实脚本。例如,一个“需要3个人中的2个人签名才能花费”的多重签名脚本。这个脚本由接收方(或多方)在链下创建和保管。

  1. 锁定脚本 (scriptPubKey)

这是发送方创建的“锁”,被放置在交易输出中。与 P2PKH 相比,P2SH 的锁定脚本非常简洁和标准化:

OP_HASH160 <赎回脚本的哈希> OP_EQUAL

  • <赎回脚本的哈希>: 接收方只需提供他们那个复杂赎回脚本的 HASH160 哈希值给发送方。
  • OP_HASH160OP_EQUAL: 这两个操作码的组合用来验证花费者提供的脚本哈希是否正确。

这个锁定脚本的意思是:“任何人想花这笔钱,首先必须提供一个脚本,这个脚本的哈希值必须等于我这里指定的 <赎回脚本的哈希>

注意:这个脚本里完全没有 OP_CHECKSIG!它不直接验证签名,只验证脚本哈希。真正的签名验证发生在下一步。

  1. 解锁脚本 (scriptSig)

这是花费者提供的“钥匙”,用来解开上面的锁。它的结构也很有特点:

<签名等参数...> <赎回脚本>

  • <签名等参数...>: 这些是用来满足赎回脚本自身条件的参数。例如,在多重签名中,这里就是多个签名。
  • <赎回脚本>: 这里是那个复杂的、完整的赎回脚本本身。

当节点验证一笔 P2SH 交易时,过程分为两个阶段:

第一阶段:验证脚本哈希

  1. 节点首先执行标准的解锁脚本 (scriptSig) 和锁定脚本 (scriptPubKey)。
  2. 解锁脚本将 <签名等参数...><赎回脚本> 推入堆栈。
  3. 锁定脚本 OP_HASH160 <赎回脚本的哈希> OP_EQUAL 开始执行。
  4. 它会取出花费者提供的 <赎回脚本>,计算其哈希值,并与锁定脚本中预设的哈希值进行比较。
  5. 如果两个哈希值匹配,第一阶段验证通过。如果不匹配,交易无效。

第二阶段:执行赎回脚本

  1. 第一阶段通过后,节点会进行一次特殊的、非标准的脚本执行。
  2. 它会拿出花费者提供的 <赎回脚本>,并用解锁脚本中提供的 <签名等参数...> 作为输入来执行它。
  3. 例如,如果赎回脚本是一个2-of-3的多签脚本,节点就会用提供的2个签名来执行这个脚本,检查签名是否有效。
  4. 如果赎回脚本执行成功(最终堆栈顶为 TRUE),则整个交易验证通过,资金可以被花费。

P2SH的优势:

  • 将复杂性转移给花费者: 发送方无需关心接收方的花费条件有多复杂(是多重签名还是其他合约)。他们只需要一个简短的、以3开头的标准地址,交易的创建过程和 P2PKH 一样简单。所有的复杂性都由接收方(在未来花费时)来处理。
  • 降低了交易费用: 无论赎回脚本有多长、多复杂,它在被花费之前都只以一个20字节的哈希形式存在于区块链的UTXO集中。这比把整个复杂脚本放在交易输出里要节省大量空间,从而为发送方降低了交易费用。
  • 增强了隐私性: 在资金被花费之前,没有人知道这个 P2SH 地址背后隐藏的是什么样的复杂条件。它可能是一个多签钱包,也可能是一个带时间锁的合约。所有的逻辑细节只有在花费时才会被揭露。
  • 促进了功能创新: P2SH 为比特币的“智能合约”功能铺平了道路,使得许多在核心协议层面不支持的功能可以通过脚本来实现,例如:
    • 多重签名 (Multi-Signature):最常见的用例,如公司资金共管、交易所冷钱包等。
    • 时间锁 (Timelocks):允许资金在某个特定时间或区块高度后才能被花费。
    • 原子交换 (Atomic Swaps):跨链交易的基础。
    • 闪电网络通道:闪电网络的支付通道合约也大量使用了 P2SH 或其后续技术 P2WSH

裸多重签名 (Bare Multisig)

BTC最初的多重签名实现方式,现在通常被称为裸多重签名 (Bare Multisig)。最初的实现方式非常直接和笨拙,把所有复杂的验证逻辑都放在了锁定脚本中,给发送方带来了很大的负担。它不使用P2SH的“脚本哈希”技巧,而是直接将一个定义了多签条件的完整脚本作为锁定脚本(scriptPubKey)。

  1. 锁定脚本 (scriptPubKey)

这个脚本的结构非常长,因为它必须包含所有参与方的完整公钥。一个 M-of-N 的裸多重签名锁定脚本格式如下:

<M> <公钥1> <公钥2> ... <公钥N> <N> OP_CHECKMULTISIG

  • <M>: 所需的最少签名数量 (例如 OP_2)。
  • <公钥1> ... <公钥N>: 所有 N 个参与方的完整公钥。这是导致脚本冗长的主要原因。
  • <N>: 参与方的总数量 (例如 OP_3)。
  • OP_CHECKMULTISIG: 这是执行多重签名验证的核心操作码。

举个例子,一个2-of-3的裸多重签名锁定脚本看起来像这样:

OP_2 <公钥A> <公钥B> <公钥C> OP_3 OP_CHECKMULTISIG

这个脚本的意思是:“要花费这笔资金,必须提供A、B、C三个公钥中任意两个对应的有效签名。”

2. 解锁脚本 (scriptSig)

花费者(签名方)需要提供满足条件的签名。其解锁脚本的结构如下:

OP_0 <签名1> <签名2> ...

  • OP_0 (一个奇怪的“历史遗留问题”): OP_CHECKMULTISIG 操作码中存在一个著名的Bug。它在执行时会从堆栈上多弹出一个额外的数据。为了填补这个空缺,所有解锁脚本的开头都必须放一个无用的占位符,通常就是 OP_0
  • <签名1> <签名2> ...: 提供 M 个有效的签名。签名的顺序必须与对应公钥在锁定脚本中出现的顺序一致。

这种直接的实现方式虽然能工作,但在实际使用中暴露了大量问题:

  1. 对发送方不友好,成本高昂
    • 发送方承担复杂性:发送方必须知道接收方所有的公钥,并构建一个又长又复杂的锁定脚本。
    • 交易体积巨大:锁定脚本包含了所有公钥,导致交易体积非常大。在比特币中,交易体积越大,需要支付的矿工费就越高。这意味着发送方要为接收方的复杂安全策略“买单”
  2. 隐私性极差: 资金被发送后,所有参与多签的公钥都原封不动地记录在区块链的UTXO集中,对全网公开。任何人都可以看到这笔钱是由哪些公钥共同管理的,这极大地损害了隐私。
  3. 非标准化,钱包支持困难: 裸多重签名交易没有一个像P2PKH或P2SH那样简短、标准的“地址”。发送方需要处理一长串脚本,这给钱包软件的设计和用户体验带来了极大的麻烦。你没法像告诉朋友一个1...3...开头的地址那样,告诉他一个裸多重签名的“地址”。
  4. 占用UTXO数据库空间: 区块链节点需要维护一个包含所有未花费交易输出(UTXO)的数据库。裸多重签名的大脚本直接存储在这个数据库中,增加了节点的存储负担。

Proof of Burn

OP_RETURN 是比特币脚本语言中的一个操作码。它的主要功能是:在一个交易中创建一个可被证明是“无法花费”(provably unspendable)的输出,并允许你在这个输出中附加一小段任意数据。

工作原理和目的

  1. 工作原理 一个标准的比特币交易输出(Output)包含一个锁定脚本(scriptPubKey),用于规定未来如何花费这笔钱。而一个 OP_RETURN 输出的锁定脚本则非常简单:

OP_RETURN <data>

  • 当比特币节点在验证脚本时,一旦遇到 OP_RETURN,它会立即停止执行并判定这个脚本路径为“失败”。然而,这并不会导致整个交易无效。相反,它是一个明确的信号,告诉全网:“这个输出是故意设计成不能花的,请不要尝试验证它,它就是一段数据。
  1. 主要目的:在区块链上存储数据

OP_RETURN 的核心目的就是提供一个标准化的、对网络友好的方式,将少量数据(通常限制在80字节以内)永久地记录在比特币区块链上。

  1. 为什么需要 OP_RETURN

OP_RETURN 被标准化之前,人们为了在区块链上记录数据,使用如下这些方法:

  • 将数据编码成一串假的P2PKH地址,然后给这些地址发送极小金额的比特币。
  • 构建复杂的、无法执行的脚本来嵌入数据。

这些老方法的坏处是:

  • 污染UTXO集:它们会产生大量永远不会被花费的“交易粉尘”(dust),这些“垃圾”UTXO会永久地占用所有比特币节点的内存和存储空间,增加了网络的负担。
  • 效率低下且不标准

OP_RETURN 的出现解决了这个问题。因为它明确地告诉节点这个输出是“无法花费的”,节点就不需要将它记录在需要随时访问的UTXO数据库中,从而为网络减负。


OP_RETURN 的关键特性

  • 无法花费OP_RETURN 输出中的资金(如果有的话,但通常为0)是永久锁定的。
  • 数据锚定:它的主要功能是作为数据的时间戳,将数据永久锚定在区块链上。
  • 数据量小:为了防止人们用大量数据撑爆区块链,OP_RETURN 输出能承载的数据量非常小(目前主流限制为80字节)。
  • 不污染UTXO集:这是它相比于老的数据存储方法最大的技术优势。

主要用途:

OP_RETURN 催生了许多比特币之上的应用和协议,例如:

  1. 文件存证和时间戳: 你可以计算一个文件(如合同、专利、照片)的哈希值,然后将这个哈希值放入 OP_RETURN 交易中。这就能以无可辩驳的方式证明,在那个时间点,你已经拥有了这个文件的完整内容。
  2. 资产发行和代币化 (Tokenization): 一些构建在比特币之上的协议,最著名的就是 Omni Layer,使用 OP_RETURN 来记录其自身代币的交易信息。例如,当你在Omni协议上发送一笔 Tether (USDT) 时,背后实际上是发生了一笔比特币交易,这笔交易的 OP_RETURN 字段里记录着“从地址A发送了XX数量的USDT到地址B”这样的信息。
  3. 发布不可篡改的公开信息: 一些人会用它来发布简短的声明或消息。