Goのブロックチェーン。 パート4:トランザクション、パート1

こんにちはHabr! 記事「 Goでブロックチェーンを構築する。パート4:トランザクション1 」の翻訳を紹介します。



内容



  1. Goのブロックチェーン。 パート1:プロトタイプ
  2. Goのブロックチェーン。 パート2:作業証明
  3. Goのブロックチェーン。 パート3:読み取り専用メモリとコマンドラインインターフェイス
  4. Goのブロックチェーン。 パート4:トランザクション、パート1
  5. Goのブロックチェーン。 パート5:アドレス
  6. Goのブロックチェーン。 パート6:トランザクション、パート2
  7. Goのブロックチェーン。 パート7:ネットワーク


エントリー



トランザクションはビットコインの心臓部であり、ブロックチェーンの唯一の目的は、トランザクションを安全で信頼できる方法で保存し、作成後に誰もトランザクションを変更できないようにすることです。 この記事では、トランザクションメカニズムの実装に関する作業を開始します。 しかし、これはかなり大きなトピックなので、2つの部分に分けました。この部分では一般的なメカニズムを実装し、2番目の部分では残りの機能を詳細に分析します。



この記事では、以前のすべてのコードをほぼ完全に編集するため、各変更を説明する意味はありません。すべての変更をここで確認できます



スプーンなし



一度Webアプリケーションを開発した場合、支払いの実装のために、おそらくデータベースにこれらのテーブルのうち2つを作成しました:







。 アカウントには、ユーザーに関する個人情報や残高などの情報が保存され、トランザクションには、あるアカウントから別のアカウントへの送金に関する情報が保存されます。 ビットコインでは、支払いはまったく異なる方法で行われます。



  1. アカウントがありません。
  2. 残高はありません。
  3. 住所はありません。
  4. コインなし。
  5. 送信者と受信者はいません。


ブロックチェーンは公開されたオープンデータベースであるため、ウォレットの所有者に関する機密情報を保存する必要はありません。 コインはアカウントに保存されません。 トランザクションは、あるアドレスから別のアドレスにお金を転送しません。 口座残高を含むフィールドと属性はありません。 トランザクションのみがあります。 しかし、中身は何ですか?



ビットコイン取引



トランザクションは、入力と出力の組み合わせです。



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





新しいトランザクションの入力は、前のトランザクションの出力を参照します(例外があります。これについては以下で説明します)。 出口-コインが保管される場所。 次の図は、トランザクションの関係を示しています。







注:



  1. 入力に接続されていない出力があります。
  2. 1つのトランザクションで、入力は複数のトランザクションの出力を参照できます。
  3. 入力は常に出力を参照する必要があります。


この記事では、「お金」、「コイン」、「支出」、「送信」、「アカウント」などの言葉を使用します。 しかし、ビットコインにはそのような概念はありません。 トランザクションは、スクリプトによってロックされた値であり、ブロックしたユーザーのみがロックを解除できます。



トランザクション出力



出口から始めましょう:



 type TXOutput struct { Value int ScriptPubKey string }
      
      





実際、これらは「コイン」を格納する出力です(上記の[ Value



フィールドに注意してください)。 資金は、 ScriptPubKey



保存されている特別なパズルによってブロックされScriptPubKey



。 内部では、ビットコインはスクリプト言語Script



使用します。これは、出力をロックおよびロック解除するためのロジックを決定するために使用されます。 この言語は非常に原始的です(これは意図的なハッキングを避けるために意図的に行われます)が、詳細については説明しません。 ここで彼についてもっと読むことができます

ビットコインでは、値フィールドにはBTCの数ではなく、Satoshiの量が格納されます。 1 Satoshi = 0.00000001 BTC。 したがって、これはビットコインの通貨の最小単位(たとえば、セント)です。

アドレスがないため、今のところ、すべてのスクリプトロジックを回避します。 まず、 ScriptPubKey



は任意の文字列(ユーザーウォレットアドレス)を格納します。

ところで、このようなスクリプト言語の存在は、ビットコインをスマートコントラクトのプラットフォームとして使用できることを意味します。

出口について知っておくべき重要なことの1つは、出口が分割できないことです。つまり、意味の一部を参照することはできません。 出口が新しいトランザクションを指す場合、それは完全に消費されます。 そして、その値が必要以上に大きい場合、差が生成され、新しい値が送信者に送り返されます。 これは、1ドルの費用に対して5ドルを支払い、4ドルの変更を得た場合の世界の実際の状況に似ています。



トランザクション入力



入力を考慮してください:



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





前述のように、エントリは前の出口を参照しますTxid



はそのようなトランザクションの識別子を格納し、 Vout



はこのトランザクションの出口インデックスを格納します。 ScriptSig



は、 ScriptPubKey



スクリプトでさらに使用されるデータを提供するスクリプトです。 データが正しい場合、出力のロックを解除し、その値を使用して新しい出力を生成できます。 そうでない場合、入力は出力を参照できません。 このメカニズムにより、ユーザーは他の人のコインを使うことができなくなります。



繰り返しますが、まだアドレスがないため、 ScriptSig



任意のユーザーウォレットアドレスのみScriptSig



保存します。 次の記事で公開鍵と署名の検証を作成します。



まとめると。 出口-これは「コイン」が保管される場所です。 各出力には、出力のロックを解除するためのロジックを定義するロック解除スクリプトがあります。 それぞれの新しいトランザクションには、少なくとも1つの入口と出口が必要です。 入力は前のトランザクションの結果を参照し、出力でロックを解除するためにスクリプトで使用されるデータ( ScriptSig



フィールド)を提供してロックを解除し、その値を使用して新しい出力を作成します。



しかし、最初に来たのは入力または出力ですか?





ビットコインでは、鶏の前に卵が現れました。 論理入力-参照-出力は、古典的な「鶏または卵」の状況です。入力は出力を生成し、出力は入力を作成できます。 また、ビットコインでは、出口は常に入口の前に表示されます。



鉱夫がブロックの採掘を開始すると、彼はそれにコインベーストランザクションを追加します。 コインベーストランザクションは、既存の出口を必要としない特別なタイプのトランザクションです。 どこからでも出口(つまり、「コイン」)を作成します。 鶏肉のない卵。 これは、鉱夫が新しいブロックを採掘することで得られる報酬です。



ご存知のように、連鎖の始まりには、起源のブロックがあります。 ブロックチェーンの最初の出力を生成するのはこのブロックです。 また、以前のトランザクションや出口がないため、以前の出口は必要ありません。



コインベーストランザクションを作成しましょう。



 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 }
      
      





コインベーストランザクションには1つのエントリしかありません。 実装では、 Txid



空で、 Vout



は-1です。 さらに、coinbaseトランザクションはScriptSig



スクリプトを保存しません。 代わりに、任意のデータがそこに保存されます。

ビットコインでは、最初のコインベーストランザクションには次のメッセージが含まれています:「The Times 03 / Jan / 2009 Chancellor on second bail out for bail out for bank」。 あなた自身がそれを見ることができます

subsidy



は報酬の額です。 ビットコインでは、この数はどこにも保存されず、ブロックの総数に基づいてのみ計算されます。ブロックの数は210,000で除算されます。 ジェネシスブロックをマイニングすると50 BTCがもたらされ、210,000ブロックごとに報酬が半分になります。 実装では、報酬を定数として保存します(少なくとも現時点では)。



チェーンでトランザクションを保存する



これ以降、各ブロックには少なくとも1つのトランザクションを格納する必要があり、トランザクションなしでブロックをマイニングすることは不可能になります。 次に、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} ... } func NewGenesisBlock(coinbase *Transaction) *Block { return NewBlock([]*Transaction{coinbase}, []byte{}) }
      
      





CreateBlockchain



関数を作成します



 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



メソッドを変更する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 }
      
      





pow.block.Data



代わりにpow.block.Data



pow.block.HashTransactions ()



追加しpow.block.HashTransactions ()







 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[:] }
      
      





この場合も、データの一意の表現を提供するメカニズムとしてハッシュを使用します。 ブロック内のすべてのトランザクションが単一のハッシュで一意に識別されるようにします。 これを実現するために、各トランザクションのハッシュを取得し、それらを組み合わせて、組み合わせた組み合わせのハッシュを取得します。

ビットコインは、より複雑な手法を使用します。ブロックに含まれるすべてのトランザクションをハッシュツリーとして表し、Proof-of-Workシステムのツリーのルートハッシュを使用します。 このアプローチにより、すべてのトランザクションをロードすることなく、ブロックにルートハッシュのみを持つ特定のトランザクションが含まれているかどうかをすばやく確認できます。
正しい動作を確認します。



 $ blockchain_go createblockchain -address Ivan 00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8a Done!
      
      





いいね! 最初の賞を受賞しました。 しかし、どのように残高を確認しますか?



未使用の出口



すべての未使用出力(UTXO)を見つける必要があります。 これは、これらの出力が入力を参照しなかったことを意味します。 上の図では、これは次のとおりです。



  1. tx0、出力1。
  2. tx1、出力0;
  3. tx3、出力0;
  4. tx4、出力0。


もちろん、残高を確認するとき、すべてを必要とするわけではなく、所有しているキーでロック解除できるものだけが必要です(現在、キーは実装されておらず、代わりにユーザーアドレスが使用されます)。 始めるために、入力と出力でロック解除メソッドを定義しましょう:



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





ここでは、 ScriptSig



のフィールドとScriptSig



を比較するだけです。 次の記事では、秘密鍵に基づいてアドレスを実装した後、それらを改善します。



次のステップ-使用されていない出力を持つトランザクションを見つける-これはすでにより複雑です:



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





入力によって既に参照されているものはスキップします(それらの値は他の出力に転送されているため、計算できません)。 出力を確認した後、指定されたアドレスでブロックされた出力をロック解除できるすべての入力を収集します(出力をロック解除しないため、コインベーストランザクションには適用されません)。



 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



コマンドを実装し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
      
      





これらは私たちの最初のコインです!



コインを送る



今、私たちは他の誰かにコインを送りたいです。 これを行うには、新しいトランザクションを作成し、ブロックに入れて処理する必要があります。 これまでは、コインベーストランザクション(特別なタイプのトランザクション)のみを実装していましたが、今度は一般的なトランザクションが必要です。



 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



メソッドです。 その後、見つかった出力ごとに、それを参照する入力が作成されます。 次に、2つの出力を作成します。



  1. 受信者アドレスでブロックされているもの。 これは、実際のコインの別のアドレスへの転送です。
  2. 送信者アドレスでブロックされているもの。 それが違いです。 新しいトランザクションに必要な出力よりも未使用の出力の方が重要な場合にのみ作成されます。 要確認:出力は分割不可です。


FindSpendableOutputs



メソッドFindSpendableOutputs



、前に定義したFindUnspentTransactions



メソッドに基づいてい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



コマンドを作成し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!
      
      





現在、ヘレンのコインは、ペドロからの出口とイヴァンからの出口の2つの出口でブロックされています。 他の人に送信:



 $ 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
      
      





おわりに



ふう! 簡単ではありませんでしたが、今ではトランザクションがあります! ただし、ビットコインのような暗号通貨のいくつかの主要な機能は欠落しています。



  1. 住所 これまでのところ、秘密鍵に基づくアドレスはありません。
  2. 賞。 地雷ブロックは絶対に採算が取れません!
  3. ウッチョ。 バランスを取るには、ブロックチェーン全体をスキャンする必要があり、非常に多くのブロックがある場合は非常に長い時間がかかることがあります。 さらに、後続のトランザクションを確認する場合、非常に長い時間がかかります。 UTXOは、これらの問題を解決し、トランザクションをすばやく処理するように設計されています。
  4. メンプール。 これは、トランザクションがブロックにパッケージ化される前に格納される場所です。 現在の実装では、ブロックに含まれるトランザクションは1つのみであり、これは非常に非効率的です。


参照資料



  1. 完全なソースコード
  2. 取引
  3. マークルツリー
  4. コインベース



All Articles