- Goのブロックチェーン。 パート1:プロトタイプ
- Goのブロックチェーン。 パート2:作業証明
- Goのブロックチェーン。 パート3:読み取り専用メモリとコマンドラインインターフェイス
- Goのブロックチェーン。 パート4:トランザクション、パート1
- Goのブロックチェーン。 パート5:アドレス
- Goのブロックチェーン。 パート6:トランザクション、パート2
- Goのブロックチェーン。 パート7:ネットワーク
エントリー
前のパートでは、PoWシステムとマイニングの可能性を備えたブロックチェーンを構築しました。 私たちの実装は完全に機能するブロックチェーンに近づいていますが、それでもいくつかの重要な機能が欠けています。 本日、データベースにブロックチェーンを保存し始め、その後、ブロックチェーンを操作するためのコマンドラインインターフェースを作成します。 本質的に、ブロックチェーンは分散データベースです。 ここでは、「分散」を省略し、「データベース」に焦点を当てます。
データベース選択
これまでのところ、実装にデータベースはありません。プログラムの起動時にブロックを作成し、メモリに保存するだけです。 ブロックチェーンを再利用したり他の人と共有したりすることはできないため、ディスクに保存する必要があります。
どのデータベースが必要ですか? 実際、誰でもできます。 Bitcoin Paperは特定のデータベースについて何も述べていないため、選択は開発者次第です。 もともと中本Sによって発行され、現在はビットコインのリファレンス実装であるビットコインコアは 、 LevelDBを使用します (ただし、2012年にのみクライアントに導入されました)。 そして、我々は使用します...
Boltdb
なぜなら:
- 彼女はシンプルでミニマルです。
- Goで実装されています
- 彼女はサーバーを起動する必要はありません
- これにより、必要なデータ構造を構築できます。
BoltDBのREADMEから:
Boltは、Howard ChuのLMDBプロジェクトに触発された単なるキーバリューリポジトリです。 プロジェクトの目標は、PostgresやMySQLなどの完全なデータベースサーバーを必要としないプロジェクトに、シンプルで高速かつ信頼性の高いデータベースを提供することです。私たちのニーズに最適です! 少し時間をかけてベースを確認してください。
Boltはこのような低レベルの機能要素として使用することを目的としているため、シンプルさが重要です。 APIは小さく、値の取得と値の設定のみに焦点を合わせます。 以上です!
BoltDBはキーと値のストレージです。つまり、リレーショナルDBMS(MySQL、PostgreSQLなど)には行と列がないため、テーブルはありません。 代わりに、データはキーと値のペアで保存されます(Golangマップのように)。 ペアは、類似のペアをグループ化するために設計された「リレーショナルバスケット」に保存されます(リレーショナルDBMSのテーブルに類似)。 したがって、値を取得するには、バスケットとキーを知る必要があります。
BoltDBの重要な点は、ここにはデータ型がないことです。キーと値はバイト配列です。 Go構造(特に
Block
)を格納するため、それらをシリアル化する必要があります。つまり、構造をバイト配列に変換し、配列から復元するメカニズムを実装する必要があります。
JSON, XML, Protocol Buffers
も適してい
JSON, XML, Protocol Buffers
これにはエンコーディング/ゴブを使用します。
encoding/gob
は、シンプルでGo標準ライブラリの一部であるため、使用します。
データベース構造
永続ロジックの実装を開始する前に、データベースにデータを保存する方法を決定する必要があります。 このために、ビットコインコアを使用する方法を使用します。
簡単な方法であれば、ビットコインコアは2つの「バスケット」を使用してデータを保存します。
-
blocks
は、チェーン内のすべてのブロックを記述するメタデータが格納されます - chainstateは、チェーンの状態を格納します。これは、未使用のトランザクション出力と一部のメタデータです
ブロックもディスク上の個別のファイルとして保存されます。 これは、パフォーマンスを向上させるために行われます。単一のブロックを読み取るには、すべて(または一部)をメモリにロードする必要はありません。 これは実装しません。
blocks
key->value
ペアは次のとおりです。
-
'b' + 32- -> 'f' + 4- -> 'l' -> 4- : 'R' -> 1- boolean : 'F' + 1- + -> 1 boolean: , 't' + 32- ->
-
'b' + 32- -> 'f' + 4- -> 'l' -> 4- : 'R' -> 1- boolean : 'F' + 1- + -> 1 boolean: , 't' + 32- ->
-
'b' + 32- -> 'f' + 4- -> 'l' -> 4- : 'R' -> 1- boolean : 'F' + 1- + -> 1 boolean: , 't' + 32- ->
-
'b' + 32- -> 'f' + 4- -> 'l' -> 4- : 'R' -> 1- boolean : 'F' + 1- + -> 1 boolean: , 't' + 32- ->
-
'b' + 32- -> 'f' + 4- -> 'l' -> 4- : 'R' -> 1- boolean : 'F' + 1- + -> 1 boolean: , 't' + 32- ->
-
'b' + 32- -> 'f' + 4- -> 'l' -> 4- : 'R' -> 1- boolean : 'F' + 1- + -> 1 boolean: , 't' + 32- ->
'b' + 32- -> 'f' + 4- -> 'l' -> 4- : 'R' -> 1- boolean : 'F' + 1- + -> 1 boolean: , 't' + 32- ->
chainstate
key->value
ペアは次のとおりです。
-
'c' + 32- -> 'B' -> 32- : ,
-
'c' + 32- -> 'B' -> 32- : ,
'c' + 32- -> 'B' -> 32- : ,
( 詳細な説明はここにあります )
まだトランザクションがないため、
blocks
バスケットのみを作成し
blocks
。 さらに、上記のように、データベース全体を1つのファイルに保存し、ブロックを個別のファイルに保存しません。 したがって、ファイル番号に関連するものは必要ありません。 したがって、使用する
key->value
ペアは次のとおりです。
- 32バイトブロックハッシュ->ブロック構造(シリアル化)
- 'l'->チェーンの最後のブロックのハッシュ
これが、恒常性(永続性)のメカニズムを実装するために知る必要があるすべてです。
連載
前述したように、BoltDBでは値は
[]byte
タイプのみであり、データベースに
Block
構造を格納する必要があります。
encoding/gob
を使用して構造をシリアル化します。
Block
の
Serialize
メソッドを実装しましょう(簡潔にするためにエラー処理は省略します)
func (b *Block) Serialize() []byte { var result bytes.Buffer encoder := gob.NewEncoder(&result) err := encoder.Encode(b) return result.Bytes() }
ここではすべてが簡単です。最初に、シリアル化されたデータを保存するバッファーを宣言し、
gob
エンコーダーを初期化してブロックをエンコードします。結果はバイトの配列として返されます。
次に、バイトの配列を受け取って
Block
を返す逆シリアル化関数が必要です。 これはメソッドではなく、独立した機能です。
func DeserializeBlock(d []byte) *Block { var block Block decoder := gob.NewDecoder(bytes.NewReader(d)) err := decoder.Decode(&block) return &block }
シリアル化に必要なのはそれだけです。
持続性
NewBlockchain
関数から始めましょう。 現在、彼女は
Blockchain
新しいインスタンスを作成し、それにジェネシスブロックを追加しています。 次のことを行います。
- dbファイルを開く
- ブロックチェーンがそこに保存されているかどうかを確認します
- 彼がいる場合:
- 新しい
Blockchain
インスタンスを作成する -
Blockchain
インスタンスのヒントを、データベースに保存されている最後のブロックのハッシュに設定します
- 新しい
- 既存のブロックチェーンがない場合
- 創世記ブロックを作成
- DBに保存
- ジェネシスハッシュを最後の最後のブロックのハッシュとして保存
- ブロックの起源を示すヒントを使用して、新しい
Blockchain
インスタンスを作成します
コードでは、次のようになります。
func NewBlockchain() *Blockchain { var tip []byte db, err := bolt.Open(dbFile, 0600, nil) err = db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) if b == nil { genesis := NewGenesisBlock() b, err := tx.CreateBucket([]byte(blocksBucket)) err = b.Put(genesis.Hash, genesis.Serialize()) err = b.Put([]byte("l"), genesis.Hash) tip = genesis.Hash } else { tip = b.Get([]byte("l")) } return nil }) bc := Blockchain{tip, db} return &bc }
コードを部分的に分析しましょう。
db, err := bolt.Open(dbFile, 0600, nil)
これは、BoltDBファイルを開く標準的な方法です。 ファイルがない場合、エラーを返さないことに注意してください。
err = db.Update(func(tx *bolt.Tx) error { ... })
BoltDBでは、データベース操作はトランザクションの一部として実行されます。 トランザクションには、読み取り専用と読み取り/書き込みの2つのタイプがあります。 ここでは、データベースに
(db.Update(...))
ブロックを配置する予定なので、読み取り/書き込みトランザクション
(db.Update(...))
を開きます。
b := tx.Bucket([]byte(blocksBucket)) if b == nil { genesis := NewGenesisBlock() b, err := tx.CreateBucket([]byte(blocksBucket)) err = b.Put(genesis.Hash, genesis.Serialize()) err = b.Put([]byte("l"), genesis.Hash) tip = genesis.Hash } else { tip = b.Get([]byte("l")) }
これが機能の中核です。 ここで、ブロックを保存するバスケットを取得します:存在する場合は、そこからキー
l
を読み取り、存在しない場合は、ブロックの生成を生成し、バスケットを作成し、その中にブロックを保存し、チェーンの最後のブロックのハッシュを保存するキー
l
を更新します
Blockchain
を作成する新しい方法にも注目してください。
bc := Blockchain{tip, db}
すべてのブロックを保存するのではなく、チェーンの先端のみを保存します。 データベースへの接続も保存します。これは、一度開いて、プログラムの実行中は開いたままにするためです。 これは、
Blockchain
構造がどのように見えるかです:
type Blockchain struct { tip []byte db *bolt.DB }
次に変更するのは
AddBlock
メソッドです。チェーンにブロックを追加することは、配列に要素を追加するほど簡単ではありません。 これから、データベースにブロックを保存します。
func (bc *Blockchain) AddBlock(data string) { var lastHash []byte err := bc.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) lastHash = b.Get([]byte("l")) return nil }) newBlock := NewBlock(data, lastHash) err = bc.db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) err := b.Put(newBlock.Hash, newBlock.Serialize()) err = b.Put([]byte("l"), newBlock.Hash) bc.tip = newBlock.Hash return nil }) }
コードを1つずつ検討してください。
err := bc.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) lastHash = b.Get([]byte("l")) return nil })
これは、BoltDBトランザクションの異なる(読み取り専用)タイプです。 ここで、新しいブロックのハッシュをマイニングするために使用するデータベースから最後のブロックのハッシュを取得します。
newBlock := NewBlock(data, lastHash) b := tx.Bucket([]byte(blocksBucket)) err := b.Put(newBlock.Hash, newBlock.Serialize()) err = b.Put([]byte("l"), newBlock.Hash) bc.tip = newBlock.Hash
新しいブロックをマイニングした後、データベースにシリアル化されたビューを保存し、新しいブロックのハッシュを保存するキー
l
を更新します。
できた! 難しくありませんでしたね。
ブロックチェーンの確認
すべての新しいブロックがデータベースに保存されるようになったため、ブロックチェーンを再度開いて新しいブロックを追加できます。 ただし、これを実装すると、便利な機能が1つ失われます。ブロックを配列に格納しなくなるため、ブロックを印刷できなくなります。 それを修正しましょう。
BoltDBを使用すると、バスケット内のすべてのキーを確認できますが、すべてのキーはバイトでソートされて保存され、ブロックチェーンに配置された順序でブロックを印刷する必要があります。 また、すべてのブロックをメモリにロードする必要はないため(ブロックチェーンは非常に巨大になる可能性があります)、それらを1つずつ読み取ります。 この目的のために、ブロックチェーンイテレータが必要です。
type BlockchainIterator struct { currentHash []byte db *bolt.DB }
イテレータは、ブロックチェーン内のブロックを反復処理するたびに作成され、現在の反復ブロックのハッシュとデータベース接続を保存します。 後者のため、イテレータは論理的にブロックチェーンにバインドされています(これはデータベースへの接続を保存するブロックチェーンのインスタンスです)。したがって、
Blockchain
メソッドで作成されます。
func (bc *Blockchain) Iterator() *BlockchainIterator { bci := &BlockchainIterator{bc.tip, bc.db} return bci }
イテレータは最初にブロックチェーンの先端を指すので、ブロックは上から下、最新から古いものの順に受信されることに注意してください。 実際、 チップを選択するということは、ブロックチェーンに対する「投票」を意味します。 ブロックチェーンには複数のブランチを含めることができ、そのうちの最も長いブランチがメインブランチと見なされます。 ヒント(ブロックチェーン上の任意のブロック)を受け取った後、ブロックチェーン全体を再作成し、その長さ、および構築に必要な作業を見つけることができます。 この事実は、チップが一種のブロックチェーン識別子であることも意味します。
BlockchainIterator
が行うことは1つだけです。ブロックチェーンから次のブロックを返します。
func (i *BlockchainIterator) Next() *Block { var block *Block err := i.db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(blocksBucket)) encodedBlock := b.Get(i.currentHash) block = DeserializeBlock(encodedBlock) return nil }) i.currentHash = block.PrevBlockHash return block }
データベースについては以上です!
コマンドラインインターフェース(CLI)
これまでのところ、実装はプログラムと対話するためのインターフェイスを提供していません
NewBlockchain, bc.AddBlock
ました。 それを改善する時が来ました! 次のコマンドが必要です。
blockchain_go addblock "Pay 0.031337 for a coffee" blockchain_go printchain
すべてのコマンドライン関連の操作は、
CLI
構造によって処理され
CLI
type CLI struct { bc *Blockchain }
構造の「エントリポイント」は
Run
関数
Run
func (cli *CLI) Run() { cli.validateArgs() addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError) printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) addBlockData := addBlockCmd.String("data", "", "Block data") switch os.Args[1] { case "addblock": err := addBlockCmd.Parse(os.Args[2:]) case "printchain": err := printChainCmd.Parse(os.Args[2:]) default: cli.printUsage() os.Exit(1) } if addBlockCmd.Parsed() { if *addBlockData == "" { addBlockCmd.Usage() os.Exit(1) } cli.addBlock(*addBlockData) } if printChainCmd.Parsed() { cli.printChain() } }
標準フラグパッケージを使用して、コマンドライン引数を解析します。
addBlockCmd := flag.NewFlagSet("addblock", flag.ExitOnError) printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) addBlockData := addBlockCmd.String("data", "", "Block data")
まず、2つの
addblock
および
printchain
を作成し、最初に
printchain
フラグを追加し
-data
。
printchain
はフラグを必要としません。
switch os.Args[1] { case "addblock": err := addBlockCmd.Parse(os.Args[2:]) case "printchain": err := printChainCmd.Parse(os.Args[2:]) default: cli.printUsage() os.Exit(1) }
次に、ユーザーが指定したコマンドを確認し、関連するサブコマンドを解析します。
if addBlockCmd.Parsed() { if *addBlockData == "" { addBlockCmd.Usage() os.Exit(1) } cli.addBlock(*addBlockData) } if printChainCmd.Parsed() { cli.printChain() }
次に、解析したサブコマンドをチェックし、関連する関数を実行します。
func (cli *CLI) addBlock(data string) { cli.bc.AddBlock(data) fmt.Println("Success!") } func (cli *CLI) printChain() { bci := cli.bc.Iterator() for { block := bci.Next() fmt.Printf("Prev. hash: %x\n", block.PrevBlockHash) fmt.Printf("Data: %s\n", block.Data) fmt.Printf("Hash: %x\n", block.Hash) pow := NewProofOfWork(block) fmt.Printf("PoW: %s\n", strconv.FormatBool(pow.Validate())) fmt.Println() if len(block.PrevBlockHash) == 0 { break } } }
このコードは以前のものと似ています。 唯一の違いは、
BlockchainIterator
を使用してブロックチェーン内のブロックを反復処理することです。
また、それに応じて
main
関数を変更することを忘れないでください:
func main() { bc := NewBlockchain() defer bc.db.Close() cli := CLI{bc} cli.Run() }
どのコマンドライン引数が渡されたかに関係なく、新しい
Blockchain
が作成されることに注意してください。
以上です! すべてが期待どおりに機能することを確認します。
$ blockchain_go printchain No existing blockchain found. Creating a new one... Mining the block containing "Genesis Block" 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b Prev. hash: Data: Genesis Block Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b PoW: true $ blockchain_go addblock -data "Send 1 BTC to Ivan" Mining the block containing "Send 1 BTC to Ivan" 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 Success! $ blockchain_go addblock -data "Pay 0.31337 BTC for a coffee" Mining the block containing "Pay 0.31337 BTC for a coffee" 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148 Success! $ blockchain_go printchain Prev. hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 Data: Pay 0.31337 BTC for a coffee Hash: 000000aa0748da7367dec6b9de5027f4fae0963df89ff39d8f20fd7299307148 PoW: true Prev. hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b Data: Send 1 BTC to Ivan Hash: 000000d7b0c76e1001cdc1fc866b95a481d23f3027d86901eaeb77ae6d002b13 PoW: true Prev. hash: Data: Genesis Block Hash: 000000edc4a82659cebf087adee1ea353bd57fcd59927662cd5ff1c4f618109b PoW: true
( ビール缶を開ける音 )
参照資料
オリジナル記事
一連の記事の最初の部分
ソースコード
ビットコインコアデータストレージ
Boltdb
エンコーディング/ゴブ
旗