第四章 密钥和地址 #1

  • berry
  • 发布于 2025-02-06 23:32
  • 阅读 19

综合介绍

Alice想向Bob支付比特币,但成千上万的比特币全节点将验证她的交易,这些节点不知道Alice或Bob的真实身份,我们希望保持这种状态以保护他们的隐私。Alice需要传达的是,Bob应该收到她的一些比特币,但不应将该交易的任何方面与Bob的真实世界身份或Bob收到的其他比特币支付联系起来。Alice使用的方法必须确保只有Bob能进一步花费他收到的比特币。

最初的比特币论文描述了一种实现这些目标的非常简单的方案,如图4-1所示。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.1.png" alt=""><figcaption><p>图 4-1. 原始比特币论文中的交易链。</p></figcaption></figure>

像Bob这样的接收者接受由花费者(如Alice)签名的交易中的比特币到一个公钥。Alice要花费的比特币之前是通过她的一个公钥接收到的,她使用相应的私钥生成她的签名。全节点可以验证Alice的签名是否承诺给出一个哈希函数的输出,该哈希函数本身承诺给出Bob的公钥和其他交易细节。

我们将在本章中讨论公钥、私钥、签名和哈希函数,然后将它们全部结合起来描述现代比特币软件使用的地址。

公钥密码学

公钥密码学是在20世纪70年代发明的,是现代计算机和信息安全的数学基础。

自从公钥密码学的发明以来,已经发现了几种合适的数学函数,比如素数指数和椭圆曲线乘法。这些数学函数在一个方向上易于计算,但在当前计算机和算法下,几乎不可能在反向上进行计算。基于这些数学函数,密码学实现了不可伪造的数字签名。比特币使用椭圆曲线加法和乘法作为其密码学的基础。&## x20;

在比特币中,我们可以使用公钥密码学来创建一个控制比特币访问权限的密钥对。密钥对由私钥和从私钥派生的公钥组成。公钥用于接收资金,私钥用于对要花费的资金签名的交易。&## x20;

公钥和私钥之间存在数学关系,使得私钥可以用来对消息生成签名。这些签名可以根据公钥进行验证,而无需揭示私钥。

注意:在一些钱包实现中,私钥和公钥一起存储作为一个密钥对是为了方便起见。然而,公钥可以从私钥计算出来,因此仅存储私钥也是可能的。

一个比特币钱包包含了一组密钥对,每个密钥对由一个私钥和一个公钥组成。私钥(k)是一个数字,通常是从随机选择的数字派生而来。通过私钥,我们使用椭圆曲线乘法,这是一个单向加密函数,来生成一个公钥(K),公钥是椭圆曲线上一个二维坐标点。

为什么比特币中使用非对称加密(公钥/私钥)? 它并不是用于“加密”(保密)交易的。相反,非对称加密的一个有用特性是能够生成数字签名。私钥可以应用到交易中产生一个数字签名。这个签名只能由拥有私钥知识的人产生。然而,任何有公钥和交易访问权限的人都可以使用它们来验证签名。非对称加密的这一有用特性使得任何人都能够验证每一笔交易上的每个签名,同时确保只有私钥的所有者可以生成有效的签名。

私钥

私钥只是一个随机选择的数字。对私钥的控制是用户对与相应的比特币公钥关联的所有资金的控制的根源。私钥用于创建签名,用于在交易中证明对用于支付比特币的资金的控制权。私钥必须始终保密,因为将其透露给第三方等同于将对其保护的比特币的控制权交给他们。私钥还必须备份和保护免受意外丢失,因为如果丢失了,就无法恢复,由其保护的资金也将永远丢失。

比特币私钥只是一个数字。你可以随机选择私钥,只需使用硬币、铅笔和纸:将一枚硬币抛掷 256 次,你就得到了一个随机私钥的二进制数字,可以用于比特币钱包。然后,公钥可以从私钥生成。但要注意,任何不完全随机的过程都可能显著降低私钥及其控制的比特币的安全性。

生成密钥的第一步,也是最重要的一步,是找到一个安全的随机源(计算机科学家称之为熵)。创建比特币密钥几乎与“在 1 到 2^256 之间选择一个数字”相同。只要选择的方法不可预测或不可重复,使用的确切方法就无关紧要。比特币软件使用加密安全的随机数生成器来产生 256 位的熵。

更准确地说,私钥可以是从 0 到 n - 1(包括 n)之间的任何数字,其中 n 是一个常数(n = 1.1578 × 10^77,略小于 2^256),定义为比特币中使用的椭圆曲线的阶(请参阅“椭圆曲线密码学解释”)。为了创建这样的密钥,我们随机选择一个 256 位的数字,并检查它是否小于 n。在编程术语中,通常通过将大量随机位串,从一个加密安全的随机源收集而来,输入到 SHA256 哈希算法中来实现,这将方便地产生一个可以解释为数字的 256 位值。如果结果小于 n,我们就有了一个合适的私钥。否则,我们只需使用另一个随机数再次尝试。

特别注意:不要编写自己的代码来创建随机数,也不要使用编程语言提供的“简单”随机数生成器。使用具有足够熵源的加密安全伪随机数生成器(CSPRNG)的种子。研究你选择的随机数生成器库的文档,确保它是加密安全的。正确实现 CSPRNG 对密钥的安全性至关重要。

以下是以十六进制格式显示的随机生成的私钥(k)(256位显示为64个十六进制数字,每个4位):

1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD

注意:比特币的私钥空间大小(2^256)是一个难以想象的大数。它约为10^77,以十进制表示。作为比较,可见宇宙估计包含10^80个原子。

椭圆曲线密码学解释

椭圆曲线密码学(ECC)是一种基于离散对数问题的非对称或公钥密码学,其表达方式是通过椭圆曲线上的点的加法和乘法来实现的。&### x20;

图4-2是椭圆曲线的一个示例,类似于比特币使用的曲线。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.2.png" alt=""><figcaption><p>图 4-2. 一种椭圆曲线</p></figcaption></figure>

比特币使用一种特定的椭圆曲线和一组数学常数,这在一种称为secp256k1的标准中定义,该标准由美国国家标准与技术研究院(NIST)确定。 secp256k1曲线由以下函数定义,该函数产生一个椭圆曲线:

$$ y^2=(x^3+7) over (F_p) $$

或者

$$ y^2\mod\ p = (x^3 + 7) \mod \ p $$

模 p(模质数 p)表示这个曲线是在一个素数 p 的有限域上,也写作 Fp,其中 p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1,是一个非常大的素数。因为这个曲线是在一个素数的有限域上定义的,而不是在实数上,所以它看起来像是在二维空间中散布的点的模式,这使得它难以可视化。然而,数学上与实数上的椭圆曲线完全相同。举个例子,图 4-3 展示了相同的椭圆曲线在一个比较小的素数阶有限域 17 上的情况,显示了一个点在网格上的模式。Bitcoin 的 secp256k1 椭圆曲线可以看作是一个非常复杂的点模式在一个超大网格上。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.3.png" alt=""><figcaption><p>图 4-3. 椭圆曲线密码学:可视化椭圆曲线在 F(p) 上,其中 p=17。</p></figcaption></figure>

因此,例如,以下是 secp256k1 曲线上的坐标为 (x, y) 的点 P:

\ P = (55066263022277343669578718895168534326250603453777594175500187360389116729240, 32670510020758816978083085130507043184471273380659243275938904335757337482424)

Example 4-1 展示了如何使用 Python 自行验证这一点。

Example 4-1. 使用 Python 确认该点位于椭圆曲线上

Python 3.10.6 (main, Nov 14 2022, 16:10:14) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
> p = 115792089237316195423570985008687907853269984665640564039457584007908834671663
> x = 55066263022277343669578718895168534326250603453777594175500187360389116729240
> y = 32670510020758816978083085130507043184471273380659243275938904335757337482424
> (x ** 3 + 7 - y**2) % p
0

在椭圆曲线数学中,有一个称为“无穷点”的点,大致对应于加法中的零。在计算机中,它有时以 x = y = 0 表示(虽然不满足椭圆曲线方程,但这是一个容易单独检查的简单情况)。

还有一个称为“加法”的 + 运算符,其性质与小学生学习的实数加法类似。给定椭圆曲线上的两点 P1 和 P2,有第三点 P3 = P1 + P2,也在椭圆曲线上。

几何上,第三点 P3 是通过在 P1 和 P2 之间画一条线来计算的。这条线将在椭圆曲线上的一个额外位置相交。将这一点称为 P3' =(x,y)。然后在 x 轴上反射以得到 P3 =(x,-y)。

有几种特殊情况解释了“无穷点”的必要性。如果 P1 和 P2 是相同的点,则“介于” P1 和 P2 之间的线应延伸成为此点 P1 上的切线。这条切线将在曲线上的一个新点上相交。您可以使用微积分技术确定切线的斜率。尽管我们将兴趣限制在具有两个整数坐标的曲线上的点,但这些技术奇迹般地奏效!

在某些情况下(即,如果 P1 和 P2 具有相同的 x 值但具有不同的 y 值),切线将正好是垂直的,此时 P3 = “无穷点”。

如果 P1 是“无穷点”,则 P1 + P2 = P2。同样,如果 P2 是无穷点,则 P1 + P2 = P1。这显示了无穷点起到了零的作用。

事实证明 + 是可结合的,这意味着(A + B)+ C = A +(B + C)。这意味着我们可以写出 A + B + C,而无需括号或模棱两可。

现在我们已经定义了加法,我们可以按照扩展加法的标准方式来定义乘法。对于椭圆曲线上的点 P,如果 k 是一个整数,则 kP = P + P + P + … + P(k 次)。请注意,在这种情况下,有时会令人困惑地称 k 为“指数”。

公钥

公钥是使用椭圆曲线乘法从私钥计算的,这是不可逆的过程:K = k × G,其中 k 是私钥,G 是一个称为生成点的常量点,而 K 是生成的公钥。反向操作称为“找到离散对数”——即在已知 K 的情况下计算 k,就像尝试所有可能的 k 值一样(即,进行穷举搜索)。在我们演示如何从私钥生成公钥之前,让我们稍微详细地了解一下椭圆曲线密码学。

注意: 椭圆曲线乘法是密码学家称之为“陷阱门”函数的一种类型:在一个方向上(乘法)很容易进行,而在相反方向(除法)则几乎不可能进行。拥有私钥的人可以轻松地创建公钥,然后将其与世界分享,因为他们知道没有人能够反转该函数并从公钥计算出私钥。这种数学技巧成为无法伪造和安全的数字签名的基础,证明对比特币资金的控制权。

从形式为随机生成的数字 k 的私钥开始,我们将其乘以曲线上的一个预定点,称为生成点 G,以在曲线上的其他位置生成另一个点,即相应的公钥 K。生成点作为 secp256k1 标准的一部分被指定,并且对比特币中的所有密钥始终是相同的:

K = k × G

其中,k 是私钥,G 是生成点,而 K 是结果公钥,即曲线上的一个点。由于生成点对于所有比特币用户都是相同的,私钥 k 乘以 G 总是会得到相同的公钥 K。k 和 K 之间的关系是固定的,但只能在一个方向上计算,即从 k 到 K。这就是为什么比特币公钥(K)可以与任何人共享而不会暴露用户的私钥(k)。

注意: 私钥可以转换为公钥,但公钥不能转换回私钥,因为数学运算只能单向进行。

实现椭圆曲线乘法时,我们将之前生成的私钥 k 与生成点 G 相乘,以得到公钥 K:

这里G的坐标为(79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DC28D9 59F2815B 16F81798, 483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8)。由于椭圆曲线,只要知道点x轴坐标,就可以算出y轴坐标,但是由于y有正负两种。所以可以使用压缩表示。前缀02表示y为正的点,03表示y为负的点,04表示不压缩。像下面两种压缩表示都是一样的点坐标,但可以看出压缩后的02表示类型所需要的字节数差不多少了一半字节。

G_compressed = 02 79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798
G_uncompressed = 04 79BE667E F9DCBBAC 55A06295 CE870B07 029BFCDB 2DCE28D9 59F2815B 16F81798 483ADA77 26A3C465 5DA4FBFC 0E1108A8 FD17B448 A6855419 9C47D08F FB10D4B8

K = 1E99423A4ED27608A15A2616A2B0E9E52CED330AC530EDCC32C8FFC6A526AEDD × G

公钥 K 被定义为点 K = (x, y):

K = (x, y)

其中,

x = F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A&### x20;

y = 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB&### x20;

为了可视化点与整数的乘法,我们将使用实数上的简单椭圆曲线——请记住,数学是一样的。我们的目标是找到生成点 G 的倍数 kG,这等同于将 G 加上自身,连续 k 次。在椭圆曲线中,将一个点加到它自身相当于在该点上画一条切线,并找到它再次与曲线相交的位置,然后将该点在 x 轴上反射。&### x20;

图 4-4 展示了在曲线上执行几何操作来得到 G、2G、4G 的过程。

注意:许多比特币实现使用 libsecp256k1 加密库执行椭圆曲线数学运算。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.4.png" alt=""><figcaption><p>图 4-4. 椭圆曲线密码学:在椭圆曲线上将点 G 与整数 k 相乘的可视化示例。</p></figcaption></figure>

输出和输入脚本

虽然原始比特币论文中的图 4-1 显示了公钥(pubkeys)和签名(sigs)直接使用的情况,但比特币的第一个版本实际上是将支付发送到一个称为输出脚本的字段,并且这些比特币的花费由一个称为输入脚本的字段授权。这些字段允许执行额外的操作,除了(或代替)验证签名是否对应于公钥。例如,一个输出脚本可以包含两个公钥,并要求在支出的输入脚本中放置两个相应的签名。

稍后,在第 143 页的“交易脚本和脚本语言”中,我们将详细了解脚本。目前,我们只需要理解比特币是接收到一个行为类似于公钥的输出脚本,并且比特币的支出是由一个行为类似于签名的输入脚本授权的。

IP 地址:比特币的原始地址(P2PK)

我们已经确定,Alice 可以通过将她的一些比特币分配给 Bob 的一个公钥来支付 Bob。但是,Alice 如何获得 Bob 的一个公钥呢?Bob 可以直接给她一份副本,但让我们再次看一下我们在“公钥”中使用的公钥。注意它非常长。想象一下 Bob 试图通过电话向 Alice 读取公钥的情景:

x = F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A&## x20;

y = 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB

在比特币软件的早期版本中,允许支付者输入接收者的 IP 地址,如图 4-5 所示。这个功能后来被移除了——使用 IP 地址存在很多问题——但对它的快速描述将有助于我们更好地理解为什么某些特性可能被添加到比特币协议中。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.5.png" alt=""><figcaption><p>图 4-5. 早期通过互联网档案馆发送比特币的截图。</p></figcaption></figure>

如果Alice在比特币0.1中输入了Bob的IP地址,她的全节点将与他的全节点建立连接,并从Bob的钱包接收一个新的公钥,这个公钥以前从未给过任何人。这个新的公钥很重要,以确保支付给Bob的不同交易不能被某人通过查看区块链连接在一起,因为他们注意到所有交易都支付给了相同的公钥。

\ 使用她的节点从Bob的节点接收的公钥,Alice的钱包将构造一个非常简单的输出脚本来支付:

\<Bob's public key> OP_CHECKSIG

Bob稍后可以使用完全由他的签名组成的输入脚本来花费该输出:

\<Bob's signature>

为了弄清楚输出和输入脚本的作用,您可以将它们组合在一起(输入脚本在前),然后注意每个数据片段(用尖括号显示)都被放置在一个名为堆栈的项目列表的顶部。当遇到操作码(opcode)时,它使用来自堆栈的项目,从最顶部的项目开始。让我们看看从组合脚本开始是如何工作的:

\<Bob's signature> \<Bob's public key> OP_CHECKSIG

\ 对于这个脚本,Bob的签名被放置在堆栈上,然后Bob的公钥被放置在签名上面。OP_CHECKSIG操作消耗两个元素,从公钥开始,接着是签名,将它们从堆栈中移除。它验证签名是否对应于公钥,并且对交易中的各个字段进行签名。如果签名正确,OP_CHECKSIG会用值1替换自身在堆栈上的位置;如果签名不正确,它会用0替换自身。如果在评估结束时堆栈顶部有一个非零项,则脚本通过。如果交易中的所有脚本都通过,并且交易的所有其他细节都有效,则完整节点将认为该交易有效。

简而言之,前面的脚本使用了原始论文中描述的相同公钥和签名,但增加了两个脚本字段和一个操作码的复杂性。这似乎是多余的工作,但当我们看到接下来的部分时,我们将开始看到它的好处。

这种类型的输出今天被称为付给公钥,或简称为P2PK。它从未被广泛用于支付,而且几乎十年来没有任何广泛使用的程序支持IP地址支付。

历史遗留地址P2PKH

为要支付的人输入IP地址有很多优点,但也有很多缺点。一个特别的缺点是接收者需要他们的钱包在线并在其IP地址上可访问。对于很多人来说,这不是一个选择。他们晚上关闭计算机,他们的笔记本电脑进入睡眠模式,他们处于防火墙后面,或者他们正在使用网络地址转换(NAT)。

这让我们回到了接收者(如Bob)不得不向发送者(如Alice)提供一个较长的公钥的问题。早期比特币开发者所知的最短版本的比特币公钥是65字节,用十六进制表示时相当于130个字符。然而,比特币已经包含了几个比65字节大得多的数据结构,需要在比特币的其他部分中以最少的数据安全地引用。

比特币通过哈希函数实现了这一点,哈希函数是一种将潜在大量数据混淆(哈希)并输出固定量数据的函数。当给定相同的输入时,加密哈希函数总是产生相同的输出,并且安全函数还会使得别人很难选择不同的输入以产生先前看到的输出。这使得输出成为对输入的承诺。实际上,这是一种承诺,只有输入x会产生输出X。

例如,假设我想问你一个问题,并且以一种你无法立即阅读的形式给出我的答案。假设问题是,“中本聪什么时候开始研发比特币?”我会以SHA256哈希函数的输出形式给你我的答案的承诺,这是比特币中最常用的函数:

94d7a772612c8f2f2ec609d41f5bd3d04a5aa1dfe3582f04af517d396a302e4e

稍后,当你告诉我你对问题答案的猜测后,我可以揭示我的答案,并向你证明我的答案作为哈希函数的输入产生了与我之前给你的完全相同的输出:

$ echo "2007. He said about a year and a half before Oct 2008" | sha256sum 94d7a772612c8f2f2ec609d41f5bd3d04a5aa1dfe3582f04af517d396a302e4e

现在想象一下,我们向 Bob 提出问题:“你的公钥是什么?” Bob 可以使用哈希函数为他的公钥提供一个具有密码学安全性的承诺。如果他后来透露了他的密钥,而我们验证它产生了与他先前给我们的完全相同的承诺,那么我们可以确定它是用于创建早期承诺的确切相同的密钥。

SHA256 哈希函数被认为非常安全,并产生 256 位(32 字节)的输出,不到原始比特币公钥大小的一半。然而,还有其他略微不太安全的哈希函数可以产生较小的输出,比如 RIPEMD-160 哈希函数,其输出为 160 位(20 字节)。由于 Satoshi Nakamoto 从未说明原因,比特币的原始版本通过首先使用 SHA256 对密钥进行哈希,然后使用 RIPEMD-160 对该输出进行哈希,从而对公钥进行承诺;这产生了对公钥的 20 字节承诺。

我们可以从算法的角度来看。从公钥 K 开始,我们计算 SHA256 哈希,然后计算结果的 RIPEMD-160 哈希,得到一个 160 位(20 字节)的数字:

A = RIPEMD160(SHA256(K))

现在我们知道如何对公钥做出承诺了,接下来我们需要弄清楚如何在交易中使用它。考虑以下输出脚本:

OP_DUP OP_HASH160 \<Bob's commitment> OP_EQUAL OP_CHECKSIG

以及以下输入脚本:

\<Bob's signature> \<Bob's public key>

它们组合在一起形成以下脚本:

OP_DUP OP_HASH160 OP_EQUALVERIFY OP_CHECKSIG

正如我们在“IP地址:比特币的原始地址(P2PK)”上一页所做的那样,我们开始将项目放在堆栈上。 首先放入Bob的签名,然后放入他的公钥。 OP_DUP操作复制顶部项目,因此堆栈顶部和次顶部现在都是Bob的公钥。 OP_HASH160操作消耗(删除)顶部公钥,并用RIPEMD160(SHA256(K))对其进行哈希处理,因此现在堆栈顶部是Bob的公钥的哈希。 接下来,将Bob的公钥的承诺添加到堆栈的顶部。 OP_EQUALVERIFY操作消耗了顶部两个项目,并验证它们是否相等; 如果Bob在输入脚本中提供的公钥与Alice支付中用于创建承诺的公钥相同,那么情况应该是如此。 如果OP_EQUALVERIFY失败,则整个脚本失败。 最后,我们得到的是一个仅包含Bob的签名和他的公钥的堆栈; OP_CHECKSIG操作码验证它们是否相互对应,并且签名是否承诺了交易。&## x20;

虽然这种支付给公钥哈希(P2PKH)的过程可能看起来有些复杂,但它允许Alice的付款只包含对他的公钥的20字节承诺,而不是公钥本身,在比特币的原始版本中会是65字节。 这使得Bob不必与Alice交流太多的数据。&## x20;

但是,我们还没有讨论Bob如何从他的比特币钱包将这20个字节传递给Alice的钱包。 字节值有常用的编码,例如十六进制,但如果在复制承诺时出现任何错误,将导致比特币被发送到不可支配的输出,从而永远丢失。 在下一节中,我们将介绍紧凑编码和可靠的校验和。

Base58check编码

为了以紧凑的方式表示长数字,使用较少的符号,许多计算机系统使用基数高于10的混合字母数字表示。例如,传统的十进制系统使用10个数字,0到9,而十六进制系统使用16个数字,包括字母A到F作为另外六个符号。以十六进制格式表示的数字比等价的十进制表示更短。更紧凑的是,base64表示法使用26个小写字母,26个大写字母,10个数字,以及两个更多的字符,如“+”和“/”,用于在基于文本的媒体(例如电子邮件)上传输二进制数据。

Base58是与base64类似的编码,使用大写和小写字母以及数字,但省略了一些经常被误认为相同并且在某些字体中显示时看起来相同的字符。具体来说,base58是base64去掉了数字0,大写字母O,小写字母l,大写字母I以及符号“+”和“/”。或者更简单地说,它是一个不包含刚提到的四个字符(0、O、l、I)的小写字母、大写字母和数字集合。示例4-2显示了完整的base58字母表。

示例4-2. 比特币的base58字母表

123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz

为了增加额外的安全性,防止输入错误或转录错误,base58check在base58字母表中包含了一个校验和。校验和是添加到正在编码的数据末尾的额外的四个字节。校验和是从编码数据的哈希派生出来的,因此可以用来检测转录和输入错误。当提供base58check代码时,解码软件将计算数据的校验和,并将其与代码中包含的校验和进行比较。如果两者不匹配,则引入了错误,base58check数据无效。这可以防止被误输入的比特币地址被钱包软件接受为有效的目标地址,否则会导致资金损失。

要将数据(一个数字)转换为base58check格式,首先我们要为数据添加一个前缀,称为“版本字节”,它用于轻松识别所编码的数据类型。例如,前缀零(十六进制中的0x00)表示数据应该用作传统P2PKH输出脚本中的承诺(哈希)。常见版本前缀列表如表4-1所示:

接下来,我们计算“双SHA”校验和,意思是我们在之前的结果(前缀与数据连接起来)上两次应用SHA256哈希算法:

checksum = SHA256(SHA256(prefix||data))

从结果得到的32字节哈希(哈希的哈希)中,我们只取前四个字节。这四个字节用作错误检查码或校验和。校验和附加到末尾。

结果由三个部分组成:一个前缀、数据和一个校验和。然后,使用先前描述的base58字母表对此结果进行编码。图4-6说明了base58check编码过程。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.6.png" alt=""><figcaption><p>图 4-6. Base58check编码:一种用于明确编码比特币数据的基于Base58、带版本和校验和的格式。</p></figcaption></figure>

在比特币中,除了公钥承诺之外,其他数据也以base58check编码的方式呈现给用户,以使数据紧凑、易于阅读和易于检测错误。base58check编码中的版本前缀用于创建易于区分的格式,当以base58编码时,在base58check编码的有效负载的开头包含特定的字符。这些字符使人们能够轻松识别编码的数据类型以及如何使用它。例如,以1开头的base58check编码的比特币地址与以5开头的base58check编码的私钥钱包导入格式(WIF)是有区别的。示例版本前缀及其相应的base58字符如下表4-1所示:

表4-1. Base58check版本前缀和编码结果示例

<table><thead><tr><th width="393">类型</th><th>版本前缀(十六进制)</th><th>Base58 结果前缀</th></tr></thead><tbody><tr><td>Address for pay to public key hash (P2PKH)</td><td>0x00</td><td>1</td></tr><tr><td>Address for pay to script hash (P2SH)</td><td>0x05</td><td>3</td></tr><tr><td>Testnet Address for P2PKH</td><td>0x6F</td><td>m or n</td></tr><tr><td>Testnet Address for P2SH</td><td>0xC4</td><td>2</td></tr><tr><td>Private Key WIF</td><td>0x80</td><td>5, K, or L</td></tr><tr><td>BIP32 Extended Public Key</td><td>0x0488B21E</td><td>xpub</td></tr></tbody></table>

将公钥与基于哈希的承诺和base58check编码相结合,图4-7说明了将公钥转换为比特币地址的过程。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.7.png" alt=""><figcaption><p>图 4-7. 公钥到比特币地址:将公钥转换为比特币地址的过程</p></figcaption></figure>

压缩公钥

当比特币刚被设计时,其开发者只知道如何创建 65 字节的公钥。然而,后来的一个开发者发现了一种只使用 33 字节的替代编码公钥的方法,而且这种方法与当时所有的比特币全节点都是向后兼容的,因此不需要改变比特币协议。这些 33 字节的公钥被称为压缩公钥,而原始的 65 字节的公钥被称为非压缩公钥。使用更小的公钥可以减小交易的大小,允许在同一个区块中进行更多的支付。&## x20;

正如我们在第 59 页的“公钥”部分中看到的那样,公钥是椭圆曲线上的一个点 (x, y)。因为曲线表示一个数学函数,曲线上的一个点代表了方程的一个解,因此如果我们知道 x 坐标,我们可以通过求解方程 y^2 mod p = (x^3 + 7) mod p 来计算出 y 坐标。这使得我们只需存储公钥点的 x 坐标,省略 y 坐标,从而减小了密钥的大小和存储它所需的空间,节省了 256 位的空间。在每个交易中几乎减小了 50% 的大小,随着时间的推移,节省了大量的数据!

&## x20;以下是我们在第 59 页的“公钥”部分创建的私钥生成的公钥:

x = F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A&## x20;

y = 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB

同样的公钥可以表示为一个 520 位的数(130 个十六进制数字),带有前缀 04,后面跟着 x 和 y 坐标,格式为 04 x y:

K = 04F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A\\ 07CF33DA18BD734C600B96A72BBC4749D5141C90EC8AC328AE52DDFE2E505BDB

与未压缩的公钥以 04 作为前缀不同,压缩的公钥以 02 或 03 作为前缀。让我们看看为什么会有两个可能的前缀:因为等式的左侧是 y2,所以 y 的解是一个平方根,可以有正值或负值。从视觉上看,这意味着得到的 y 坐标可以在 x 轴上方或下方。正如您从图 4-2 中的椭圆曲线图中所看到的那样,曲线是对称的,这意味着它像镜子一样被 x 轴反射。因此,虽然我们可以省略 y 坐标,但我们必须存储 y 的符号(正或负);换句话说,我们必须记住它是否在 x 轴上方或下方,因为这两种选项代表不同的点和不同的公钥。在二进制算术上计算素数阶 p 的有限域上的椭圆曲线时,y 坐标要么是偶数,要么是奇数,这对应于前面解释的正负号。因此,为了区分 y 的两个可能值,如果 y 是偶数,则存储带有前缀 02 的压缩公钥,如果 y 是奇数,则存储带有前缀 03 的压缩公钥,从而使软件能够从 x 坐标推断出 y 坐标,并将公钥展开为点的完整坐标。公钥压缩如图 4-8 所示。

<figure><img src="https://img.learnblockchain.cn/masterbitcoin3/assets/4.8.png" alt=""><figcaption><p>图 4-8. 公钥压缩</p></figcaption></figure>

这里是与在第59页生成的相同的公钥,显示为压缩的公钥,以264位(66个十六进制数字)存储,并带有前缀03,表示y坐标为奇数:

K = 03F028892BAD7ED57D2FB57BF33081D5CFCF6F9ED3D3D7F159C2E2FFF579DC341A

这个压缩的公钥对应于相同的私钥,意味着它是由相同的私钥生成的。然而,它与未压缩的公钥看起来不同。更重要的是,如果我们将这个压缩的公钥转换为一个承诺,使用HASH160函数(RIPEMD160(SHA256(K))),它将产生一个不同的承诺,而不是未压缩的公钥,从而导致不同的地址。这可能会让人困惑,因为这意味着一个单一的私钥可以产生以两种不同格式(压缩和未压缩)表示的公钥,从而产生两个不同的比特币地址。然而,私钥对于两个比特币地址是相同的。

几乎所有比特币软件现在默认使用压缩的公钥,并且在稍后的协议升级中添加了某些新功能时需要使用压缩的公钥。

然而,一些软件仍然需要支持未压缩的公钥,比如从旧钱包导入私钥的钱包应用程序。当新钱包扫描区块链以查找旧的P2PKH输出和输入时,它需要知道是扫描65字节的密钥(以及这些密钥的承诺)还是33字节的密钥(以及它们的承诺)。如果没有扫描正确的类型,用户可能无法花费他们的全部余额。为了解决这个问题,当私钥从钱包中导出时,在新的比特币钱包中使用的WIF有所不同,以指示这些私钥已被用于生成压缩的公钥。

历史遗留的支付到脚本哈希地址(P2SH)

正如我们在前面的章节中所看到的,接收比特币的人(比如Bob)可以要求支付给他的比特币在其输出脚本中包含某些约束条件。当Bob花费这些比特币时,他将需要使用输入脚本来满足这些约束条件。在“IP地址:比特币的原始地址(P2PK)”中,约束条件简单地是输入脚本需要提供适当的签名。在“用于P2PKH的遗留地址”中,还需要提供适当的公钥。

对于支付给Bob的输出脚本中放置Bob想要的约束条件的支出者(比如Alice),Bob需要将这些约束条件传达给她。这类似于Bob需要将他的公钥传达给她的问题。就像这个问题一样,公钥可以相当大,Bob使用的约束条件也可能非常大——潜在地有数千字节。这不仅是需要传达给Alice的数千字节,而且是她每次想向Bob支付款项时都需要支付交易费用的数千字节。然而,使用哈希函数为大量数据创建小承诺的解决方案在这里也适用。

2012年比特币协议的BIP16升级允许输出脚本承诺一个赎回脚本(redeem script)。当Bob花费他的比特币时,他的输入脚本需要提供一个与承诺匹配的赎回脚本,以及满足赎回脚本所需的任何数据(例如签名)。让我们首先想象一下,Bob想要要求两个签名来花费他的比特币,一个来自他的桌面钱包,另一个来自硬件签名设备。他将这些条件放入一个赎回脚本中:

\<public key 1> OP_CHECKSIGVERIFY \<public key 2> OP_CHECKSIG

然后,他使用与P2PKH承诺相同的HASH160机制创建对赎回脚本的承诺,即RIPEMD160(SHA256(脚本))。该承诺被放置到输出脚本中,使用特殊的模板:

OP_HASH160 OP_EQUAL

特别注意:使用支付至脚本哈希(P2SH)时,必须使用特定的P2SH模板,在输出脚本中没有额外的数据或条件。如果输出脚本不是完全是 OP_HASH160 <20字节> OP_EQUAL,赎回脚本将不会被使用,而且任何比特币可能要么无法支配,要么可以被任何人支配(意味着任何人都可以取走它们)。

当Bob要花费他收到的支付,用于他脚本的承诺时,他会使用一个包含赎回脚本的输入脚本,将其序列化为一个单独的数据元素。他还提供了满足赎回脚本所需的签名,按照它们被操作码消耗的顺序放置:

\<signature2> \<signature1> \<redeem script>

当比特币全节点接收到Bob的交易时,它们将验证序列化的赎回脚本是否会哈希为与承诺相同的值。然后,它们将其替换为堆栈上的反序列化值:

\<signature2> \<signature1> \<pubkey1> OP_CHECKSIGVERIFY \<pubkey2> OP_CHECKSIG

脚本被执行,如果通过并且所有其他交易细节都正确,则交易有效。&## x20;

P2SH的地址也使用base58check创建。版本前缀设置为5,这会导致编码地址以3开头。P2SH地址的示例是3F6i6kwkevjR7AsAd4te2YB2zZyASEm1HM。

注意:P2SH不一定等同于多重签名交易。P2SH地址通常代表一个多重签名脚本,但也可能代表编码其他类型交易的脚本。

P2PKH和P2SH是使用base58check编码的仅有两种脚本模板。它们现在被称为传统地址,并且随着时间的推移变得越来越不常见。传统地址已经被bech32地址家族所取代。

P2SH碰撞攻击

基于哈希函数的所有地址理论上都容易受到攻击者独立找到产生哈希函数输出(承诺)的相同输入的影响。在比特币的情况下,如果攻击者以与原始用户相同的方式找到输入,他们将知道用户的私钥并能够花费该用户的比特币。攻击者独立生成现有承诺的输入的机会与哈希算法的强度成正比。对于像HASH160这样的安全160位算法,这种可能性是1/2^160。这是一种原像攻击。&## x20;

攻击者还可以尝试生成两个不同的输入(例如,赎回脚本),这些输入会产生相同的承诺。对于完全由单方创建的地址,攻击者生成现有承诺的不同输入的机会也大约是1/2^160,对于HASH160算法而言也是如此。这是二阶原像攻击。&## x20;

然而,当攻击者能够影响原始输入值时情况就会改变。例如,攻击者参与了多方签名脚本的创建,在这种情况下,他们在了解所有其他参与方的公钥之后不需要提交自己的公钥。在这种情况下,哈希算法的强度降低到其平方根。对于HASH160,概率变为1/2^80。这是一种碰撞攻击。 为了将这些数字放入上下文,截至2023年初,所有比特币矿工每小时执行大约2^80个哈希函数。他们运行与HASH160不同的哈希函数,因此他们现有的硬件无法为其创建碰撞攻击,但比特币网络的存在证明了对HASH160等160位函数的碰撞攻击是切实可行的。比特币矿工已经花费了数十亿美元的特殊硬件,因此创建碰撞攻击不会很便宜,但有些组织预计将获得数十亿美元的比特币到与多方参与的过程相关的地址,这可能会使攻击变得有利可图。 有着成熟的密码协议用于预防碰撞攻击,但一个简单的解决方案,不需要钱包开发人员具有任何特殊的知识,就是简单地使用更强大的哈希函数。比特币的后续升级使这成为可能,新的比特币地址提供了至少128位的碰撞抵抗能力。执行2^128次哈希操作将需要所有当前的比特币矿工大约320亿年。&## x20;

尽管我们认为没有任何立即威胁到任何人创建新的P2SH地址,但我们建议所有新钱包使用更新类型的地址,以消除地址碰撞攻击的担忧。

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

0 条评论

请先 登录 后评论