网站外链发布平台,媒体发稿平台以资源+策略+技术的方式让品牌传播更简单高效!

Go实现区块链(五)---地址

作者:jcmp      发布时间:2021-05-16      浏览量:0
1.前言上一章我们实现了交易,到目前为止

1.前言

上一章我们实现了交易,到目前为止,我们已经使用任意用户定义的字符串作为地址,比特币的账户地址其实就是用户公钥经过一系列 Hash ( HASH 160,或先进行 SHA256, 然后进行 RIPEMD160 )及编码运算后生成的 160 位( 20 字节)的字符串。

2.知识点

知识点 学习网页 特性 Public-key Public-key cryptography 一对加密密钥与解密密钥 Digital signatures 数字签名 、 快速理解签名 1、 可靠性:签名接受者可以验证签名是否为签名者所签 2、 不可伪造:只有签名者可以生成自己独有的签名 3、 不可重复使用:签名文件包含的信息不会对其他文档进行签名 4、 不可否认性:签名者在任何时候不能否认自己的签名 Elliptic curve 椭圆曲线 下面有介绍 Address 比特币地址 见下面的图 Base58 base58 去掉0(零),O(大写o),I(大写i),l(小写L),因为它们看起来相似。此外,没有+和/符号。

3.重要概念与知识理解

比特币地址:

比特币采用了非对称的加密算法,用户自己保留私钥,对自己发出的交易进行签名确 认,并公开公钥。 比特币的账户地址其实就是用户公钥经过一系列 Hash ( HASH 160,或先进行 SHA256, 然后进行 RIPEMD160 )及编码运算后生成的 160 位( 20 字节)的字符串。 一般地,对账户地址串进行 Base58Check 编码,并添加前导字节(表明支持哪种脚本) 和 4 字节校验字节,以提高可读性和准确性。

公钥加密算法:

公钥加密算法使用成对的密钥:公钥和私钥。公钥是不敏感的,可以向任何人公开。与此相反,私钥不应该被公开:除了所有者之外,没有人可以访问它们,因为它是作为所有者标识符的私钥。你是你的私钥(当然,在密码货币的世界里)。

从本质上讲,比特币钱包就是一对这样的密钥。当您安装一个钱包应用程序或使用一个比特币客户端来生成一个新地址时,将为您生成一对密钥。控制私钥的人控制着所有比特币的比特币。

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

好了,现在我们知道了什么是比特币的用户。但是,比特币如何检查交易输出的所有权(以及储存在它们上面的信息)?

数字签名:

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

签名的操作会产生一个签名,它存储在事务输入中。为了验证签名,需要以下操作:

简单地说,验证过程可以被描述为:检查这个签名是从这个数据中获得的,它带有一个用于生成公钥的私钥。

每个比特币的交易输入都由创建该交易的人签名。比特币的每一笔交易都必须经过验证,才能被放入一个区块。验证意味着(除了其他程序):

从图表上看,签名数据和验证签名的过程是这样的:

现在让我们回顾一下事务的完整生命周期:

椭圆曲线密码学(Elliptic Curve Cryptography):

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

比特币使用椭圆曲线来生成私钥。椭圆曲线是一个复杂的数学概念,我们不打算在这里详细解释(如果你很好奇,看看这个 this gentle introduction to elliptic curves :数学公式!)我们需要知道的是这些曲线可以用来产生非常大的随机数。比特币使用的曲线可以在0到2之间随机选择一个数字(大约10个,当在可见的宇宙中有10到10个原子)。如此巨大的上限意味着几乎不可能两次生成相同的私钥。

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

Base58:

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

0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93。

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

让我们从一个公共密钥的角度来设想一个地址的过程:

因此,上面提到的解码公钥包括三个部分:

Version Public key hash Checksum00 62E907B15CBF27D5425399EBF6F0FB50EBB88F18 C29B7D93。

因为哈希函数是一种方法(即它们不能被逆转),不可能从散列中提取公钥。但是,我们可以检查是否使用了公钥来获取散列,以便运行它认为save散列函数并比较散列。

好了上面的算法很多比较难理解,但知道了算法后帮助我们实现代码思路更清晰。

4.地址代码实现

Wallet:

const version = byte(0x00) //16进制0 版本号const walletFile = "db/wallet.dat"const addressChecksumLen = 4 //地址检查长度4// 钱包type Wallet struct { /** PrivateKey: ECDSA基于椭圆曲线 使用曲线生成私钥,并从私钥生成公钥 */ PrivateKey ecdsa.PrivateKey //私钥 PublicKey []byte //公钥}// 创建一个新钱包func NewWallet() *Wallet { //公钥私钥生成 private, public := newKeyPair() wallet := Wallet{private, public} return &wallet}// 得到一个钱包地址func (w Wallet) GetAddress() []byte { pubKeyHash := HashPubKey(w.PublicKey) //将版本号+pubKeyHash得到一个散列 versionedPayload := append([]byte{version}, pubKeyHash...) //校验前4个字节的散列 checksum := checksum(versionedPayload) //将校验和附加到version+PubKeyHash组合。 fullPayload := append(versionedPayload, checksum...) //BASE58得到一个钱包地址 address := utils.Base58Encode(fullPayload) return address}// 使用RIPEMD160(SHA256(PubKey))哈希算法得到hsahpubkeyfunc HashPubKey(pubKey []byte) []byte { publicSHA256 := sha256.Sum256(pubKey) RIPEMD160Hasher := ripemd160.New() _, err := RIPEMD160Hasher.Write(publicSHA256[:]) if err != nil { log.Panic(err) } publicRIPEMD160 := RIPEMD160Hasher.Sum(nil) return publicRIPEMD160}// 校验地址func ValidateAddress(address string) bool { pubKeyHash := utils.Base58Decode([]byte(address)) actualChecksum := pubKeyHash[len(pubKeyHash)-addressChecksumLen:] version := pubKeyHash[0] pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-addressChecksumLen] targetChecksum := checksum(append([]byte{version}, pubKeyHash...)) return bytes.Compare(actualChecksum, targetChecksum) == 0}//SHA256(SHA256(payload))算法返回前4个字节func checksum(payload []byte) []byte { firstSHA := sha256.Sum256(payload) secondSHA := sha256.Sum256(firstSHA[:]) return secondSHA[:addressChecksumLen]}//椭圆算法返回私钥与公钥func newKeyPair() (ecdsa.PrivateKey, []byte) { curve := elliptic.P256() //获取私钥 private, err := ecdsa.GenerateKey(curve, rand.Reader) if err != nil { log.Panic(err) } //在基于椭圆曲线的算法中,公钥是曲线上的点。因此,公钥是X,Y坐标的组合。在比特币中,这些坐标被连接起来形成一个公钥。 pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...) return *private, pubKey}

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

Wallets:

//钱包集合type Wallets struct { Wallets map[string]*Wallet //map集合}func NewWallets() (*Wallets, error) { wallets := Wallets{} wallets.Wallets = make(map[string]*Wallet) err := wallets.LoadFromFile() return &wallets, err}// 创建一个钱包func (ws *Wallets) CreateWallet() string { wallet := NewWallet() address := fmt.Sprintf("%s", wallet.GetAddress()) ws.Wallets[address] = wallet return address}// 迭代所有钱包地址返回到数组中func (ws *Wallets) GetAddresses() []string { var addresses []string for address := range ws.Wallets { addresses = append(addresses, address) } return addresses}// 获取并返回一个钱包地址func (ws Wallets) GetWallet(address string) Wallet { return *ws.Wallets[address]}// 加载钱包func (ws *Wallets) LoadFromFile() error { if _, err := os.Stat(walletFile); os.IsNotExist(err) { return err } fileContent, err := ioutil.ReadFile(walletFile) if err != nil { log.Panic(err) } var wallets Wallets gob.Register(elliptic.P256()) decoder := gob.NewDecoder(bytes.NewReader(fileContent)) err = decoder.Decode(&wallets) if err != nil { log.Panic(err) } ws.Wallets = wallets.Wallets return nil}// 保存钱包func (ws Wallets) SaveToFile() { var content bytes.Buffer gob.Register(elliptic.P256()) encoder := gob.NewEncoder(&content) err := encoder.Encode(ws) if err != nil { log.Panic(err) } err = ioutil.WriteFile(walletFile, content.Bytes(), 0644) if err != nil { log.Panic(err) }}

将生成的钱包地址保存到文件中。

交易输入:

//输入事物type TXInput struct { Txid []byte //事物hash Vout int //输出值 Signature []byte //签名 PubKey []byte //公钥}//检查输入是否使用特定的键来解锁输出func (in *TXInput) UsesKey(pubKeyHash []byte) bool { lockingHash := wallet.HashPubKey(in.PubKey) return bytes.Compare(lockingHash, pubKeyHash) == 0}

交易输出:

//一个事物输出type TXOutput struct { Value int //值 PubKeyHash []byte //解锁脚本key}// Lock只需锁定输出func (out *TXOutput) Lock(address []byte) { pubKeyHash := utils.Base58Decode(address) pubKeyHash = pubKeyHash[1 : len(pubKeyHash)-4] out.PubKeyHash = pubKeyHash}// 检查提供的公钥散列是否用于锁定输出func (out *TXOutput) IsLockedWithKey(pubKeyHash []byte) bool { return bytes.Compare(out.PubKeyHash, pubKeyHash) == 0}// 新的交易输出func NewTXOutput(value int, address string) *TXOutput { txo := &TXOutput{value, nil} txo.Lock([]byte(address)) return txo}

请注意,我们不再使用ScriptPubKey和ScriptSig领域,因为我们不打算执行的脚本语言。相反,ScriptSig被分成Signature和PubKey字段,并被ScriptPubKey重命名为PubKeyHash。我们将像比特币一样实现相同的输出锁定/解锁和输入签名逻辑,但是我们会在方法中执行此操作。

该UsesKey方法检查输入是否使用特定的键来解锁输出。请注意,输入存储原始公钥(即,不散列),但该函数需要散列一个。IsLockedWithKey检查提供的公钥散列是否用于锁定输出。这是一个补充功能UsesKey,并且它们都用于在FindUnspentTransactions事务之间建立连接。

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

我们现在创建一个钱包地址: 修改cli

//创建钱包func (cli *CLI) createWallet() { wallets, _ := wallet.NewWallets() address := wallets.CreateWallet() wallets.SaveToFile() fmt.Printf("Your new address: %s\n", address)}

执行编译命令、执行创建钱包命令:

C:\go-worke\src\github.com\study-bitcoin-go>go build github.com/study-bitcoin-goC:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createwalletYour new address: 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrBC:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go createblockchain -address 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB Mining the block containing "�╔4���tȢ�l�+�酝���.3 �"�Z\/��" Dig into mine 00000d35a6c93bfc05cefc690822a4e083cf5922d0f398f6fe975abf01530da0Done!C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go getbalance -address 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrBBalance of '13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB': 10。

5.实现签名

交易必须签署,因为这是比特币唯一能够保证不能花钱购买属于他人的硬币的方法。如果签名无效,交易也被视为无效,因此无法添加到区块链。

除了一件事:签署的数据之外,我们已经完成了所有的事务签名。交易的哪些部分实际签署了?或者一项交易是整体签署的?选择要签名的数据非常重要。问题是要签名的数据必须包含以独特方式标识数据的信息。例如,仅对输出值进行签名是没有意义的,因为此签名不会考虑发件人和收件人。

考虑到事务解锁先前的输出,重新分配其值并锁定新的输出,必须对以下数据进行签名:

正如你所看到的,我们不需要签名存储在输入中的公钥。正因为如此,在比特币中,这不是一个已签署的交易,而是其修剪后的副本,其输入存储ScriptPubKey在参考输出中。

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

func (tx *Transaction) Sign(privKey ecdsa.PrivateKey, prevTXs map[string]Transaction) { if tx.IsCoinbase() { return } txCopy := tx.TrimmedCopy() for inID, vin := range txCopy.Vin { prevTx := prevTXs[hex.EncodeToString(vin.Txid)] txCopy.Vin[inID].Signature = nil txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash txCopy.ID = txCopy.Hash() txCopy.Vin[inID].PubKey = nil r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID) signature := append(r.Bytes(), s.Bytes()...) tx.Vin[inID].Signature = signature }}

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

让我们一步一步回顾这个方法:

if tx.IsCoinbase() { return}

Coinbase交易没有签名,因为它们没有真正的输入。

txCopy := tx.TrimmedCopy()

剪裁的副本将被签名,而不是完整的交易:

func (tx *Transaction) TrimmedCopy() Transaction { var inputs []TXInput var outputs []TXOutput for _, vin := range tx.Vin { inputs = append(inputs, TXInput{vin.Txid, vin.Vout, nil, nil}) } for _, vout := range tx.Vout { outputs = append(outputs, TXOutput{vout.Value, vout.PubKeyHash}) } txCopy := Transaction{tx.ID, inputs, outputs} return txCopy}

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

接下来,我们遍历副本中的每个输入:

for inID, vin := range txCopy.Vin { prevTx := prevTXs[hex.EncodeToString(vin.Txid)] txCopy.Vin[inID].Signature = nil txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash。

在每个输入中,Signature被设置为nil(只是一个双重检查)并被PubKey设置为PubKeyHash参考输出。在这一刻,所有的交易,但目前的一个是“空”的,即它们的Signature和PubKey字段设置为零。因此,输入是分开签署的,虽然这对于我们的应用程序不是必需的,但比特币允许交易包含引用不同地址的输入。

txCopy.ID = txCopy.Hash() txCopy.Vin[inID].PubKey = nil。

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

现在,中心部分:

r, s, err := ecdsa.Sign(rand.Reader, &privKey, txCopy.ID) signature := append(r.Bytes(), s.Bytes()...) tx.Vin[inID].Signature = signature。

我们签txCopy.ID有privKey。ECDSA签名是一对数字,我们连接并存储在输入Signature字段中。

现在,验证功能:

// 验证方法func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool { if tx.IsCoinbase() { return true } for _, vin := range tx.Vin { if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil { log.Panic("ERROR: Previous transaction is not correct") } } txCopy := tx.TrimmedCopy() curve := elliptic.P256() for inID, vin := range tx.Vin { prevTx := prevTXs[hex.EncodeToString(vin.Txid)] txCopy.Vin[inID].Signature = nil txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash txCopy.ID = txCopy.Hash() txCopy.Vin[inID].PubKey = nil r := big.Int{} s := big.Int{} sigLen := len(vin.Signature) r.SetBytes(vin.Signature[:(sigLen / 2)]) s.SetBytes(vin.Signature[(sigLen / 2):]) x := big.Int{} y := big.Int{} keyLen := len(vin.PubKey) x.SetBytes(vin.PubKey[:(keyLen / 2)]) y.SetBytes(vin.PubKey[(keyLen / 2):]) rawPubKey := ecdsa.PublicKey{curve, &x, &y} if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false { return false } } return true}

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

txCopy := tx.TrimmedCopy()

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

for inID, vin := range tx.Vin { prevTx := prevTXs[hex.EncodeToString(vin.Txid)] txCopy.Vin[inID].Signature = nil txCopy.Vin[inID].PubKey = prevTx.Vout[vin.Vout].PubKeyHash txCopy.ID = txCopy.Hash() txCopy.Vin[inID].PubKey = nil。

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

r := big.Int{} s := big.Int{} sigLen := len(vin.Signature) r.SetBytes(vin.Signature[:(sigLen / 2)]) s.SetBytes(vin.Signature[(sigLen / 2):]) x := big.Int{} y := big.Int{} keyLen := len(vin.PubKey) x.SetBytes(vin.PubKey[:(keyLen / 2)]) y.SetBytes(vin.PubKey[(keyLen / 2):])。

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

rawPubKey := ecdsa.PublicKey{curve, &x, &y} if ecdsa.Verify(&rawPubKey, txCopy.ID, &r, &s) == false { return false }}return true。

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

现在,我们需要一个函数来获取以前的事务。由于这需要与区块链互动,我们将使其成为一种方法Blockchain:

func (bc *Blockchain) FindTransaction(ID []byte) (Transaction, error) { bci := bc.Iterator() for { block := bci.Next() for _, tx := range block.Transactions { if bytes.Compare(tx.ID, ID) == 0 { return *tx, nil } } if len(block.PrevBlockHash) == 0 { break } } return Transaction{}, errors.New("Transaction is not found")}func (bc *Blockchain) SignTransaction(tx *Transaction, privKey ecdsa.PrivateKey) { prevTXs := make(map[string]Transaction) for _, vin := range tx.Vin { prevTX, err := bc.FindTransaction(vin.Txid) prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX } tx.Sign(privKey, prevTXs)}func (bc *Blockchain) VerifyTransaction(tx *Transaction) bool { prevTXs := make(map[string]Transaction) for _, vin := range tx.Vin { prevTX, err := bc.FindTransaction(vin.Txid) prevTXs[hex.EncodeToString(prevTX.ID)] = prevTX } return tx.Verify(prevTXs)}

这些功能很简单:FindTransaction按ID查找事务(这需要遍历区块链中的所有区块); SignTransaction采取交易,找到它引用的交易并签名; VerifyTransaction做同样的事情,而是验证交易。

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

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction { ... tx := Transaction{nil, inputs, outputs} tx.ID = tx.Hash() bc.SignTransaction(&tx, wallet.PrivateKey) return &tx}

验证发生在交易被放入块之前:

func (bc *Blockchain) MineBlock(transactions []*Transaction) { var lastHash []byte for _, tx := range transactions { if bc.VerifyTransaction(tx) != true { log.Panic("ERROR: Invalid transaction") } } ...}

实现以下签名后的命令:

C:\go-worke\src\github.com\study-bitcoin-go>study-bitcoin-go send -from 13qBhEZ9edWk7Kr4mDM4Shs9qVVVfG3XrB -to 1BB5hnQVMqiQ2JC6fGU8a3gVkRNMzKVgnX -amount 1 Mining the block containing "��C����siP �~PA8�����G�╝�m羖" Dig into mine 00000082b57010bdd217b837f7ecae20a20e65f7435ee03ee06d38a2e67598e9Success!

6.总结

我们到现在实现了地址、签名等功能。后续我们将继续完成挖矿矿工费,以及网络等。本章涉及的一些算法笔记多,需要花时间去了解这些算法。

资料