golang重写区块链——0.5 区块链中钱包、地址和签名的实现

在上一章节中,我们把简单的用户定义的字节当做地址来使用,比如在上一 章我测试用到的zyj和dxn。在这一章节中我们要正真的去实现区块链中的地址。

    大家应该还记得上一章中提到过区块链中的交易是地址与地址之间的,地址的背后才是我们人来操作,因此我们会发现一个问题,就是在上一章中的这些我们自定义的地址并没有什么意义,因为随便谁都可以使用,转移该地址中的getbalnace,但是现实中我们并不想这样。所以这就涉及到了公钥、私钥与数字签名了。

    在比特币中,你的身份是通过一对公钥和私钥来证明的,公钥是可以公开的,私钥就不能公开了,私钥相当于密码用来解锁你地址上的币。私钥和公钥只不过是随机的字节序列,人类通过肉眼去读取很困难。比特币使用了Base58转换算法把公钥转换成我们方便识别的字符串

    数字签名:当数据从发送方传送到接收方时,数据不会被修改;数据由某一确定的发送方创建;发送方无法否认发送过数据这一事实。通过在数据上应用签名算法(也就是对数据进行签名),你就可以得到一个签名,这个签名晚些时候会被验证。生成数字签名需要一个私钥,而验证签名需要一个公钥。签名有点类似于印章。

    签名的具体过程为:交易发送方对发送的交易进行签名,此时需要用到发送的交易和发送方的私钥。交易接收方进行验证签名,此时需要用到的是接收到的 被签名的交易、接收到的签名、发送方的公钥。简单来说,验证过程可以被描述为:检查签名是由被签名数据加上私钥得来,并且公钥恰好是由该私钥生成。

现在来回顾一个交易完整的生命周期:

  1. 起初,创世块里面包含了一个 coinbase 交易。在 coinbase 交易中,没有输入,所以也就不需要签名。coinbase 交易的输出包含了一个哈希过的公钥(使用的是
    RIPEMD16(SHA256(PubKey)) 算法)

  2. 当一个人发送币时,就会创建一笔交易。这笔交易的输入会引用之前交易的输出。每个输入会存储一个公钥(没有被哈希)和整个交易的一个签名。

  3. 比特币网络中接收到交易的其他节点会对该交易进行验证。除了一些其他事情,他们还会检查:在一个输入中,公钥哈希与所引用的输出哈希相匹配(这保证了发送方只能花费属于自己的币);签名是正确的(这保证了交易是由币的实际拥有者所创建)。

  4. 当一个矿工准备挖一个新块时,他会将交易放到块中,然后开始挖矿。

  5. 当新块被挖出来以后,网络中的所有其他节点会接收到一条消息,告诉其他人这个块已经被挖出并被加入到区块链。

  6. 当一个块被加入到区块链以后,交易就算完成,它的输出就可以在新的交易中被引用

比特币使用椭圆曲线来产生私钥。椭圆曲线是一个复杂的数学概念,比特币使用的是 ECDSA(Elliptic Curve Digital Signature Algorithm)算法来对交易进行签名,我们也会使用该算法。

    在开始写代码之前我们需要清除这几个关系:首先私钥是随机从底层机器码中取出的256位的2进制数,然后通过椭圆曲线算法生成我们的私钥。公钥是由私钥经经过复杂的哈希运算得到的,当然是不可逆的。然后就是地址,地址又是公钥经过哈希运算和base58得到我们能容易识别的地址。

这里我们通过画图来直观的认识通过公钥生成地址的过程:

下面就用代码实现钱包地址:

package wallet
 
import (
	"bytes"
	"crypto/sha256"
	"crypto/elliptic"
	"crypto/ecdsa"
	"crypto/rand"
	"log"
	"os"
	"fmt"
	"io/ioutil"
	"encoding/gob"
	"golang.org/x/crypto/ripemd160"
	"go_code/A_golang_blockchain/base58"
 
)
const version = byte(0x00)
const walletFile = "wallet.dat"
const addressChecksumLen = 4 //对校验位一般取4位
 
//创建一个钱包结构体,钱包里面只装公钥和私钥
type Wallet struct {
	PrivateKey 		ecdsa.PrivateKey
	PublicKey 		[]byte
}
 
//实例化一个钱包
func NewWallet() *Wallet {
	//生成秘钥对
	private , public := newKeyPair()
	wallet := &Wallet{private,public}
	return wallet
}
//生成密钥对函数
func newKeyPair() (ecdsa.PrivateKey,[]byte) {
	//返回一个实现了P-256的曲线
	curve := elliptic.P256()
	//通过椭圆曲线 随机生成一个私钥
	private ,err := ecdsa.GenerateKey(curve,rand.Reader)
	if err != nil {
		log.Panic(err)
	}
	pubKey := append(private.PublicKey.X.Bytes(),private.PublicKey.Y.Bytes()...)
 
	return *private,pubKey
}
 
//生成一个地址
func (w Wallet) GetAddress() []byte {
	//调用公钥哈希函数,实现RIPEMD160(SHA256(Public Key))
	pubKeyHash := HashPubKey(w.PublicKey)
	//存储version和公钥哈希的切片
	versionedPayload := append([]byte{version},pubKeyHash...)
	//调用checksum函数,对上面的切片进行双重哈希后,取出哈希后的切片的前面部分作为检验位的值
	checksum := checksum(versionedPayload)
	//把校验位加到上面切片后面
	fullPayload := append(versionedPayload,checksum...)
	//通过base58编码上述切片得到地址
	address := base58.Base58Encode(fullPayload)
 
	return address
}
 
//公钥哈希函数,实现RIPEMD160(SHA256(Public Key))
func HashPubKey(pubKey []byte) []byte {
	//先hash公钥
	publicSHA256 := sha256.Sum256(pubKey)
	//对公钥哈希值做 ripemd160运算
	RIPEMD160Hasher := ripemd160.New()
	_,err := RIPEMD160Hasher.Write(publicSHA256[:])
	if err != nil {
		log.Panic(err)
	}
	publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)
 
	return publicRIPEMD160
}
//校验位checksum,双重哈希运算
func checksum(payload []byte) []byte {
	//下面双重哈希payload,在调用中,所引用的payload为(version + Pub Key Hash)
	firstSHA := sha256.Sum256(payload)
	secondSHA := sha256.Sum256(firstSHA[:])
 
	//addressChecksumLen代表保留校验位长度
	return secondSHA[:addressChecksumLen]  
}
 
//判断输入的地址是否有效,主要是检查后面的校验位是否正确
func ValidateAddress(address string) bool {
	//解码base58编码过的地址
	pubKeyHash := base58.Base58Decode([]byte(address))
	//拆分pubKeyHash,pubKeyHash组成形式为:(一个字节的version) + (Public key hash) + (Checksum) 
	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
}
 
//创建一个钱包集合的结构体
type Wallets struct {
	Wallets map[string]*Wallet
}
 
// 实例化一个钱包集合,
func NewWallets() (*Wallets, error) {
	wallets := Wallets{}
	wallets.Wallets = make(map[string]*Wallet)
	err := wallets.LoadFromFile()
 
	return &wallets, err
}
 
// 将 Wallet 添加进 Wallets
func (ws *Wallets) CreateWallet() string {
	wallet := NewWallet()
	address := fmt.Sprintf("%s", wallet.GetAddress())
	ws.Wallets[address] = wallet
	return address
}
 
// 得到存储在wallets里的地址
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]
}
 
// 从文件中加载钱包s
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
}
 
// 将钱包s保存到文件
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)
	}
}

说明:代码中引用了包"golang.org/x/crypto/ripemd160",这个包我们在命令行直接 go get golang.org/x/crypto/ripemd160 命令一般=是连不通的,除非翻墙。但是我们可以直接在github上面下载整个crypto包下来,操作如下:

  1. 退回到你的电脑的go文件的src目录下,创建两个目录为golang.org/x/,
  2. 在x目录下面输入命令git clone https://github.com/golang/crypto.git
  3. 此时crypto包已经克隆成功,你的编辑器目录下已经有了golang.org目录
  4. 然后直接引用"golang.org/x/crypto/ripemd160"包

    至此,我们已经得到了一个钱包地址。下面我们会实现签名,由于代码比较多,我把签名的几段核心代码附在上面,里面注解也比较详细,相信之前几章看完后看这章的代码就比较容易了。

签名的核心代码:

//对交易签名
func (tx *Transaction) Sign(privKey ecdsa.PrivateKey,prevTXs map[string]Transaction) {
	if tx.IsCoinbase() {
		return
	} 
 
	for _,vin := range tx.Vin {
		if prevTXs[hex.EncodeToString(vin.Txid)].ID == nil {
			log.Panic("ERROR: Previous transaction is not correct")
		}
	}
	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)
		if err != nil {
			log.Panic(err)
		}
		signature := append(r.Bytes(),s.Bytes()...)
 
		tx.Vin[inID].Signature = signature
	}
 
}

验证签名的代码:

//验证 交易输入的签名
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
	if tx.IsCoinbase() {
		return true
	}
	for _,vin := range tx.Vin {
		//遍历输入交易,如果发现输入交易引用的上一交易的ID不存在,则Panic
		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
}

 在签名和验证签名中都会用到的修剪交易副本的方法,这个方法很重要,我觉得也是理解签名的一个关键所在。

//创建在签名中修剪后的交易副本,之所以要这个副本是因为简化了输入交易本身的签名和公钥
func (tx *Transaction) TrimmedCopy() Transaction {
	var inputs []TXInput
	var outputs []TXOutput
 
	for _,vin := range tx.Vin {
		inputs = append(i
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值