Introduction
交易是比特幣的核心,區塊鏈的唯一目的是以安全可靠的方式儲存交易,因此沒有人可以在建立交易後對其進行修改。今天我們開始實施交易。但是因為這是一個相當大的話題,我將它分為兩部分:在這部分中,我們將實現交易的一般機制,在第二部分,我們將通過細節進行處理。
此外,由於程式碼更改很大,因此在這裡描述所有程式碼都沒有意義。您可以看到所有更改here.
There is no spoon
如果您曾經開發過Web應用程式,為了實現付款,您可能會在資料庫中建立這些表:accounts
和 transactions
。帳戶將儲存關於使用者的資訊,包括他們的個人資訊和餘額,並且交易將儲存關於從一個帳戶轉移到另一個帳戶的錢的資訊。在比特幣中,支付以完全不同的方式實現。有:
- No accounts.
- No balances.
- No addresses.
- No coins.
- 沒有發件人和收件人。
由於區塊鏈是公共和開放資料庫,我們不希望儲存有關錢包所有者的敏感資訊。帳戶中不會收集硬幣。交易不會將錢從一個地址轉移到另一個地址。沒有儲存帳戶餘額的欄位或屬性。只有交易。但是交易中有什麼?
Bitcoin Transaction
交易是輸入和輸出的組合:
type Transaction struct {
ID []byte
Vin []TXInput
Vout []TXOutput
}
複製程式碼
輸入前一個事務的新事務引用輸出(雖然有一個例外,我們將在後面討論)。輸出是硬幣實際儲存的地方。下圖演示了事務的互連:
Notice that:
- 有些輸出與輸入無關。
- 在一個事務中,輸入可以引用多個事務的輸出。
- 輸入必須引用輸出。
在整篇文章中,我們將使用“錢”,“硬幣”,“花”,“傳送”,“帳戶”等詞語。但比特幣中沒有這樣的概念。事務只是用指令碼鎖定值,只能由鎖定它們的人解鎖。
Transaction Outputs
讓我們先從輸出開始:
type TXOutput struct {
Value int
ScriptPubKey string
}
複製程式碼
實際上,它的輸出儲存“硬幣”(請注意Value
上面的欄位)。並且儲存意味著用謎題鎖定它們,謎題儲存在謎題中ScriptPubKey
。在內部,比特幣使用一種叫做的指令碼語言
,用於定義輸出鎖定和解鎖邏輯。這種語言非常原始(這是故意製作的,以避免可能的黑客攻擊和濫用),但我們不會詳細討論。你可以找到它的詳細解釋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)。
表示這些輸出未在任何輸入中引用。在上圖中,這些是:
- tx0, output 1;
- tx1, output 0;
- tx3, output 0;
- 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
方法所做的工作。之後,對於每個找到的輸出,建立引用它的輸入。接下來,我們建立兩個輸出:
- 一個與接收者地址鎖定的。這是將硬幣實際轉移到其他地址。
- 一個與發件人地址鎖定的。這是一個變化。它僅在未使用的輸出儲存的值超過新事務所需的值時建立。記住:輸出是不可分割的.
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
唷!這不容易,但我們現在有交易!雖然缺少類比特幣加密貨幣的一些關鍵特徵:
- 地址。我們還沒有真正的基於私鑰的地址。
- 獎勵。採礦區絕對沒有利潤!
- UTXO設定。獲得平衡需要掃描整個區塊鏈,這可能需要很長時間才能有很多塊。此外,如果我們想要驗證以後的事務,可能需要很長時間。 UTXO集旨在解決這些問題並快速進行事務處理。
- 記憶體池。這是在塊打包之前儲存事務的地方。在我們當前的實現中,塊只包含一個事務,這是非常低效的。
英文原文:https://jeiwan.cc/posts/building-blockchain-in-go-part-4/
更多文章歡迎訪問 http://www.apexyun.com/
聯絡郵箱:public@space-explore.com
(未經同意,請勿轉載)