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)算法来签名交易。
上文提到的比特币地址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 结论
很惊讶我们竟然完成了这么多有关比特币的关键特性!除了网络我们几乎完成了所有的特性,下一节我们将继续完善交易。
|
请发表评论