1. 程式人生 > >用go編寫區塊鏈系列之5--地址與數字簽名

用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 結論

很驚訝我們竟然完成了這麼多有關比特幣的關鍵特性!除了網路我們幾乎完成了所有的特性,下一節我們將繼續完善交易。