在Go中構建區塊鏈 第4部分:交易1

銀河1號發表於2019-02-17

Introduction

交易是比特幣的核心,區塊鏈的唯一目的是以安全可靠的方式儲存交易,因此沒有人可以在建立交易後對其進行修改。今天我們開始實施交易。但是因為這是一個相當大的話題,我將它分為兩部分:在這部分中,我們將實現交易的一般機制,在第二部分,我們將通過細節進行處理。

此外,由於程式碼更改很大,因此在這裡描述所有程式碼都沒有意義。您可以看到所有更改here.

There is no spoon

如果您曾經開發過Web應用程式,為了實現付款,您可能會在資料庫中建立這些表:accounts 和 transactions。帳戶將儲存關於使用者的資訊,包括他們的個人資訊和餘額,並且交易將儲存關於從一個帳戶轉移到另一個帳戶的錢的資訊。在比特幣中,支付以完全不同的方式實現。有:

  1. No accounts.
  2. No balances.
  3. No addresses.
  4. No coins.
  5. 沒有發件人和收件人。

由於區塊鏈是公共和開放資料庫,我們不希望儲存有關錢包所有者的敏感資訊。帳戶中不會收集硬幣。交易不會將錢從一個地址轉移到另一個地址。沒有儲存帳戶餘額的欄位或屬性。只有交易。但是交易中有什麼?

Bitcoin Transaction

交易是輸入和輸出的組合:

type Transaction struct {
	ID   []byte
	Vin  []TXInput
	Vout []TXOutput
}
複製程式碼

輸入前一個事務的新事務引用輸出(雖然有一個例外,我們將在後面討論)。輸出是硬幣實際儲存的地方。下圖演示了事務的互連:

在Go中構建區塊鏈  第4部分:交易1

Notice that:

  1. 有些輸出與輸入無關。
  2. 在一個事務中,輸入可以引用多個事務的輸出。
  3. 輸入必須引用輸出。

在整篇文章中,我們將使用“錢”,“硬幣”,“花”,“傳送”,“帳戶”等詞語。但比特幣中沒有這樣的概念。事務只是用指令碼鎖定值,只能由鎖定它們的人解鎖。

Transaction Outputs

讓我們先從輸出開始:

type TXOutput struct {
	Value        int
	ScriptPubKey string
}
複製程式碼

實際上,它的輸出儲存“硬幣”(請注意Value上面的欄位)。並且儲存意味著用謎題鎖定它們,謎題儲存在謎題中ScriptPubKey。在內部,比特幣使用一種叫做的指令碼語言

Script

,用於定義輸出鎖定和解鎖邏輯。這種語言非常原始(這是故意製作的,以避免可能的黑客攻擊和濫用),但我們不會詳細討論。你可以找到它的詳細解釋here.

In Bitcoin, value欄位儲存的數量satoshis,而不是BTC的數量。一個satoshi是比特幣的百萬分之一(0.00000001 BTC),因此這是比特幣中最小的貨幣單位(如一分錢)。

由於我們沒有實現地址,因此我們現在將避免使用與指令碼相關的整個邏輯。ScriptPubKey將儲存任意字串(使用者定義的錢包地址)。

順便說一句,擁有這樣的指令碼語言意味著比特幣也可以用作智慧合約平臺。

關於產出的一個重要問題是它們是不可分割的,這意味著你不能引用它的一部分價值。在新事務中引用輸出時,它將作為一個整體使用。如果其值大於要求,則會生成更改並將其傳送回發件人。這類似於現實世界的情況,當你支付5美元的鈔票,價格為1美元並且變化4美元。

Transaction Inputs

這是輸入:

type TXInput struct {
	Txid      []byte
	Vout      int
	ScriptSig string
}
複製程式碼

如前所述,輸入引用了以前的輸出:Txid儲存此類交易的ID,以及Vout儲存事務中輸出的索引。ScriptSig是一個指令碼,提供要在輸出中使用的資料ScriptPubKey。如果資料正確,則可以解鎖輸出,並且可以使用其值來生成新輸出;如果不正確,則無法在輸入中引用輸出。這是保證使用者不能花錢屬於其他人的機制。

再說一遍,既然我們還沒有實現地址,ScriptSig將只儲存任意使用者定義的錢包地址。我們將在下一篇文章中實現公鑰和簽名檢查。

讓我們總結一下。輸出是儲存“硬幣”的地方。每個輸出都帶有一個解鎖指令碼,它確定解鎖輸出的邏輯。每個新事務必須至少有一個輸入和輸出。輸入引用先前事務的輸出並提供資料(ScriptSig欄位)在輸出的解鎖指令碼中用於解鎖它並使用其值來建立新輸出。

但首先是:輸入還是輸出?

The egg

在比特幣中,它是雞肉之前的雞蛋。輸入 - 參考 - 輸出邏輯是經典的“雞肉或雞蛋”情況:輸入產生輸出和輸出使輸入成為可能。在比特幣中,輸出在輸入之前。

當一個礦工開始挖掘一個區塊時,它會為它新增一個coinbase transaction。 coinbase transaction是一種特殊型別的事務,不需要以前存在的輸出。它無處不在地創造輸出(即“硬幣”)。雞蛋沒有雞肉。這是礦工開採新區塊的獎勵。

如你所知,區塊鏈開頭就有創世塊。正是這個塊在區塊鏈中生成了第一個輸出。並且不需要先前的輸出,因為沒有先前的交易且沒有這樣的輸出。

讓我們建立一個coinbase transaction

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 transaction只有一個輸入。在我們的實施中Txid 為空並且Vout等於-1。此外,coicoinbase transaction不會儲存指令碼ScriptSig。相反,任意資料儲存在那裡。

在比特幣中,第一個coinbase transaction包含以下資訊:“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”。You can see it yourself.

subsidy是獎勵金額。在比特幣中,此數字不儲存在任何地方,僅基於塊總數計算:塊數除以210000。挖掘成因塊產生了50個BTC,並且每個210000阻止獎勵減半。在我們的實施中,我們將獎勵儲存為常數(至少現在為止)。

在區塊鏈中儲存交易

從現在開始,每個塊必須至少儲存一個事務,並且不可能在沒有事務的情況下挖掘塊。這意味著我們應該刪除 Block的Data欄位而是儲存交易:

type Block struct {
	Timestamp     int64
	Transactions  []*Transaction
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}
複製程式碼

NewBlock and NewGenesisBlock也必須相應改變:

func NewBlock(transactions []*Transaction, prevBlockHash []byte) *Block {
	block := &Block{time.Now().Unix(), transactions, prevBlockHash, []byte{}, 0}
	...
}

func NewGenesisBlock(coinbase *Transaction) *Block {
	return NewBlock([]*Transaction{coinbase}, []byte{})
}
複製程式碼

接下來要改變的是建立一個新的區塊鏈:

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

		b, err := tx.CreateBucket([]byte(blocksBucket))
		err = b.Put(genesis.Hash, genesis.Serialize())
		...
	})
	...
}
複製程式碼

現在,該函式獲取一個地址,該地址將獲得挖掘生成塊的獎勵。

Proof-of-Work

工作證明演算法必須考慮儲存在塊中的事務,以保證區塊鏈作為事務儲存的一致性和可靠性。所以現在我們必須修改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
}
複製程式碼

Instead of pow.block.Data we now use pow.block.HashTransactions() which is:

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[:]
}
複製程式碼

同樣,我們使用雜湊作為提供資料唯一表示的機制。我們希望塊中的所有事務都由單個雜湊唯一標識。為了實現這一點,我們得到每個事務的雜湊值,連線它們,並得到連線組合的雜湊值。

比特幣使用更精細的技術:它將包含在塊中的所有事務表示為Merkle tree並在Proof-of-Work系統中使用樹的根雜湊。此方法允許快速檢查塊是否包含特定事務,僅具有根雜湊並且不下載所有事務。

我們到目前為止檢查一切是否正確:

$ blockchain_go createblockchain -address Ivan
00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a

Done!
複製程式碼

好!我們收到了第一次採礦獎勵。但是我們如何檢查結餘?

未花費的交易輸出

我們需要找到所有未使用的事務輸出(UTXO)。

Unspent

表示這些輸出未在任何輸入中引用。在上圖中,這些是:

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

當然,當我們檢查餘額時,我們不需要所有這些,但只有那些可以通過我們擁有的金鑰解鎖的那些(目前我們沒有實現金鑰,而是將使用使用者定義的地址)。首先,讓我們在輸入和輸出上定義鎖定解鎖方法:

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

func (out *TXOutput) CanBeUnlockedWith(unlockingData string) bool {
	return out.ScriptPubKey == 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
}
複製程式碼

由於事務儲存在塊中,我們必須檢查區塊鏈中的每個塊。我們從輸出開始:

if out.CanBeUnlockedWith(address) {
	unspentTXs = append(unspentTXs, tx)
}
複製程式碼

如果輸出被同一地址鎖定,我們正在搜尋未使用的事務輸出,那麼這就是我們想要的輸出。但在獲取之前,我們需要檢查輸入中是否已引用輸出:

if spentTXOs[txID] != nil {
	for _, spentOut := range spentTXOs[txID] {
		if spentOut == outIdx {
			continue Outputs
		}
	}
}
複製程式碼

我們跳過在輸入中引用的那些(它們的值被移動到其他輸出,因此我們無法計算它們)。檢查輸出後,我們收集所有可以解鎖使用提供的地址鎖定的輸出的輸入(這不適用於coinbase事務,因為它們不解鎖輸出):

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)
        }
    }
}
複製程式碼

該函式返回包含未使用輸出的事務列表。為了計算餘額,我們需要另外一個函式來獲取事務並僅返回輸出:

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
}
複製程式碼

而已!現在我們可以實施getbalance 命令:

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

複製程式碼

帳戶餘額是帳戶地址鎖定的所有未使用的交易輸出的值的總和。

我們在挖掘起源塊後檢查我們的結餘:

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 10
複製程式碼

這是我們的第一筆錢!

Sending Coins

現在,我們想向其他人傳送一些硬幣。為此,我們需要建立一個新事務,將其放在一個塊中,然後挖掘塊。到目前為止,我們只實現了coinbase transaction(這是一種特殊型別的事務),現在我們需要一個通用事務:

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方法所做的工作。之後,對於每個找到的輸出,建立引用它的輸入。接下來,我們建立兩個輸出:

  1. 一個與接收者地址鎖定的。這是將硬幣實際轉移到其他地址。
  2. 一個與發件人地址鎖定的。這是一個變化。它僅在未使用的輸出儲存的值超過新事務所需的值時建立。記住:輸出是不可分割的.

FindSpendableOutputs方法是基於FindUnspentTransactions(我們之前定義的)方法:

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
}

複製程式碼

該方法迭代所有未花費的事務並累積其值。當累計值大於或等於我們想要轉移的金額時,它會停止並返回按交易ID分組的累計值和輸出索引。我們不想花費超過我們將花費的。

現在我們可以修改Blockchain.MineBlock 方法:

func (bc *Blockchain) MineBlock(transactions []*Transaction) {
	...
	newBlock := NewBlock(transactions, lastHash)
	...
}
複製程式碼

最後,讓我們來實現send 命令:

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

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

傳送硬幣意味著建立交易並通過挖掘塊將其新增到區塊鏈。但比特幣不會立即這樣做(就像我們一樣)。相反,它將所有新事務放入記憶體池(或mempool),當礦工準備挖掘塊時,它會從mempool獲取所有事務並建立候選塊。僅當包含它們的塊被挖掘並新增到區塊鏈時,才會確認事務。

讓我們檢查傳送硬幣是否有效:

$ blockchain_go send -from Ivan -to Pedro -amount 6
00000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 4

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 6
複製程式碼

太好了!現在,讓我們建立更多事務並確保從多個輸出傳送工作正常:

$ blockchain_go send -from Pedro -to Helen -amount 2
00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf

Success!

$ blockchain_go send -from Ivan -to Helen -amount 2
000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa

Success!
複製程式碼

現在,海倫的硬幣鎖定在兩個輸出中:一個來自佩德羅,一個來自伊萬。讓我們把它們發給別人:

$ blockchain_go send -from Helen -to Rachel -amount 3
000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0

Success!

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Helen
Balance of 'Helen': 1

$ blockchain_go getbalance -address Rachel
Balance of 'Rachel': 3
複製程式碼

看起來很好!現在讓我們測試失敗:

$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds

$ blockchain_go getbalance -address Pedro
Balance of 'Pedro': 4

$ blockchain_go getbalance -address Ivan
Balance of 'Ivan': 2
複製程式碼

Conclusion

唷!這不容易,但我們現在有交易!雖然缺少類比特幣加密貨幣的一些關鍵特徵:

  1. 地址。我們還沒有真正的基於私鑰的地址。
  2. 獎勵。採礦區絕對沒有利潤!
  3. UTXO設定。獲得平衡需要掃描整個區塊鏈,這可能需要很長時間才能有很多塊。此外,如果我們想要驗證以後的事務,可能需要很長時間。 UTXO集旨在解決這些問題並快速進行事務處理。
  4. 記憶體池。這是在塊打包之前儲存事務的地方。在我們當前的實現中,塊只包含一個事務,這是非常低效的。

英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-4/


更多文章歡迎訪問 http://www.apexyun.com/

聯絡郵箱:public@space-explore.com

(未經同意,請勿轉載)


相關文章