1. 程式人生 > >Go語言實現區塊鏈與加密貨幣-Part2(交易與地址,餘額翻倍漏洞)

Go語言實現區塊鏈與加密貨幣-Part2(交易與地址,餘額翻倍漏洞)

準備工作:
安裝依賴包:$ go get golang.org/x/crypto/ripemd160
安裝失敗請檢視:https://blog.csdn.net/ak47000gb/article/details/79561358

交易

交易(transaction)是比特幣的核心所在,而區塊鏈唯一的目的,也正是為了能夠安全可靠地儲存交易。在區塊鏈中,交易一旦被建立,就沒有任何人能夠再去修改或是刪除它。今天,我們將會開始實現交易。不過,由於交易是很大的話題,我會把它分為兩部分來講:在今天這個部分,我們會實現交易的基本框架。在第二部分,我們會繼續討論它的一些細節。
由於比特幣採用的是 UTXO(未使用交易輸出) 模型,並非賬戶模型,並不直接存在“餘額”這個概念,餘額需要通過遍歷整個交易歷史得來。

比特幣的交易

點選這裡blockchain.info 檢視下圖中的交易資訊:
在這裡插入圖片描述

一筆交易由一些輸入(input)和輸出(output)組合而來:

// 一筆交易由一些輸入和輸出構成
type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

對於每一筆新的交易,它的輸入會引用(reference)之前一筆交易的輸出(這裡有個例外,coinbase 交易),引用就是花費的意思。所謂引用之前的一個輸出,也就是將之前的一個輸出包含在另一筆交易的輸入當中,就是花費之前的交易輸出。交易的輸出,就是幣實際儲存的地方。下面的圖示闡釋了交易之間的互相關聯:
在這裡插入圖片描述


注意:
1.有一些輸出並沒有被關聯到某個輸入上
2.一筆交易的輸入可以引用之前多筆交易的輸出
3.一個輸入必須引用一個輸出
貫穿本文,我們將會使用像“錢(money)”,“幣(coin)”,“花費(spend)”,“傳送(send)”,“賬戶(account)” 等等這樣的詞。但是在比特幣中,其實並不存在這樣的概念。交易僅僅是通過一個指令碼(script)來鎖定(lock)一些值(value),而這些值只可以被鎖定它們的人解鎖(unlock)。
每一筆比特幣交易都會創造輸出,輸出都會被區塊鏈記錄下來。給某個人傳送比特幣,實際上意味著創造新的 UTXO 並註冊到那個人的地址,可以為他所用。

交易的輸出

先從輸出(output)開始:

// 輸出包含兩部分
//Value:有多少幣,儲存在這裡
//ScriptPubkey:對輸出進行鎖定,在這裡,將僅用一個字串來代替
type TXOutput struct {
	Value      int
	PubKeyHash []byte  //相當於ScriptPubKey
}

實際上,正是輸出裡面儲存了“幣”(注意,也就是上面的 Value 欄位)。而這裡的儲存,指的是用一個數學難題對輸出進行鎖定,這個難題被儲存在 ScriptPubKey 裡面。在內部,比特幣使用了一個叫做 Script 的指令碼語言,用它來定義鎖定和解鎖輸出的邏輯。雖然這個語言相當的原始(這是為了避免潛在的黑客攻擊和濫用而有意為之),並不複雜,但是我們也並不會在這裡討論它的細節。你可以在這裡 找到詳細解釋。

在比特幣中,value 欄位儲存的是 satoshi 的數量,而不是 BTC 的數量。一個 satoshi (聰)等於一億分之一的 BTC(0.00000001 BTC),這也是比特幣裡面最小的貨幣單位(就像是 1 分的硬幣)。

由於還沒有實現地址(address),所以目前我們會避免涉及邏輯相關的完整指令碼。ScriptPubKey 將會儲存一個任意的字串(使用者定義的錢包地址)。

順便說一下,有了一個這樣的指令碼語言,也意味著比特幣其實也可以作為一個智慧合約平臺。

關於輸出,非常重要的一點是:它們是不可再分的(indivisible)。也就是說,你無法僅引用它的其中某一部分。要麼不用,如果要用,必須一次性用完。當一個新的交易中引用了某個輸出,那麼這個輸出必須被全部花費。如果它的值比需要的值大,那麼就會產生一個找零,找零會返還給傳送方。這跟現實世界的場景十分類似,當你想要支付的時候,如果一個東西值 1 RMB,而你給了一張5 RMB的紙幣,那麼你會得到4 RMB的找零。

交易的輸入

這裡是輸入(input):

// Txid:一個交易輸入引用了之前一筆交易的一個輸出,ID表明的是之前的哪筆交易
// Vout:一筆交易可能有多個輸出,Vout為輸出的索引
//ScriptSig:提供解鎖輸出,ID+Out的資料
type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string  
	
}

正如之前所提到的,一個輸入引用了之前交易的一個輸出:Txid 儲存的是之前交易的 ID,Vout 儲存的是該輸出在那筆交易中所有輸出的索引(因為一筆交易可能有多個輸出,需要有資訊指明是具體的哪一個)。ScriptSig 是一個指令碼,提供了可解鎖輸出結構裡面 ScriptPubKey 欄位的資料。如果 ScriptSig 提供的資料是正確的,那麼輸出就會被解鎖,然後被解鎖的值就可以被用於產生新的輸出;如果資料不正確,輸出就無法被引用在輸入中,或者說,無法使用這個輸出。這種機制,保證了使用者無法花費屬於其他人的幣。
再次強調,由於我們還沒有實現地址,所以目前 ScriptSig 將僅僅儲存一個使用者自定義的任意錢包地址。我們會在下一篇文章中實現公鑰(public key)和簽名(signature)。
來簡要總結一下。輸出,就是 “幣” 儲存的地方。每個輸出都會帶有一個解鎖指令碼,這個指令碼定義瞭解鎖該輸出的邏輯。每筆新的交易,必須至少有一個輸入和輸出。一個輸入引用了之前一筆交易的輸出,並提供瞭解鎖資料(也就是 ScriptSig 欄位),該資料會被用在輸出的解鎖指令碼中解鎖輸出,解鎖完成後即可使用它的值去產生新的輸出。
每一筆輸入都是之前一筆交易的輸出,那麼假設從某一筆交易開始不斷往前追溯,它所涉及的輸入和輸出到底是誰先存在呢?換個說法,這是個雞和蛋誰先誰後的問題,是先有蛋還是先有雞呢?

先有輸出

在比特幣中,是先有蛋,然後才有雞。輸入引用輸出的邏輯,是經典的“蛋還是雞”問題:輸入先產生輸出,然後輸出使得輸入成為可能。在比特幣中,最先有輸出,然後才有輸入。換而言之,第一筆交易只有輸出,沒有輸入。
當礦工挖出一個新的塊時,它會向新的塊中新增一個 coinbase 交易。coinbase 交易是一種特殊的交易,它不需要引用之前一筆交易的輸出。它“憑空”產生了幣(也就是產生了新幣),這是礦工獲得挖出新塊的獎勵,也可以理解為“發行新幣”。
在區塊鏈的最初,也就是第一個塊,叫做創世塊。正是這個創世塊,產生了區塊鏈最開始的輸出。對於創世塊,不需要引用之前的交易輸出。因為在創世塊之前根本不存在交易,也就沒有不存在交易輸出。
來建立一個 coinbase 交易:

// 這個函式建立一筆Coinbase交易,這筆交易沒有輸入,只有一個輸出。這個函式返回一筆交易的地址。
func NewCoinbaseTX(to, data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("Reward to '%s'", to)   //to就是目標地址
	}

	txin := TXInput{[]byte{}, -1, data}   //沒有輸入,表現為Txid為空,Vout=-1,ScripSig中只是儲存了任意的data
	txout := NewTXOutput(subsidy, to)                  //只有一個輸出,subsidy是獎勵金
	tx := Transaction{nil, []TXInput{txin}, []TXOutput{*txout}}
	tx.SetID()

	return &tx
}

在比特幣中,第一筆 coinbase 交易包含了如下資訊:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。
subsidy 是挖出新塊的獎勵金。在比特幣中,實際並沒有儲存這個數字,而是基於區塊總數進行計算而得:區塊總數除以 210000 就是 subsidy。挖出創世塊的獎勵是 50 BTC,每挖出 210000 個塊後,獎勵減半。在我們的實現中,這個獎勵值將會是一個常量(至少目前是)。

將交易儲存到區塊鏈

從現在開始,每個塊必須儲存至少一筆交易。如果沒有交易,也就不可能出新的塊。這意味著我們應該移除 Block 的 Data 欄位,取而代之的是儲存交易:

// //區塊的資料結構
type Block struct {
	Timestamp     int64
	Transactions  []*Transaction  //交易
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int   //難度
}

NewBlock 和 NewGenesisBlock 也必須做出相應改變:

// 建立一個區塊
func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
	block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
	pow := NewProofOfWork(block)
	nonce, hash := pow.Run()//呼叫計算雜湊的方法

	block.Hash = hash[:]
	block.Nonce = nonce

	return block
}

// //函式用來建立並返回一個創世區塊
func NewGenesisBlock(coinbase *Transaction) *Block {
	return NewBlock([]*Transaction{coinbase}, []byte{})
}

接下來修改建立區塊鏈的函式:

// 建立一個新的區塊鏈資料庫
// address用來接收挖出創世塊的獎勵
func CreateBlockchain(address string) *Blockchain {
	if dbExists() {
		fmt.Println("Blockchain already exists.")
		os.Exit(1)
	}

	var tip []byte

	cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
	genesis := NewGenesisBlock(cbtx)

	db, err := bolt.Open(dbFile, 0600, nil)
	//這是開啟一個BoltDB檔案的標準做法。注意,即便不存在這樣的檔案,它也不會返回錯誤 
	//在BoltDB中,資料庫操作通過一個事務(transaction)進行操作
	//這裡開啟的是一個讀寫事務(db.Update(...)),因為我們可能會向資料庫中新增創世塊
	if err != nil {
		log.Panic(err)
	}

	err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket([]byte(blocksBucket))//建立一個名為“blocks”的Bucket
		if err != nil {
			log.Panic(err)
		}

		err = b.Put(genesis.Hash, genesis.Serialize())//將創世區塊序列化後,與該塊的雜湊(作為鍵值)一起存入Bucket
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("l"), genesis.Hash)//用“1”作為創世雜湊的鍵,因為此時創世塊作為最後一個塊存在
		if err != nil {
			log.Panic(err)
		}
		tip = genesis.Hash//指向創世區塊

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	bc := Blockchain{tip, db}

	return &bc
}

工作量證明

工作量證明演算法必須要將儲存在區塊裡面的交易考慮進去,從而保證區塊鏈交易儲存的一致性和可靠性。所以,我們必須修改 ProofOfWork.prepareData 方法:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(),  //這裡做出了改變
			IntToHex(pow.block.Timestamp),
			IntToHex(int64(targetBits)),
			IntToHex(int64(nonce)),
		},
		[]byte{},
	)

	return data
}

不像之前使用 pow.block.Data,現在我們使用 pow.block.HashTransactions() :

// 計算區塊裡所有交易的雜湊,為了通過這個雜湊就能識別一個塊中的所有交易
func (b *Block) HashTransactions() []byte {
	var txHashes [][]byte
	var txHash [32]byte

	for _, tx := range b.Transactions {
		txHashes = append(txHashes, tx.Hash())   //先獲取每筆交易的雜湊再連線起來
	}
	txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))  //組合後的雜湊

	return txHash[:]
}

通過雜湊提供資料的唯一表示,這種做法我們已經不是第一次遇到了。我們想要通過僅僅一個雜湊,就可以識別一個塊裡面的所有交易。為此,先獲得每筆交易的雜湊,然後將它們關聯起來,最後獲得一個連線後的組合雜湊。

比特幣使用了一個更加複雜的技術:它將一個塊裡面包含的所有交易表示為一個 Merkle tree ,然後在工作量證明系統中使用樹的根雜湊(root hash)。這個方法能夠讓我們快速檢索一個塊裡面是否包含了某筆交易,即只需 root hash 而無需下載所有交易即可完成判斷。

來檢查一下到目前為止是否正確:
在這裡插入圖片描述
nice!我們已經獲得了第一筆挖礦獎勵,但是,我們要如何檢視餘額呢?

未花費交易輸出

我們需要找到所有的未花費交易輸出(unspent transactions outputs, UTXO)。未花費(unspent) 指的是這個輸出還沒有被包含在任何交易的輸入中,或者說沒有被任何輸入引用。在“比特幣的交易”一節的圖示中,未花費的輸出是:
1.tx0, output 1;
2.tx1, output 0;
3.tx3, output 0;
4.tx4, output 0.
當然了,檢查餘額時,我們並不需要知道整個區塊鏈上所有的 UTXO,只需要關注那些我們能夠解鎖的那些 UTXO(目前我們還沒有實現金鑰,所以我們將會使用使用者定義的地址來代替)。首先,讓我們定義在輸入和輸出上的鎖定解鎖方法:
在這裡插入圖片描述
在這裡,我們只是將 script 欄位與 unlockingData 進行了比較。在後續文章我們基於私鑰實現了地址以後,會對這部分進行改進。
下一步,找到包含未花費輸出的交易,這一步其實相當困難:

// 返回一個交易列表,找到未花費輸出的交易
func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []Transaction {
	var unspentTXs []Transaction
	spentTXOs := make(map[string][]int)
	bci := bc.Iterator()

	for {
		block := bci.Next()

		for _, tx := range block.Transactions {
			txID := hex.EncodeToString(tx.ID)

		Outputs:
			for outIdx, out := range tx.Vout {
				// 跳過那些已經被包含在其他輸入中的輸出,即已經被花費的輸出
				if spentTXOs[txID] != nil {
					for _, spentOutIdx := range spentTXOs[txID] {
						if spentOutIdx == outIdx {
							continue Outputs
						}
					}
				}
				//如果該交易輸出可以被解鎖,即可以被花費
				if out.IsLockedWithKey(pubKeyHash) {
					unspentTXs = append(unspentTXs, *tx)      //此處有問題!!!
				}
			}
			//將給定地址所有能夠解鎖輸出的輸入聚集起來
			//這並不適用於coinbase交易,因為它們不解鎖輸出
			if tx.IsCoinbase() == false {
				for _, in := range tx.Vin {
					if in.UsesKey(pubKeyHash) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
					}
				}
			}
		}

		if len(block.PrevBlockHash) == 0 {
			break
		}
	}

	return unspentTXs
}

這個函式返回了一個交易列表,裡面包含了未花費輸出。為了計算餘額,我們還需要一個函式將這些交易作為輸入,然後僅返回一個輸出:

// 找到並返回所有的未花費交易輸出,僅返回一個
func (bc *Blockchain) FindUTXO(pubKeyHash []byte) []TXOutput {
	var UTXOs []TXOutput
	unspentTransactions := bc.FindUnspentTransactions(pubKeyHash)

	for _, tx := range unspentTransactions {
		for _, out := range tx.Vout {
			if out.IsLockedWithKey(pubKeyHash) {  //已經被鎖定的
				UTXOs = append(UTXOs, out)
			}
		}
	}

	return UTXOs
}

就是這麼多了!現在我們來實現 getbalance 命令:
在這裡插入圖片描述
賬戶餘額就是由賬戶地址鎖定的所有未花費交易輸出的總和。

在挖出創世塊以後,來檢查一下我們的餘額:
在這裡插入圖片描述
這就是我們的第一桶金!

傳送幣

現在,我們想要給其他人傳送一些幣。為此,我們需要建立一筆新的交易,將它放到一個塊裡,然後挖出這個塊。之前我們只實現了 coinbase 交易(這是一種特殊的交易),現在我們需要一種通用的普通交易:

// NewUTXOTransaction 建立一筆新的普通交易
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	var inputs []TXInput
	var outputs []TXOutput
	//找到足夠的未花費輸出,確保餘額是足夠的(VALUE)
	wallets, err := NewWallets()
	if err != nil {
		log.Panic(err)
	}
	wallet := wallets.GetWallet(from)
	pubKeyHash := HashPubKey(wallet.PublicKey)
	acc, validOutputs := bc.FindSpendableOutputs(pubKeyHash, amount)

	if acc < amount {
		log.Panic("ERROR: Not enough funds")
	}

	// 對於每個找到的輸入,會建立一個引用該輸出的輸入
	for txid, outs := range validOutputs {
		txID, err := hex.DecodeString(txid)
		if err != nil {
			log.Panic(err)
		}

		for _, out := range outs {
			input := TXInput{txID, out, nil, wallet.PublicKey}
			inputs = append(inputs, input)
		}
	}

	// 建立輸出,由接受者地址鎖定,這是給其他地址實際轉移的幣
	outputs = append(outputs, *NewTXOutput(amount, to))

	//如果UTXO總數超過所需,則產生找零,由傳送者地址鎖定
	if acc > amount {
		outputs = append(outputs, *NewTXOutput(acc-amount, from)) // a change
	}

	tx := Transaction{nil, inputs, outputs}
	tx.SetID ()

	return &tx
}

再看看FindSpendableOutputs 方法。這是基於之前定義的 FindUnspentTransactions 方法:

// FindSpendableOutputs 從address中找到至少 amount 的UTXO
func (bc *Blockchain) FindSpendableOutputs(pubKeyHash []byte, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(pubKeyHash)
	accumulated := 0

Work:
	for _, tx := range unspentTXs {  //對所有未花費交易進行迭代,並累加
		txID := hex.EncodeToString(tx.ID)//將十六進位制轉換為字串形式

		for outIdx, out := range tx.Vout {
			if out.IsLockedWithKey(pubKeyHash) && accumulated < amount {//如果輸出是能被解鎖的,並且此時還未湊夠錢
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx) //那麼就把對應的ID新增到索引中

				if accumulated >= amount {
					break Work
				}
			}
		}
	}

	return accumulated, unspentOutputs  //返回累加值,同時還有通過交易ID進行分組的輸出索引
}

這個方法對所有的未花費交易進行迭代,並對它的值進行累加。當累加值大於或等於我們想要傳送的值時,它就會停止並返回累加值,同時返回的還有通過交易 ID 進行分組的輸出索引。我們只需取出足夠支付的錢就夠了。
現在,我們可以修改 Blockchain.MineBlock 方法:

// MineBlock 通過給出的交易建立一個新的塊
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lastHash []byte

	for _, tx := range transactions {
		if bc.VerifyTransaction(tx) != true {   //在一筆交易被放入一個塊之前進行驗證
			log.Panic("ERROR: Invalid transaction")
		}
	}

	err := bc.db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		lastHash = b.Get([]byte("l"))

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	newBlock := NewBlock(transactions, lastHash)

	err = bc.db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(blocksBucket))
		err := b.Put(newBlock.Hash, newBlock.Serialize())
		if err != nil {
			log.Panic(err)
		}

		err = b.Put([]byte("l"), newBlock.Hash)
		if err != nil {
			log.Panic(err)
		}

		bc.tip = newBlock.Hash

		return nil
	})
	if err != nil {
		log.Panic(err)
	}
}

最後,讓我們來實現 send 方法:

func (cli *CLI) send(from, to string, amount int) { //傳入傳送方和接收方的地址,以及傳送的數量
	if !ValidateAddress(from) {//檢查地址是否合法
		log.Panic("ERROR: Sender address is not valid")
	}
	if !