第七章. 授权和认证 Part2

  • berry
  • 发布于 2025-02-09 15:54
  • 阅读 14

第七章. 授权和认证 Part2

综合介绍

当你收到比特币时,你必须决定谁有权花费它们,这称为授权。你还必须决定全节点如何区分经授权的花费者和其他人,这称为认证。你的授权指示和花费者的认证证明将由成千上万个独立的全节点进行检查,所有这些节点都需要得出相同的结论,即一笔交易的花费是经过授权和认证的,才能使包含它的交易有效。

比特币的最初描述使用了公钥进行授权。艾丽斯通过将鲍勃的公钥放入交易的输出中向他支付了比特币。认证来自鲍勃,形式是一笔承诺进行支出的签名,例如从鲍勃到卡罗尔。

最初发布的比特币实际版本提供了更灵活的授权和认证机制。此后的改进进一步增强了这种灵活性。在本章中,我们将探讨这些功能,并了解它们最常见的用法。

交易脚本和脚本语言

比特币的原始版本引入了一种称为脚本(Script)的新编程语言,它是一种类似于Forth的基于堆栈的语言。在交易输出中放置的脚本以及用于支出交易的传统输入脚本都是用这种脚本语言编写的。

脚本是一种非常简单的语言。它需要最少的处理,并且不能轻松地做现代编程语言可以做的许多花哨的事情。

当遗留交易是比特币网络中最常见的交易类型时,大多数通过比特币网络处理的交易都具有“支付给Bob的比特币地址”形式,并使用一种称为支付到公钥哈希(P2PKH)脚本的脚本。然而,比特币交易并不限于“支付给Bob的比特币地址”脚本。事实上,脚本可以编写成表达各种复杂条件的形式。为了理解这些更复杂的脚本,我们必须首先了解交易脚本和脚本语言的基础。

在本节中,我们将演示比特币交易脚本语言的基本组成部分,并展示如何使用它来表达花费条件以及如何满足这些条件。

注意:比特币交易验证不是基于静态模式,而是通过执行脚本语言来实现的。这种语言允许表达几乎无限种类的条件。

图灵不完备

比特币交易脚本语言包含许多运算符,但在一个重要方面有意限制——它没有循环或复杂的流程控制能力,除了条件流程控制。这确保了该语言不是图灵完备的,也就是说,脚本具有有限的复杂性和可预测的执行时间。脚本不是通用的语言。这些限制确保了语言不能用于创建无限循环或其他形式的“逻辑炸弹”,这可能会以导致针对比特币网络的拒绝服务攻击的方式嵌入到交易中。请记住,比特币网络上的每个全节点都会验证每个交易。有限的语言防止了交易验证机制被用作漏洞。

无状态验证

比特币交易脚本语言包含许多运算符,但在一个重要方面有意限制——它没有循环或复杂的流程控制能力,除了条件流程控制。这确保了该语言不是图灵完备的,也就是说,脚本具有有限的复杂性和可预测的执行时间。脚本不是通用的语言。这些限制确保了语言不能用于创建无限循环或其他形式的“逻辑炸弹”,这可能会以导致针对比特币网络的拒绝服务攻击的方式嵌入到交易中。请记住,比特币网络上的每个全节点都会验证每个交易。有限的语言防止了交易验证机制被用作漏洞。

脚本构建

比特币的传统交易验证引擎依赖于脚本的两个部分来验证交易:输出脚本和输入脚本。

输出脚本指定了必须满足的条件,以便在未来花费输出,例如谁被授权花费输出以及如何进行身份验证。

输入脚本是满足输出脚本中设定条件并允许花费输出的脚本。输入脚本是每个交易输入的一部分。在传统交易中,大部分情况下它们包含用户钱包从私钥生成的数字签名,但并非所有输入脚本都必须包含签名。

每个比特币验证节点将通过执行输出脚本和输入脚本来验证交易。正如我们在第6章中看到的那样,每个输入包含一个指向先前交易输出的输出点。输入还包含一个输入脚本。验证软件将复制输入脚本,检索由输入引用的UTXO,并从该UTXO中复制输出脚本。然后一起执行输入和输出脚本。如果输入脚本满足输出脚本的条件(参见“分别执行输出和输入脚本”),则输入有效。所有输入都独立验证,作为交易的总体验证的一部分。

请注意,上述步骤涉及复制所有数据。先前输出和当前输入的原始数据永远不会更改。特别是,先前的输出是不变的,不受未能花费它的尝试的影响。只有一个有效的交易才能正确满足输出脚本的条件,才能将输出视为“已花费”。

图7-1是传统比特币交易(付款给公钥哈希)最常见类型的输出和输入脚本的示例,显示了在验证之前对脚本进行串联的组合脚本。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/7.1.png" alt=""><figcaption><p>图 7-1. 组合输入脚本和输出脚本以评估交易脚本</p></figcaption></figure>

脚本执行堆栈

\ 比特币的脚本语言被称为基于栈的语言,因为它使用一种称为栈的数据结构。栈是一种非常简单的数据结构,可以被视为一叠卡片。栈具有两个基本操作:push 和 pop。push 将一个项目添加到栈顶。pop 从栈顶移除项目。

脚本语言通过从左到右处理每个项目来执行脚本。数字(数据常量)被推送到栈上。操作符从栈中推送或弹出一个或多个参数,对它们进行操作,并可能将结果推送到栈上。例如,OP_ADD 将从栈上弹出两个项目,将它们相加,并将结果推送到栈上。

条件运算符评估一个条件,产生一个布尔结果,为 TRUE 或 FALSE。例如,OP_EQUAL 从栈中弹出两个项目,并在它们相等时推送 TRUE(TRUE 用数字 1 表示),在它们不相等时推送 FALSE(用数字 0 表示)。比特币交易脚本通常包含一个条件运算符,以便产生表示有效交易的 TRUE 结果。

一个简单的脚本

现在让我们将所学到的关于脚本和堆栈的知识应用到一些简单的示例中。

正如我们将在图7-2中看到的那样,脚本 2 3 OP_ADD 5 OP_EQUAL 展示了算术加法操作符 OP_ADD,将两个数字相加并将结果放入堆栈中,然后是条件操作符 OP_EQUAL,它检查结果的总和是否等于5。为了简洁起见,本书中的示例有时会省略 OP_ 前缀。有关可用的脚本操作符和函数的更多详细信息,请参阅比特币维基的脚本页面

尽管大多数传统的输出脚本都引用了一个公钥哈希(本质上是一个传统的比特币地址),从而要求证明拥有权才能花费这些资金,但脚本并不一定要那么复杂。任何输出和输入脚本的组合,只要产生 TRUE 值,都是有效的。我们用作脚本语言示例的简单算术也是有效的脚本。

使用算术示例脚本的一部分作为输出脚本:

3 OP_ADD 5 OP_EQUAL

这可以通过包含以下输入脚本的交易来满足:

2

验证软件结合了这些脚本:

2 3 OP_ADD 5 OP_EQUAL

\ 正如我们在图7-2中所看到的那样,当执行此脚本时,结果为OP_TRUE,使得交易有效。尽管这是一个有效的交易输出脚本,但请注意,得到的UTXO可以被任何具有算术技能的人花费,以知道数字2满足脚本。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/7.2.png" alt=""><figcaption><p>图 7-2. 比特币的脚本验证进行简单的数学运算</p></figcaption></figure>

注意:如果栈顶的结果为TRUE(任何非零值),则交易有效。如果栈顶的值为FALSE(值为零或空栈),脚本执行被明确中止(例如使用VERIFY、OP_RETURN等操作符),或者脚本不是语义上有效的(例如包含未由OP_ENDIF操作码终止的OP_IF语句),则交易无效。详情请参阅比特币维基的脚本页面。

以下是一个稍微复杂一些的脚本,它计算了2 + 7 - 3 + 1。请注意,当脚本中包含多个连续的操作符时,栈允许一个操作符的结果被下一个操作符使用:

2 7 OP_ADD 3 OP_SUB 1 OP_ADD 7 OP_EQUAL

尝试使用铅笔和纸验证前面的脚本。当脚本执行结束时,你应该在堆栈上留下一个TRUE值。

输出脚本和输入脚本的分离执行

在最初的比特币客户端中,输出脚本和输入脚本被连接在一起并按顺序执行。出于安全原因,这在2010年发生了改变,因为存在一个漏洞,即1 OP_RETURN bug。在当前的实现中,这些脚本是分开执行的,同时在两次执行之间传递栈数据。

首先,使用栈执行引擎执行输入脚本。如果输入脚本在执行过程中没有出现错误并且没有剩余操作,则栈数据会被复制,然后执行输出脚本。如果使用从输入脚本复制的栈数据执行输出脚本的结果为TRUE,则表示输入脚本已成功解析了输出脚本所施加的条件,因此输入是有效的,可以用于花费UTXO。如果在合并脚本执行后仍然存在除TRUE之外的任何结果,则表示输入是无效的,因为它未能满足对输出所施加的花费条件。

支付至公钥哈希(P2PKH)

支付至公钥哈希(P2PKH)脚本使用一个包含哈希值的输出脚本,该哈希值与一个公钥相关联。P2PKH 最为人熟知的是作为传统比特币地址的基础。一个 P2PKH 输出可以通过提供与哈希值相匹配的公钥以及由相应私钥创建的数字签名来进行消费(参见第 8 章)。让我们看一个 P2PKH 输出脚本的例子:

OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG

密钥哈希是将编码到传统的base58check地址中的数据。大多数应用程序会使用十六进制编码显示脚本中的公钥哈希,而不是以“1”开头的熟悉的比特币地址base58check格式。

前面的输出脚本可以通过以下形式的输入脚本满足:

\<Signature> \<Public Key>\

两个脚本合在一起将形成以下组合验证脚本:

\<Sig> OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG

如果输入脚本具有与设置为限制条件的公钥哈希相对应的Bob的私钥的有效签名,结果将为TRUE。

图 7-3 和 7-4 分两部分展示了组合脚本的逐步执行过程,证明这是一个有效的交易。\

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/7.3.png" alt=""><figcaption><p>图 7-3. 执行P2PKH交易的脚本(1/2)</p></figcaption></figure>

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/7.4.png" alt=""><figcaption><p>图 7-4. 执行P2PKH交易的脚本(2/2)</p></figcaption></figure>

脚本化的多重签名

多重签名脚本设置了一个条件,其中 k 个公钥被记录在脚本中,至少其中的 t 个必须提供签名才能花费资金,称为 t-of-k。 例如,一个 2-of-3 多重签名是指列出了三个潜在签名者的公钥,并且至少必须使用其中的两个来为有效交易创建签名以花费资金。

注意:一些比特币文档,包括本书早期的版本,使用术语“m-of-n”来表示传统的多重签名。然而,当它们被说出时,“m”和“n”很难区分,因此我们使用替代的 t-of-k。这两个短语都指的是相同类型的签名方案。

设置 t-of-k 多重签名条件的输出脚本的一般形式是:

t \<Public Key 1> \<Public Key 2> ... \<public Key k> k OP_CHECKMULTISIG

其中,k 是列出的公钥总数,t 是需要的签名门槛,以花费输出。

一个设置 2-of-3 多重签名条件的输出脚本如下所示:

2 \<Public Key A> \<Public Key B> \<Public Key C> 3 OP_CHECKMULTISIG

前面的输出脚本可以通过包含签名的输入脚本满足:

\<Signature B> \<Signature C>

或者是来自所列的3个公钥对应的任意两个私钥的签名组合。

两个脚本一起形成组合验证脚本:

\<Sig B> \<Sig C> 2 \<Pubkey A> \<Pubkey B> \<Pubkey C> 3 OP_CHECKMULTISIG

执行此组合脚本时,如果输入脚本具有与设置为限制条件的三个公钥中的两个相对应的两个有效签名,则评估结果将为TRUE。

目前,Bitcoin Core的交易中继策略将多重签名输出脚本限制为最多三个列出的公钥,这意味着你可以在范围内执行从1-of-1到3-of-3的任何操作,或者在该范围内的任何组合。你可能希望检查IsStandard()函数,以查看网络当前接受的内容。请注意,三个密钥的限制仅适用于标准(也称为“裸”)多重签名脚本,而不适用于包含在其他结构(如P2SH、P2WSH或P2TR)中的脚本。P2SH多重签名脚本受到政策和共识的限制,最多可包含15个密钥,允许最多15-of-15的多重签名。我们将在“支付到脚本哈希”中了解有关P2SH的信息。所有其他脚本根据每个OP_CHECKMULTISIG或OP_CHECKMULTISIGVERIFY操作码的共识限制为20个密钥,尽管一个脚本可能包含多个这些操作码。\

CHECKMULTISIG执行中的一些怪异情况

\ CHECKMULTISIG执行中存在一个奇怪的问题,需要做一些小的调整。当执行CHECKMULTISIG操作时,它应该消耗t + k + 2个堆栈项作为参数。然而,由于这个奇怪的问题,CHECKMULTISIG会多弹出一个值,或者比预期多一个值。

让我们通过之前的验证示例更详细地了解这个问题:

\<Sig B> \<Sig C> 2 \<Pubkey A> \<Pubkey B> \<Pubkey C>3 OP_CHECKMULTISIG

首先,OP_CHECKMULTISIG弹出顶部项目,即k(在此示例中为“3”)。然后,它弹出k个项目,即可以签名的公钥;在此示例中,为公钥A、B和C。然后,它弹出一个项目,即t,即需要的签名数(即有多少个签名是必需的)。在这里t = 2。此时,OP_CHECKMULTISIG应该弹出最后的t个项目,即签名,并查看它们是否有效。然而,不幸的是,实现中的一个异常导致OP_CHECKMULTISIG弹出了比应该多一个项目(总共是t + 1)。这个额外的项目被称为虚拟堆栈元素,当检查签名时,它被忽略,因此对OP_CHECKMULTISIG本身没有直接影响。然而,虚拟元素必须存在,因为如果在OP_CHECKMULTISIG尝试在空堆栈上弹出时不存在,它将导致堆栈错误和脚本失败(将交易标记为无效)。由于虚拟元素被忽略,它可以是任何值。早期的惯例是使用OP_0,后来成为一个中继策略规则,并最终成为一个共识规则(通过BIP147的执行)。

由于弹出虚拟元素是共识规则的一部分,因此必须永远复制。因此,脚本应如下所示:

OP_0 \<Sig B> \<Sig C> 2 \<Pubkey A> \<Pubkey B> \<Pubkey C> 3 OP_CHECKMULTISIG

因此,实际上在多签名中使用的输入脚本不是:

\<Signature B> \<Signature C>

而是:

OP_0 \<Sig B> \<Sig C>

\ 一些人认为这种奇怪的现象是比特币原始代码中的一个错误,但还存在一个合理的替代解释。验证 t-of-k 签名可能需要比 t 或 k 更多的签名检查操作。让我们考虑一个简单的例子,1-of-5,具有以下组合脚本:

\<dummy> \<Sig4> 1 \<key0> \<key2> \<key3> \<key4> 5 OP_CHECKMULTISIG

首先对签名进行与 key0 的比对,然后是 key1,然后再对其前面的其他键进行比对,最后才与其对应的 key4 进行比对。这意味着即使只有一个签名,也需要执行五次签名检查操作。消除这种冗余的一种方法是为 OP_CHECKMULTISIG 提供一个映射,指示提供的签名与哪个公钥对应,从而使 OP_CHECKMULTISIG 操作只执行确切的 t 次签名检查操作。可能比特币的原始开发者在比特币的原始版本中添加了额外的元素(现在称为虚拟堆栈元素),以便他们可以在以后的软分叉中添加传递映射的功能。然而,该功能从未实现,并且2017年对共识规则的BIP147更新使得将来不可能添加该功能。

只有比特币的原始开发者能告诉我们虚拟堆栈元素是一个错误还是一个未来升级的计划。在本书中,我们只是将其称为奇异现象。

从现在开始,如果你看到一个多重签名脚本,你应该期望在开头看到一个额外的 OP_0,它唯一的目的是解决共识规则中的一个奇异现象。

\

支付到脚本哈希(Pay to Script Hash,P2SH)

\ 支付到脚本哈希(Pay to Script Hash,P2SH)于2012年引入,作为一种强大的新型操作,极大地简化了复杂脚本的使用。为了解释P2SH的必要性,让我们看一个实际的例子。

穆罕默德是一家总部位于迪拜的电子产品进口商。穆罕默德的公司广泛使用比特币的多重签名功能来管理其企业账户。多重签名脚本是比特币高级脚本功能中最常见的用法之一,也是一种非常强大的功能。穆罕默德的公司为所有客户支付使用了多重签名脚本。客户支付的任何款项都被锁定,需要至少两个签名才能释放。穆罕默德、他的三位合伙人和他们的律师每人都可以提供一个签名。这样的多重签名方案提供了企业治理控制,并防止了盗窃、侵占或丢失。

由此产生的脚本相当长,看起来像这样:

\ 2 \<Mohammed's Public Key> \<Partner1 Public Key> \<Ppartner2 Public Key> \<Partner3 Public Key> \<Attorney Public Key> 5 OP_CHECKMULTISIG

\ 虽然多重签名脚本是一种强大的功能,但它们使用起来很麻烦。考虑到前面的脚本,穆罕默德必须在支付之前将该脚本传达给每个客户。每个客户都必须使用具有创建自定义交易脚本能力的特殊比特币钱包软件。此外,由于该脚本包含非常长的公钥,因此生成的交易将比简单的支付交易大约多五倍。额外的数据负担将以额外的交易费用的形式由客户承担。最后,像这样的大型交易脚本将在每个完整节点的UTXO集中保存,直到被花费。所有这些问题使得在实践中使用复杂的输出脚本变得困难。

P2SH的开发旨在解决这些实际困难,并使使用复杂脚本像向单个密钥比特币地址支付一样简单。在P2SH支付中,复杂脚本被替换为一个承诺,即加密哈希的摘要。当稍后尝试花费UTXO的交易被提交时,它必须包含与承诺匹配的脚本,以及满足脚本的数据。简单地说,P2SH意味着“支付到与此哈希匹配的脚本,一个将在此输出被花费时稍后呈现的脚本”。

在P2SH交易中,被哈希替换的脚本被称为赎回脚本,因为它是在赎回时向系统呈现的,而不是作为输出脚本。表7-1显示了不使用P2SH的脚本,表7-2显示了使用P2SH编码的相同脚本。

表 7-1. 不使用P2SH的复杂脚本

Output script 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 OP_CHECKMULTISIG
Input script Sig1 Sig2

表 7-2. 使用P2SH的复杂脚本

Redeem script 2 PubKey1 PubKey2 PubKey3 PubKey4 PubKey5 5 OP_CHECKMULTISIG
Output script OP_HASH160 <20-byte hash of redeem script> OP_EQUAL
Input script Sig1 Sig2

你可以从表格中看到,使用 P2SH,用于描述支出条件(赎回脚本)的复杂脚本并不包含在输出脚本中。相反,输出脚本中只包含其哈希值,而赎回脚本本身会在支出输出时作为输入脚本的一部分呈现。这将费用和复杂性的负担从交易发起者转移到了交易接收者身上。

让我们来看看 Mohammed 公司的情况,以及用于所有客户付款的复杂多重签名脚本及其生成的 P2SH 脚本。

首先,是 Mohammed 公司用于所有客户付款的多重签名脚本:

2 \<Mohammed's Public Key> \<Partner1 Public Key> \<Partner2 Public Key>

\<Partner3 Public Key> \<Attorney Public Key> 5 OP_CHECKMULTISIG

整个脚本可以通过首先应用 SHA256 哈希算法,然后在结果上应用 RIPEMD-160 算法来表示为一个 20 字节的加密哈希。例如,以 Mohammed 的赎回脚本的哈希值为起点:

54c557e07dde5bb6cb791c7a540e0a4796f5e97e

一个 P2SH 交易将输出锁定到此哈希值,而不是较长的赎回脚本,使用了一个特殊的输出脚本模板:

OP_HASH160 54c557e07dde5bb6cb791c7a540e0a4796f5e97e OP_EQUAL

正如你所看到的,这个输出脚本要短得多。与“支付给这个 5 个密钥的多重签名脚本”相比,P2SH 等效交易是“支付给一个具有此哈希的脚本”。客户向 Mohammed 的公司付款时只需在付款中包含这个更短的输出脚本。当 Mohammed 和他的合作伙伴想要花费这个 UTXO 时,他们必须呈现原始的赎回脚本(锁定该 UTXO 的哈希)以及解锁它所需的签名,就像这样:

\<Sig1> \<Sig2> <2 PK1 PK2 PK3 PK4 PK5 5 OP_CHECKMULTISIG>

两个脚本在两个阶段进行合并。首先,检查赎回脚本与输出脚本是否匹配:

<2 PK1 PK2 PK3 PK4 PK5 5 OP_CHECKMULTISIG> OP_HASH160 OP_EQUAL

如果赎回脚本的哈希值匹配,则执行赎回脚本:

\<Sig1> \<Sig2> 2 \<PK1> \<PK2> \<PK3> \<PK4> \<PK5> 5 OP_CHECKMULTISIG\

P2SH地址

P2SH功能的另一个重要部分是将脚本哈希编码为地址,这在BIP13中定义。P2SH地址是脚本的20字节哈希的base58check编码,就像比特币地址是公钥的20字节哈希的base58check编码一样。P2SH地址使用版本前缀“5”,这导致了以“3”开头的base58check编码地址。

例如,Mohammed的复杂脚本,经过哈希和base58check编码成为P2SH地址,变成了39RF6JqABiHdYHkfChV6USGMe6Nsr66Gzw。

现在,Mohammed可以将这个“地址”提供给他的客户,他们可以使用几乎任何比特币钱包进行简单支付,就像对任何其他比特币地址进行支付一样。前缀3提示他们这是一种特殊类型的地址,对应于脚本而不是公钥,但除此之外,它的工作方式与对任何其他比特币地址的支付完全相同。

P2SH地址隐藏了所有复杂性,因此进行支付的人看不到脚本。

P2SH的优点

P2SH功能相比直接在输出中使用复杂脚本具有以下优势:

• 与原始遗留地址的相似性意味着发送者和发送者的钱包不需要复杂的工程来实现P2SH。

• P2SH将长脚本的数据存储负担从输出(除了存储在区块链上之外,还在UTXO集合中)转移到输入(仅存储在区块链上)。

• P2SH将长脚本的数据存储负担从当前时间(支付)转移到将来时间(在其被花费时)。

• P2SH将长脚本的交易费用成本从发送者转移到收件人,后者必须包含长的赎回脚本才能花费它。

赎回脚本和验证

你不能在P2SH赎回脚本中放置一个P2SH,因为P2SH规范不支持递归。此外,虽然技术上可以在赎回脚本中包含OP_RETURN(参见“数据记录输出(OP_RETURN)”),因为规则中没有阻止这样做,但实际上没有任何实际用途,因为在验证期间执行OP_RETURN会导致交易被标记为无效。

请注意,因为直到你尝试花费P2SH输出时才会向网络展示赎回脚本,所以如果你创建一个带有无效赎回脚本哈希的输出,你将无法花费它。包含赎回脚本的花费交易将不会被接受,因为它是一个无效的脚本。这会带来风险,因为你可以将比特币发送到一个以后无法花费的P2SH地址。

注意:P2SH输出脚本包含赎回脚本的哈希,这不会给出有关赎回脚本内容的线索。即使赎回脚本无效,P2SH输出也会被视为有效并被接受。你可能会意外地以一种无法稍后花费的方式接收比特币。

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论