1. 程式人生 > >用go編寫區塊鏈系列之4--交易1

用go編寫區塊鏈系列之4--交易1

0 介紹

比特幣區塊鏈的核心就是交易,區塊鏈唯一的目的就是用一種安全可信的方式去儲存交易,交易一經建立就無法更改。這章中我們將在區塊鏈中引入交易。

1 比特幣中的交易

如果你是開發網路應用的程式設計師,若讓你開發一個線上支付交易,你多半會在資料庫中建立倆張表:賬戶表和交易表。賬戶表中將會儲存使用者賬戶資訊,比如個人資訊和餘額。交易表中將會儲存交易資訊,比如錢從一個賬戶轉賬給另一個賬戶。但是在比特幣系統中,交易是以一種完全不同的方式實現的,在比特幣中:

  • 沒有賬戶
  • 沒有餘額
  • 沒有地址
  • 沒有貨幣
  • 沒有傳送者和接受者

由於區塊鏈是一個公共公開的資料庫,我們不想在其中存放敏感的錢包資訊。賬戶中不儲存資金。交易並不從一個地址傳送到另一個地址。同時也不儲存使用者賬戶餘額。只有交易。那麼交易裡面有什麼呢?

  • 比特幣交易

一個交易時輸入和輸出的集合:

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}

ID是交易的hash;Vin是輸入陣列;Vout是輸出陣列。一筆新交易的輸入時此前交易的輸出(有一種例外情況,下文會講到)。輸出是儲存錢幣的地方。下圖顯示了交易的輸入輸出連線關係:

注意點:

(1)輸出不一定會連線到輸入。

(2)一筆交易中,輸入可以引用多筆交易的輸出。

(3)一個輸入必須引用一個輸出。

這篇文章中,我們使用錢、貨幣、支付、轉賬、賬戶等術語,但是在真實比特幣系統中不存在這些概念。交易只是用指令碼鎖定一些值,這些值只能由鎖定者才能解鎖。

  • 交易輸出

交易輸出結構體TXOutput:

type TXOutput struct {
	Value        int
	ScriptPubKey string
}

TXOuntput通過Value值來"儲存貨幣"。這裡的“儲存”就是通過ScriptPublikey密語來鎖定。比特幣使用Script指令碼語言來定義鎖定和解鎖邏輯。我們在這裡使用簡單字串形式的地址來實現鎖定解鎖邏輯。

需要注意的是交易輸出是不可拆分的,你不能只花費它的一部分,而必須全部花出去。當這個交易輸出的金額大於所需支付的金額時,將會產生一個找零返回給傳送者。這很像現實世界中的鈔票,你用10塊錢去買一瓶3塊錢的飲料,最後店家會給你找零7塊錢。

  • 交易輸入

定義輸入結構體

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}

交易輸入都是引入以前交易的輸出。其中Txid是引用的交易Hash,Vout是這個輸出所在陣列的索引。ScriptSig對應於交易輸入中的ScriptPubKey,用來進行解鎖操作。如果ScriptSig能夠解鎖,這筆輸出就能夠被使用者使用。如果解鎖失敗,則這筆錢不能被使用者使用。這個機制保證了使用者不能使用屬於別人的錢。我們在這裡使用簡單的字串來進行解鎖。關於公鑰和簽名,以後文章會講到。

總結一下,交易輸出用來儲存錢幣。每個輸出都帶有一個指令碼密語,用來進行解鎖操作。每筆交易必須帶有至少一個輸入和一個輸出。一個輸入必須引用自一筆以前的交易。一個輸入帶有一段鎖定密語,使用者必須有相應的鑰匙解鎖才能解鎖該輸入,解鎖以後就能使用這筆輸入中的錢。

這裡產生了一個問題:輸入和輸出誰先產生?

  • 雞生蛋還是蛋生雞

輸入引用輸出,輸入輸出誰先誰後的問題是一個經典的雞生蛋還是蛋生雞的問題。但是在比特幣中,輸出先於輸入產生。

當一個礦工開始挖掘一個區塊的時候,它將一個coinbase交易新增到區塊裡面。coinbase交易時一種特殊的交易,它不需要引用以前產生的交易輸出。它憑空產生了一個交易輸出,這也是作為對礦工的挖礦獎勵。

區塊鏈中有一個創世區塊,它產生了第一個交易輸出。讓我們建立一個coinbase交易:

func NewCoinbaseTX(to, data string) *Transaction {
	if data == "" {
		data = fmt.Sprintf("Reward to '%s'", to)
	}

	txin := TXInput{[]byte{}, -1, data}
	txout := TXOutput{subsidy, to}
	tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
	tx.SetID()

	return &tx
}

coinbase交易只有一個交易輸入,在這裡設定它的Txid為空,設定Vout為-1,指令碼ScriptSig設定為任意字串。在比特幣中,這個指令碼通常設定為 “The Times 03/Jan/2009 Chancellor on brink of second bailout for banks” 。subsidy是區塊獎勵,這裡設定為10。

2 在區塊鏈中儲存交易

我們需要在區塊中儲存交易陣列,所以我們改造Block結構體:

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{})
}

還要修改建立區塊鏈的方法:

// CreateBlockchain creates a new blockchain DB
func CreateBlockchain(address string) *Blockchain {
	if dbExists() {
		fmt.Println("Blockchain already exists.")
		os.Exit(1)
	}

	var tip []byte
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Panic(err)
	}

	err = db.Update(func(tx *bolt.Tx) error {
		cbtx := NewCoinbaseTX(address, genesisCoinbaseData)
		genesis := NewGenesisBlock(cbtx)

		b, err := tx.CreateBucket([]byte(blocksBucket))
		if err != nil {
			log.Panic(err)
		}

		err = b.Put(genesis.Hash, genesis.Serialize())
		if err != nil {
			log.Panic(err)
		}

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

		return nil
	})

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

	bc := Blockchain{tip, db}

	return &bc
}

3 PoW

PoW演算法必須考慮引入交易。為了保證區塊鏈對交易一致性的要求,我們修改ProofOfWork.prepareData方法:

func (pow *ProofOfWork) prepareData(nonce int) []byte {
	data := bytes.Join(
		[][]byte{
			pow.block.PrevBlockHash,
			pow.block.HashTransactions(), // This line was changed
			IntToHex(pow.block.Timestamp),
			IntToHex(int64(targetBits)),
			IntToHex(int64(nonce)),
		},
		[]byte{},
	)

	return data
}

其中的計算交易陣列Hash值的方法:

func (b *Block) HashTransactions() []byte {
	var txHashes [][]byte
	var txHash [32]byte

	for _, tx := range b.Transactions {
		txHashes = append(txHashes, tx.ID)
	}
	txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))

	return txHash[:]
}

我們在這裡將交易陣列的每個交易hash進行拼接然後再一起計算hash。在比特幣中,使用交易的默克爾樹的根雜湊來表示交易陣列,這種方法可以只需要根雜湊就可以很快的查詢到區塊中是否存在某個交易,而不用下載所有的交易。

4 未花費交易輸出(UTXO)

我們需要找出所有未花費出去的交易輸出(UTXO),未花費意味著這些交易輸出還沒有被任何交易輸入引用。在上圖中,有這些UTXO:

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0

當我們檢查地址餘額的時候,我們只需要我們可以解鎖的交易。我們定義鎖定和解鎖函式:

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == unlockingData
}

func (in *TXInput) CanUnlockOutputWith(unlockingData string) bool {
	return in.ScriptSig == unlockingData
}

這裡我們只是簡單的比較輸入輸出的指令碼密語是否和我們提供的unlockingData一致。以後我們將進入地址和私鑰。

查詢未花費交易的方法:

func (bc *Blockchain) FindUnspentTransactions(address string) []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 {
				// Was the output spent?
				if spentTXOs[txID] != nil {
					for _, spentOut := range spentTXOs[txID] {
						if spentOut == outIdx {
							continue Outputs
						}
					}
				}

				if out.CanBeUnlockedWith(address) {
					unspentTXs = append(unspentTXs, *tx)
				}
			}

			if tx.IsCoinbase() == false {
				for _, in := range tx.Vin {
					if in.CanUnlockOutputWith(address) {
						inTxID := hex.EncodeToString(in.Txid)
						spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
					}
				}
			}
		}

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

	return unspentTXs
}

首先往前遍歷每一個區塊,對於每一個區塊,又遍歷它的每一筆交易。下面程式碼:

// Was the output spent?
if spentTXOs[txID] != nil {
	for _, spentOut := range spentTXOs[txID] {
	    if spentOut == outIdx {
		    continue Outputs
	    }
	}
}

if out.CanBeUnlockedWith(address) {
	unspentTXs = append(unspentTXs, *tx)
}

如果一個交易輸出已經被花費了,就忽略;否則先檢查是否能夠用當前地址解鎖,若解鎖成功就將它加入到未花費陣列中。

if tx.IsCoinbase() == false {
	for _, in := range tx.Vin {
		if in.CanUnlockOutputWith(address) {
			inTxID := hex.EncodeToString(in.Txid)
			spentTXOs[inTxID] = append(spentTXOs[inTxID], in.Vout)
		}
	}
}

檢查交易輸入時這裡先排除coinbase交易,因為沒有花費的coinbase交易輸出通過上一步已經新增到unspentTXs陣列中了。在這裡我們只檢查普通交易。遍歷交易每一個輸入,若該輸入能夠被使用者地址解鎖,我們就將該交易新增到已花費Map中。

下面是查詢跟地址相關的UTXO的方法,我們先查詢到所有未花費交易,然後只新增地址能夠解鎖的交易。

func (bc *Blockchain) FindUTXO(address string) []TXOutput {
       var UTXOs []TXOutput
       unspentTransactions := bc.FindUnspentTransactions(address)

       for _, tx := range unspentTransactions {
               for _, out := range tx.Vout {
                       if out.CanBeUnlockedWith(address) {
                               UTXOs = append(UTXOs, out)
                       }
               }
       }

       return UTXOs
}

現在我們實現查詢餘額的方法:

func (cli *CLI) getBalance(address string) {
	bc := NewBlockchain(address)
	defer bc.db.Close()

	balance := 0
	UTXOs := bc.FindUTXO(address)

	for _, out := range UTXOs {
		balance += out.Value
	}

	fmt.Printf("Balance of '%s': %d\n", address, balance)
}

一個地址的餘額是跟地址相關的所有未花費交易的錢幣之和。

5 傳送錢幣

我們實現從一個地址from向另一個地址to傳送amount金額的傳送交易函式:

func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
	var inputs []TXInput
	var outputs []TXOutput

	acc, validOutputs := bc.FindSpendableOutputs(from, amount)

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

	// Build a list of inputs
	for txid, outs := range validOutputs {
		txID, err := hex.DecodeString(txid)

		for _, out := range outs {
			input := TXInput{txID, out, from}
			inputs = append(inputs, input)
		}
	}

	// Build a list of outputs
	outputs = append(outputs, TXOutput{amount, to})
	if acc > amount {
		outputs = append(outputs, TXOutput{acc - amount, from}) // a change
	}

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

	return &tx
}

在建立新的交易輸出的之前,我們需要找到所有可用的未花費輸出,還要保證它們攜帶有足夠的錢幣,這是通過方法FindSpendableOutputs 實現的。然後,對於每一個UTXO輸出,建立了一個引用它的交易輸入。然後我們建立了倆個輸出:

1 用接收者地址進行鎖定的交易輸出,它攜帶有不少於需要支付的金額。

2 用傳送者地址進行鎖定的交易輸出,它是找零給傳送者的錢幣。

FindSpendableOutputs 方法:

func (bc *Blockchain) FindSpendableOutputs(address string, amount int) (int, map[string][]int) {
	unspentOutputs := make(map[string][]int)
	unspentTXs := bc.FindUnspentTransactions(address)
	accumulated := 0

Work:
	for _, tx := range unspentTXs {
		txID := hex.EncodeToString(tx.ID)

		for outIdx, out := range tx.Vout {
			if out.CanBeUnlockedWith(address) && accumulated < amount {
				accumulated += out.Value
				unspentOutputs[txID] = append(unspentOutputs[txID], outIdx)

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

	return accumulated, unspentOutputs
}

這個方法遍歷所有可用的為花費交易輸出,然後收集他們的餘額,若金額不小於需要支付的金額amount時,我們就退出,這保證了我們收集到足夠的金額又不至於過多。

然後我們實現區塊挖掘函式:

// MineBlock mines a new block with the provided transactions
func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	var lastHash []byte

	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
	})
}

實現交易傳送命令:

func (cli *CLI) send(from, to string, amount int) {
	bc := NewBlockchain()
	defer bc.db.Close()

	tx := NewUTXOTransaction(from, to, amount, bc)
	bc.MineBlock([]*Transaction{tx})
	fmt.Println("Success!")
}

傳送金幣也就是也就是建立一筆交易並且通過挖礦將它新增到區塊中。比特幣沒有立即挖礦,而是先將所有的新交易新增到記憶體中的交易池,當一個礦工要開始挖掘區塊時,它將交易池中的所有交易打包到新區快。當區塊被挖掘出來併發布到區塊鏈中後,這些交易也將生效。

6 驗證

所有原始碼見https://github.com/Jeiwan/blockchain_go/blob/part_4

執行工程,建立區塊鏈:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe createblockcha in -address Ivan
000024a0c2b4c7cf9c44e065ae2d4a8f4e6d2ea4c1ea490e599cce7c1909c384
21247
Done!

查詢Ivan餘額:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Ivan
Balance of 'Ivan': 10

傳送一筆交易並查詢餘額:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Ivan -to Peter -amount 6
00000e146f87f072fc5677dd9e88bf61e3f2f34bd672472e09c64c80676aef5b
50939
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Ivan
Balance of 'Ivan': 4

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Peter
Balance of 'Peter': 6

傳送多筆交易並查詢餘額:

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Peter -to Helen -amount 2
0000d0a4d4c468ffcc1112c9395ab73e2f4abfa1b88448f4f8a255ad359bba5e
7289
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Ivan -to Helen -amount 2
0000c6c86094e1201dcbe9a913772745a9930fe772302aa634dcc212d3e02616
17825
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe send -from Helen -to Cookie -amount 3
00005b12a0726a68188b7e0148c26d69740f755d8eade14c53fef294d98eb5e8
92174
Success!

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Ivan
Balance of 'Ivan': 2

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Peter
Balance of 'Peter': 4

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Helen
Balance of 'Helen': 1

F:\GoPro\src\TBCPro\Transaction1>go_build_TBCPro_Transaction1.exe getbalance -address Cookie
Balance of 'Cookie': 3

7 結論

我們已經實現了一個帶交易的區塊鏈,但是還缺少比特幣區塊鏈的一些關鍵特性:

1 地址。我們還沒有基於私鑰的地址。

2 挖礦獎勵。現在還沒有挖礦獎勵。

3 UTXO集合。查詢餘額需要遍歷整個區塊鏈,當區塊數很多時這將會是很耗時的操作。UTXO集合將能加快整個過程。

4 交易池。交易在被打包到新區快前將臨時儲存在交易池中。在現在的系統中一個區塊只包含一筆交易,這樣很低效。