用go编写区块链系列之5--地址与数字签名

0 介绍

上一篇文章我们实现了交易。你被灌输了这样一种观念:在比特币中没有账户,个人信息数据不需要也不会被存储。但是仍然需要一些东西去证明你是一笔交易的输出的所有者。这是比特币需要地址的原因。之前我们使用字符串去代表用户地址,现在我们需要引入地址了。

1 地址密码学

  • 比特币地址

这里有一个比特币地址的例子: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa。它传说是比特币发明者中本聪的账户地址。比特币地址是公开的,如果你需要发送比特币给某人,你需要知道他的地址。但是地址并不是能够证明你是某个钱包的拥有者的凭证,实际上地址只是一种人类可识别的公钥的表示方式。在比特币种,钱包的凭证是存储在你电脑中由公钥和私钥组成的密钥对。比特币依赖于密码学方法来产生这些秘钥,它们保证了钱包的安全。

  • 公钥加密算法

公钥加密算法使用密钥对,包含公钥和私钥。公钥可以公开,但是私钥需要绝对保密。比特币钱包本质上就是这样一个密钥对。当你安装一个钱包app或使用比特币客户端来产生一个新地址时,会为你生成一对公私钥。控制了私钥就控制了这个钱包以及钱包中的比特币。

公钥和私钥都是随机字节数组,它们不能再屏幕打印出来也不能被人类识别。比特币使用了一种算法来讲公钥转换成人类可以识别的字符串。如果你使用过比特币钱包应用,可能应用会帮你生成一串助记词。这串助记词是私钥转换过来的可识别字符串。BIP-039标准定义了这套算法。

  • 数字签名

在密码学中有数字签名这样一个概念。数字签名保证了:

1 数据从发送者发送到接收者的传输过程中没有被更改

2 数据是有确定的发送者创建的

3 发送者不能拒绝发送数据

对一串数据进行数字签名算法后会得到一个签名,这个签名可以被验证。签名过程需要私钥,验证过程需要公钥。

签名过程需要:

1 待签名数据

2 私钥

签名操作产生一个数字签名,它被存储于交易输入中。为了验证签名,需要:

1 被签名数据

2 数字签名

3 公钥

比特币中每笔交易都需要由交易创建账户进行数字签名,交易被打包进区块时都需要进行签名认证。签名认证意味着:

1 检查交易输入有权使用它引用的交易输出

2 检查交易签名是正确的

数字签名和验证过程可以用下图表示:

交易的完整生命周期是:

1 最开始存在一个创世区块,它包含一笔coinbase交易。由于coinbase交易不存在输入,所以不需要进行数字签名。coinbase的输出包含coinbase账户的公钥哈希。

2 当账户发送钱币的时候,一笔交易被创建。交易输入必须引用之前已有的交易输出。交易输入存储了公钥(不是公钥的哈希!)以及该交易的哈希。

3 比特币网络中接收到该笔交易的节点将会验证这笔交易。它们将检查交易输入中的公钥与它引用的输出中的公钥哈希相匹配;此外还要验证输入中的签名是正确的(这保证了该交易是由钱币所有者创建的)

4 当一个矿工节点开始挖掘一个新区块时,它将打包区块中所有的交易并开始挖矿。

5 当新区快被挖掘出来,网络中其它节点将接收到区块挖掘成功的消息,然后将该区块写入到区块链中。

6 当区块被写入区块链,其中的交易就算完成了,交易的输出将能够被新交易所引用。

  • 椭圆曲线加密

比特币在创建钱包私钥时需要保证该私钥的唯一性,我们不希望创建的新钱包跟已有的某个钱包的私钥相同。比特币使用的椭圆曲线加密算法来创建私钥。椭圆曲线算法可以用来产生大量真正的随机数。比特币使用的椭圆曲线可以产生从0到2²⁵⁶ 中的任意随机数(约等于10⁷⁷,可观测宇宙中总共有大约10⁷⁸~10⁸²个原子),这么巨大的范围意味着产生相同私钥的可能性极小。

比特币使用ECDSA(Elliptic Curve Digital Signature Algorithm)算法来签名交易。

  • Base58

上文提到的比特币地址1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa,它是一个人类可读的公钥表示形式。如果我们解码这个地址,得到的公钥将是:0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93。比特币使用Base58算法来将公钥转换成地址。Base58类似于Base64,它的字符集不包含0,O, I(大写的i)、l(小写的L)、+、/ 等字符。

从公钥产生地址的流程图:

 

上面提到的地址对应的公钥0062E907B15CBF27D5425399EBF6F0FB50EBB88F18C29B7D93由三部分组成:

Version  Public key hash                           Checksum
00       62E907B15CBF27D5425399EBF6F0FB50EBB88F18  C29B7D93

2 地址实现

我们创建wallet结构体:

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) {
	curve := elliptic.P256()
	private, err := ecdsa.GenerateKey(curve, rand.Reader)
	pubKey := append(private.PublicKey.X.Bytes(), private.PublicKey.Y.Bytes()...)

	return *private, pubKey
}

地址就是一对公私钥。我们在newKeyPair函数中创建了一对密钥。我们先构建了一条椭圆曲线curve,然后使用curve根据ECDSA算法生成了一个私钥,私钥包含的publicKey对象含有X,Y坐标,将X,Y坐标拼接就成了最终的公钥。

现在来生成地址:

func (w Wallet) GetAddress() []byte {
	pubKeyHash := HashPubKey(w.PublicKey)

	versionedPayload := append([]byte{version}, pubKeyHash...)
	checksum := checksum(versionedPayload)

	fullPayload := append(versionedPayload, checksum...)
	address := Base58Encode(fullPayload)

	return address
}

func HashPubKey(pubKey []byte) []byte {
	publicSHA256 := sha256.Sum256(pubKey)

	RIPEMD160Hasher := ripemd160.New()
	_, err := RIPEMD160Hasher.Write(publicSHA256[:])
	publicRIPEMD160 := RIPEMD160Hasher.Sum(nil)

	return publicRIPEMD160
}

func checksum(payload []byte) []byte {
	firstSHA := sha256.Sum256(payload)
	secondSHA := sha256.Sum256(firstSHA[:])

	return secondSHA[:addressChecksumLen]
}

公钥生成Base58格式的地址的步骤:

1 对公钥的hash使用REPEMD160算法以计算最终的哈希。返回结果pubKeyHash是RIPEMD160(SHA256(PubKey)。

2 准备地址生成算法所使用的版本version,将version与步骤1的结果拼接起来。

3 对步骤2的结果做2次哈希运算来计算校验和checkSum。返回校验和的前4位。

4 拼接校验和,version+pubKeyHash+checkSum。

5 对步骤4的结果做Base58运算,得到最终的地址。

你可以在blockchain.info网站查询刚才生成的新地址的余额,但是我可以保证不管你重新生成多少次新地址,最终查询到地址的余额都会是0。这就是选择公钥生成算法的重要性:生成相同私钥和公钥的可能性必须是几乎没有。

对于钱包,我们还需要将它保存起来。我们构建一个钱包管理的结构:

// Wallets stores a collection of wallets
type Wallets struct {
	Wallets map[string]*Wallet
}

// NewWallets creates Wallets and fills it from a file if it exists
func NewWallets() (*Wallets, error) {
	wallets := Wallets{}
	wallets.Wallets = make(map[string]*Wallet)

	err := wallets.LoadFromFile()

	return &wallets, err
}

// CreateWallet adds a Wallet to Wallets
func (ws *Wallets) CreateWallet() string {
	wallet := NewWallet()
	address := fmt.Sprintf("%s", wallet.GetAddress())

	ws.Wallets[address] = wallet

	return address
}

// GetAddresses returns an array of addresses stored in the wallet file
func (ws *Wallets) GetAddresses() []string {
	var addresses []string

	for address := range ws.Wallets {
		addresses = append(addresses, address)
	}

	return addresses
}

// GetWallet returns a Wallet by its address
func (ws Wallets) GetWallet(address string) Wallet {
	return *ws.Wallets[address]
}

// LoadFromFile loads wallets from the file
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
}

// SaveToFile saves wallets to a file
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)
	}
}

wallets结构管理多个钱包对象。SaveToFile方法将多个钱包序列化以后然后存入磁盘文件。LoadFromFile方法从磁盘文件中读取钱包对象。CreateWallet方法创建一个新钱包并且将其添加到wallets中。

接下来我们需要修改交易输入和输出结构:

type TXInput struct {
	Txid      []byte
	Vout      int
	Signature []byte
	PubKey    []byte
}

func (in *TXInput) UsesKey(pubKeyHash []byte) bool {
	lockingHash := HashPubKey(in.PubKey)

	return bytes.Compare(lockingHash, pubKeyHash) == 0
}

type TXOutput struct {
	Value      int
	PubKeyHash []byte
}

func (out *TXOutput) Lock(address []byte) {
	pubKeyHash := 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
}

我们将上一章中的ScriptPubKey和ScriptSig成员移除,ScriptPubKey被替换成公钥哈希PybKeyHash,ScriptPubKey被替换成签名和公钥。

交易输入结构的useKey方法检查一个输入能否用一个特定的公钥去解锁一个输出。注意输入中存储的是公钥,但是这个方法带的参数却是公钥哈希。

交易输出的Lock方法用来锁定一笔输出,它从一个地址中解析出公钥哈希,然后将这个公钥哈希复制给它的成员PubKeyHash。在解锁方法IsLockedWithKey中,就是比较给定的公钥哈希是否与它的PubKeyHash相同。

3 数字签名实现

交易必须被签名,这是比特币中保证一个用户不会使用属于别人的钱币的唯一方法。如果交易签名验证正确,这笔交易就认为有效,否则交易不能被加入区块。

我们差不多已经有了实现一个区块的所有知识,但是还有一个问题就是哪些数据需要签名。交易的哪部分数据需要签名,还是整个交易都要被签名?选择签名数据是很重要的事情。要签名的哪部分数据必须包含这些数据的标识信息。比如签名交易输出将是无意义的,因为输出中不包含交易的发送方信息。

考虑到交易解锁了以前交易的输出,重新分配他们的钱币,锁定新的输出,下列数据必须签名:

1 被解锁的输出的公钥哈希,它是交易发送者的标识。

2 在新建并锁定的输出的公钥哈希,它是交易接受者的标识。

3 交易输出的值。

实现交易签名的方法:

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)
		if err!=nil{
			log.Panic(err)
		}
		signature := append(r.Bytes(), s.Bytes()...)

		tx.Vin[inID].Signature = signature
	}
}

这个方法带有私钥和以前交易的map作为参数,根据上面说的,为了签名一个交易,我们必须访问交易输入引用的以前交易的输出,所以我们需要收集存储有这些输出的以前的交易。

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

这里遍历交易的每一个输入,输入的签名被设置为空,输入的公钥被设置为引用输出的公钥哈希。在这里每个输入都是独立进行签名的。

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

这里使用私钥,采用ESDSA签名算法对交易哈希进行签名,签名结果是一对数r和s,将r、s拼接成最终的签名。

签名验证方法:

func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
	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()

然后构造椭圆曲线用来产生密钥对:

curve := elliptic.P256()

这里像签名方法一样,遍历每一个输入,并且构造和签名方法一样的数据,我们验证时需要这些数据。

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

签名方法中对r、s进行拼接得到了输入的签名,这里对输入签名进行拆分得到了r、s,验证时需要它们作为参数。

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

还记得上文中的创建钱包函数中生成公钥的过程吗?公钥是由x,y拼接成的,这里将公钥进行拆分得到x,y。

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

这里我们先用ESDSA算法从curve和x,y恢复出一个公钥,然后再公钥来验证签名。

我们需要一个函数去找出以前的交易,因为需要和区块链交互,我们将这些方法放到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根据交易输入引用的交易id,使用FindTransaction来查找它引用的所有交易。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")
		}
	}
	...
}

4 实验验证

工程代码:https://github.com/Jeiwan/blockchain_go/tree/part_5

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe createwalletYour new address: 1MQ2nYkEMXjputd1jAzBqNY6pMFkVSuskW

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe createwalletYour new address: 17Tfcs5xL2az8RycRYhwuNFQ3a1GPbGDfC

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe createblockchain -address 1MQ2nYkEMXjputd1jAzBqNY6pMFkVSuskW
000094feeed417592d7f0a97513b29b34beb6ab8488b3a7621e055ca48e4e21d
216600
Done!

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe getbalance -address 1MQnYkEMXjputd1jAzBqNY6pMFkVSuskW
Balance of '1MQ2nYkEMXjputd1jAzBqNY6pMFkVSuskW': 10

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe send -from 1MQ2nYkEMXjp
td1jAzBqNY6pMFkVSuskW -to 17Tfcs5xL2az8RycRYhwuNFQ3a1GPbGDfC -amount 4
00001539e36c60661369688da86d64e896e906d47b5297a98e34b887105d3841
15058
Success!

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe getbalance -address 1MQnYkEMXjputd1jAzBqNY6pMFkVSuskW
Balance of '1MQ2nYkEMXjputd1jAzBqNY6pMFkVSuskW': 6

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe getbalance -address 17Tcs5xL2az8RycRYhwuNFQ3a1GPbGDfC
Balance of '17Tfcs5xL2az8RycRYhwuNFQ3a1GPbGDfC': 4

一切顺利!

让我们注释掉交易签名,看一下未签名的交易能否被打包:

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
}

再运行:

F:\GoPro\src\TBCPro\Address>go_build_TBCPro_Address.exe send -from 1MQ2nYkEMXjpu
td1jAzBqNY6pMFkVSuskW -to 17Tfcs5xL2az8RycRYhwuNFQ3a1GPbGDfC -amount 1
2018/09/20 16:40:09 ERROR: Invalid transaction
panic: ERROR: Invalid transaction

5 结论

很惊讶我们竟然完成了这么多有关比特币的关键特性!除了网络我们几乎完成了所有的特性,下一节我们将继续完善交易。

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值