用GO语言构建区块链——第五部分:地址

前言

在上一篇文章中,我们开始初步实现交易,并且还了解了交易的非个人性质:没有用户帐户,您的个人数据(例如,姓名,护照号码或SSN)不是必需的,也不存储在比特币的任何地方。但必须存在一些东西可以确定您是交易输出的所有者(即您是锁定在这些输出上的硬币的所有者),这就是比特币需要的地址。到目前为止,我们已经使用任意用户定义的字符串作为地址,现在我们将要实现一个跟比特币一样的真实地址。

这部分介绍了重要的代码更改,因此在这里解释所有这些都没有意义。请参阅此页面以查看自上一篇文章以来的所有更改。

比特币地址

以下是比特币地址的示例:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。这是第一个比特币地址,据称属于Satoshi Nakamoto,比特币地址是公开的。如果您想向某人发送硬币,您需要知道他们的地址。但地址(尽管是独一无二的)并不能确定您是“钱包”的拥有者。实际上,所谓的地址,只不过是将公钥表示成人类可读的形式而已,因为原生的公钥人类很难阅读。在比特币中,你的身份(identity)就是一对(或者多对)保存在你的电脑(或者你能够获取到的地方)上的公钥(public key)和私钥(private key)。比特币基于一些加密算法的组合来创建这些密钥,并且保证了在这个世界上没有其他人能够取走你的币,除非拿到你的密钥。下面,让我们来讨论一下这些算法到底是什么。

公钥加密

公钥加密算法使用密钥对:公钥和私钥。公钥并不敏感,可以向任何人透露。相反,私钥不可被公开:除了所有者之外,没有人可以访问它们,因为它是用作所有者标识符的私钥。在加密货币的世界中,你的私钥代表的就是你,私钥就是一切。

本质上,比特币钱包也只不过是这样的密钥对而已。当您安装钱包应用程序或使用比特币客户端生成新地址时,会为您生成一对密钥。控制私钥的人即控制在比特币中发送给该密钥的所有硬币。

私钥和公钥只是字节的随机序列,因此它们不能在屏幕上打印也无法由人读取。这就是比特币使用算法将公钥转换为人类可读字符串的原因。

如果你曾经使用过比特币钱包应用程序,很可能会为你生成一个助记符密码短语。使用这些短语代替私钥,并且可以用于生成它们。该机制在BIP-039中实现。

好的,我们现在知道用比特币识别用户的内容了。但比特币如何检查交易输出(以及存储在其上的硬币)的所有权?

数字签名

在数学和密码学中,有一个数字签名的概念,算法可以保证:

1.  从发件人转移到收件人时,数据未被修改;

2.  该数据是由某个发件人创建的;

3.  发件人不能拒绝发送数据的事实。

通过对数据应用签名算法(即,对数据进行签名),可以获得签名,稍后可以对其进行验证。使用私钥进行数字签名,验证需要公钥。

为了签署数据,我们需要以下内容:

1.  要签署的数据;

2.  私钥。

应用签名算法可以生成一个签名,并且这个签名会被存储在交易输入中。为了对一个签名进行验证,我们需要以下三样东西:

1.  已签署的数据;

2.  签名;

3.  公钥。

简单来说,验证过程可以描述为:检查签名是由被签名数据加上私钥得来,并且公钥恰好是由该私钥生成。

数字签名不是加密,您无法从签名重建数据。这与散列类似:您通过散列算法运行数据并获取数据的唯一表示。签名和散列之间的区别是密钥对:它们使签名验证成为可能。但密钥对也可用于加密数据:私钥用于加密,公钥用于解密数据。比特币不使用加密算法。

比特币中的每个交易输入都由创建交易的人签名。比特币中的每笔交易必须经过验证才能进入区块。验证手段(除了其他程序):

1.  检查输入是否有权使用先前事务的输出。

2.  检查事务签名是否正确。

如图,签署数据和验证签名的过程看起来像这样:

用GO语言构建区块链——第五部分:地址

现在让我们回顾一下交易的整个生命周期:

1.  最初,有一个包含coinbase交易的创世块。在coinbase交易中没有真正的输入,因此不需要签名。coinbase事务的输出包含散列公钥(使用RIPEMD16(SHA256(PubKey))算法)。

2.  当人们发送硬币时,就会创建一个交易。交易的输入将参考先前交易的输出。每个输入都将存储一个公钥(不是哈希)和整个交易的签名。

3.  比特币网络中接收交易的其他节点将对其进行验证。除了其他事情,他们还会检查:输入中公钥的哈希值与引用输出的哈希值匹配(这可以确保发送者只花费属于它们的硬币); 签名是正确的(这确保了交易是由硬币的真正所有者创建的)。

4.  当矿工节点准备挖掘新块时,它会将交易放入块中并开始挖掘它。

5.  当新块被挖掘时,网络中的每个其他节点都会收到一条消息,说明该块已被挖掘,并将该块添加到区块链中。

6.  将块添加到区块链后,交易完成,其输出可在新事务中引用。

椭圆曲线加密

如上所述,公钥和私钥是随机字节序列。由于它是用于识别硬币所有者的私钥,因此存在必需条件:随机性算法必须产生真正的随机字节。我们不希望意外生成其他人拥有的私钥。

比特币使用椭圆曲线生成私钥。椭圆曲线是一个复杂的数学概念,我们在这里不会详细解释(如果你很好奇,请查看http://andrea.corbellini.name/2015/05/17/elliptic-curve-cryptography-a-gentle-introduction/)。我们需要知道的是,这些曲线可用于生成非常大且随机的数字。比特币使用的曲线可以随机选取一个介于0和2²⁵⁶之间的数字(当可见宇宙中有10⁷⁸到 10⁸²之间的原子时)。如此巨大的上限意味着几乎不可能两次生成相同的私钥。

此外,比特币使用ECDSA(椭圆曲线数字签名算法)算法来签署交易。

Base58

现在让我们回到上面提到的比特币地址:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。现在我们知道这是一个人类可读的公钥表示。如果我们解码它,这就是公钥的样子(作为十六进制系统中写入的字节序列):

用GO语言构建区块链——第五部分:地址

比特币使用Base58算法将公钥转换为人类可读格式。该算法与著名的Base64非常相似,但它使用较短的字母表:从字母表中删除了一些字母,以避免一些使用字母相似性的攻击。因此,没有这些符号:0(零),O(大写o),I(大写i),l(小写L),因为它们看起来相似。此外,没有+和/符号。

让我们示意地想象从公钥获取地址的过程:

用GO语言构建区块链——第五部分:地址

因此,上述解码公钥由三部分组成:

用GO语言构建区块链——第五部分:地址

由于散列函数是一种方式(即,它们不能被反转),因此无法从散列中提取公钥。但是我们可以通过运行它来检查是否使用公钥来获取哈希,以及保存哈希函数和比较哈希值。

好的,现在我们已经完成了所有的部分,让我们编写一些代码。当用代码编写时,一些概念应该更清楚。

实施地址

我们将从Wallet结构开始:

用GO语言构建区块链——第五部分:地址

钱包只不过是一个密钥。我们还需要这种Wallets类型来保存多个钱包集合,将它们保存到文件中,然后从中加载它们。在Wallet生成新密钥对的构造函数中。newKeyPair函数很简单:ECDSA是基于椭圆曲线,所以我们需要一个椭圆曲线。接下来,使用该曲线生成私钥,并且从私钥生成公钥。需要注意的一点是:在基于椭圆曲线的算法中,公钥是曲线上的点。因此,公钥是X,Y坐标的组合。在比特币中,这些坐标被连接起来并形成公钥。

现在,让我们生成一个地址:

用GO语言构建区块链——第五部分:地址

以下是将公钥转换为Base58地址的步骤:

1.  使用公钥并使用RIPEMD160(SHA256(PubKey))散列算法将其哈希两次。

2.  将地址生成算法的版本添加到哈希。

3.  通过散列步骤2的结果来计算校验和SHA256(SHA256(payload))。校验和是结果哈希的前四个字节。

4.  将校验和附加到version+PubKeyHash组合。

5.   version+PubKeyHash+checksum使用Base58 对组合进行编码。

因此,您将获得一个真正的比特币地址,您甚至可以在blockchain.info上查看其余额。但我可以向您保证,无论您生成新地址多少次并检查其余额,余额为0。这就是选择适当的公钥加密算法至关重要的原因:考虑私钥是随机数,生成相同数字的机会必须尽可能低。理想情况下,它必须低到“永远”不会重复。

另外,请注意您不需要连接到比特币节点来获取地址。地址生成算法利用在许多编程语言和库中实现的开放算法的组合。

现在我们需要修改它们的输入和输出以使用地址:

用GO语言构建区块链——第五部分:地址

请注意,我们不再使用ScriptPubKeyScriptSig字段,因为我们不会实现脚本语言。相反,ScriptSig它被拆分为SignaturePubKey字段,ScriptPubKey并重命名为PubKeyHash。我们将实现与比特币相同的输出锁定/解锁和输入签名逻辑,但我们将在方法中执行此操作。

UsesKey方法检查输入是否使用特定键来解锁输出。请注意,输入存储原始公钥,但该函数采用哈希值。IsLockedWithKey检查是否提供了公钥哈希用于锁定输出。这是一个UsesKey的辅助函数,它们都用于在FindUnspentTransactions交易之间建立连接。

Lock只需锁定输出。当我们向某人发送硬币时,我们只知道他们的地址,因此该函数将地址作为唯一的参数。然后解码该地址,并从中提取公钥哈希并保存在PubKeyHash字段中。

现在,让我们检查一切是否正常:

用GO语言构建区块链——第五部分:地址

太好了!现在让我们实现交易签名。

实施签名

必须签署交易,因为这是比特币里面保证发送方不会花费属于其他人的币的唯一方式。如果签名无效,则该交易也被视为无效,因此无法添加到区块链中。

我们拥有实现交易签名的所有部分,还差一件事:要签署的数据。交易的哪些部分实际签署了?或者一项交易是否作为一个整体签署?选择要签名的数据非常重要。问题是要签名的数据必须包含以独特方式标识数据的信息。例如,仅签署输出值是没有意义的,因为此类签名不会考虑发件人和收件人。

考虑到交易解锁以前的输出,重新分配它们的值并锁定新输出,必须签署以下数据:

1.  公钥哈希存储在未锁定的输出中。这标识了交易的“发件人”。

2.  公钥哈希存储在新的锁定输出中。这标识了交易的“收件人”。

      新的输出值。

在比特币,锁定/解锁逻辑被存储在脚本,它们被分别存储在输入和输出的 ScriptSig 和 ScriptPubKey 字段。由于比特币允许不同类型的此类脚本,因此它标记了整个内容ScriptPubKey

如您所见,我们不需要对存储在输入中的公钥进行签名。因此,在比特币中,它不是一个已签名的交易,而是一个去除部分内容的输入副本,存储ScriptPubKey来自参考输出。

获取修剪后的交易副本的详细过程在这里. 虽然它可能已经过时了,但是我并没有找到另一个更可靠的来源。

好吧,它看起来很复杂,所以让我们开始编码。我们将从Sign方法开始:

用GO语言构建区块链——第五部分:地址

该方法采用私钥和先前事务的map。如上所述,为了对事务进行签名,我们需要访问事务输入中引用的输出,因此我们需要存储这些输出的交易。

让我们一步一步地分析这个方法:

用GO语言构建区块链——第五部分:地址

Coinbase交易未签名,因为它们中没有实际输入。

用GO语言构建区块链——第五部分:地址

修剪后的交易副本将被签署,而非完整交易:

用GO语言构建区块链——第五部分:地址

该副本将包括所有的输入和输出,但TXInput.SignatureTXInput.PubKey设置为零。

接下来,我们浏览副本中的每个输入:

用GO语言构建区块链——第五部分:地址

在每个输入中,Signature设置为nil(只是一个双重检查),PubKey设置为PubKeyHash引用输出的。此时,除当前事务之外的所有事务都是“空”,即它们SignaturePubKey字段设置为nil。因此,输入是单独签名的,虽然这对我们的应用程序来说不是必需的,但比特币允许事务包含引用不同地址的输入。

用GO语言构建区块链——第五部分:地址

Hash方法将事务序列化并使用SHA-256算法对其进行哈希处理,结果哈希是我们要签名的数据。获取哈希后我们应该重置PubKey字段,以便它不会影响进一步的迭代。

现在,关键是:

用GO语言构建区块链——第五部分:地址

我们通过 privKey 对 txCopy.ID 进行签名。ECDSA签名是一对数字,我们将其连接并存储在输入Signature字段中。

现在,验证功能:

用GO语言构建区块链——第五部分:地址

该方法非常简单。首先,我们需要相同的交易副本:

用GO语言构建区块链——第五部分:地址

接下来,我们需要用于生成密钥对的相同曲线:

用GO语言构建区块链——第五部分:地址

接下来,我们检查每个输入中的签名:

用GO语言构建区块链——第五部分:地址

此部分与Sign方法中的部分相同,因为在验证期间,我们需要签署的相同数据。

用GO语言构建区块链——第五部分:地址

这里我们解压缩存储在TXInput.Signature和中的值TXInput.PubKey,因为签名是一对数字而公钥是一对坐标。我们之前将它们连接起来进行存储,现在我们需要将它们解压缩才能在crypto/ecdsa函数中使用。

用GO语言构建区块链——第五部分:地址

这是:我们创建一个ecdsa.PublicKey使用从输入中提取的公钥并执行ecdsa.Verify传递从输入中提取的签名。如果验证了所有输入,则返回true; 如果至少有一个输入验证失败,则返回false。

现在,我们需要一个函数来获取以前的事务。由于这需要与区块链进行交互,我们将使其放在Blockchain的方法里

用GO语言构建区块链——第五部分:地址

这些函数很简单:FindTransaction按ID查找交易(这需要迭代区块链中的所有块); SignTransaction接受一个交易,查找它引用的事务并签名; VerifyTransaction做同样的事情,但改为验证交易。

现在,我们需要实际签署和验证交易。签名发生在NewUTXOTransaction

用GO语言构建区块链——第五部分:地址

在将交易放入块之前进行验证:

用GO语言构建区块链——第五部分:地址

就是这样!让我们再一次检查一切:

用GO语言构建区块链——第五部分:地址

一切正常,真棒!

我们还要注释掉bc.SignTransaction(&tx, wallet.PrivateKey)调用NewUTXOTransaction以确保无法挖掘未签名的交易:

用GO语言构建区块链——第五部分:地址

结论

到目前为止我们真的很棒,实现了比特币的许多关键功能!我们已经实现了几乎所有网络外的东西,在下一部分,我们将完成交易。

编辑:却原来

本文系转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们。