用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 結論
很驚訝我們竟然完成了這麼多有關比特幣的關鍵特性!除了網路我們幾乎完成了所有的特性,下一節我們將繼續完善交易。