Go言語とテスト駆動開発(TDD)を使ってブロックチェーンの基本を実装してみよう

2023/04/24に公開

ブロックチェーン初心者向けに、Go 言語を使って基本的なブロックチェーンを実装する方法を紹介します。また、テスト駆動開発(TDD)の流れに沿った内容で解説しています。

ブロックチェーンの基本概念

ブロックチェーンは、取引(トランザクション)の記録を持つデータ構造で、分散型のデータベースとして機能します。

ブロックチェーンは、複数のブロックが連結されたリスト構造で構成されています。下図の緑の箱1つ1つがブロックを表しており、それぞれがハッシュ値で繋がっている構造になっているため、ブロックチェーンと呼ばれます。

各ブロックには、前のブロックのハッシュ値、Nonce 値、タイムスタンプ、およびトランザクションが含まれます。

詳しくは
https://zenn.dev/okuma/articles/582023ce2fe077
にまとめています。

ブロックが追加されるまでの流れ

ブロックが追加されるまでの流れを図で示します。

番号に沿って説明します。説明文の「利用ユーザー」とはブロックチェーンにトランザクションとしてデータを書き込みたいユーザーを指します。「マイナー」とは PoW によってブロックのハッシュ値を計算しトランザクションをブロックチェーンに追加するユーザーを指します。

  1. ブロックチェーンと初期ブロックの生成
    まずはブロックチェーン自体とそこに含める最初のブロックを生成します。
  2. トランザクションプールへのトランザクションの追加
    利用ユーザーが実行したトランザクションをトランザクションプール(未確認のトランザクションが一時的に保管されている場所)へ追加します。
  3. ブロックへのトランザクションの追加
    マイナーはトランザクションプールからトランザクションを選択し、ブロックに追加します。
  4. PoW によるハッシュ値計算
    マイナーは PoW(Proof of Work)を行い、適切な Nonce 値 を見つけ、ブロックのハッシュ値を計算します。詳細は先ほどと同じ記事 go で実装しながらブロックチェーンの PoW におけるハッシュ値計算を理解する にて説明しています。
  5. ブロックチェーンへの新たなブロックの追加
    ブロックチェーンへ新たなブロックを追加します。その際、1つ前のブロックのハッシュ値をブロックの情報として保存します。
  6. ブロックの検証
    追加されたブロックが正当であることを検証します。これはマイナーのノードではなく別のノードで実行されます。検証後は、そのブロックに含まれるトランザクションは「確認済み」となり、マイナーに報酬が支払われることになります。

以降では、この流れに則って実装を進めていきます。図と照らし合わせながら実装してみると理解の補助になると思います。

準備

実装を始める前に、Go 言語の環境が整っていることを確認してください。Go 言語のインストールや設定については、公式ドキュメントを参照してください。

まずは以下のコマンドを実行し、環境を初期化します。

# 専用のディレクトリを作成し、goのモジュール環境を初期化
mkdir blockchain-tdd-golang-for-learning
cd blockchain-tdd-golang-for-learning
go mod init blockchain-tdd-golang-for-learning

# 各種ファイルを作成
mkdir blockchain
touch blockchain/blockchain.go
touch blockchain/blockchain_test.go
  • blockchain_test.go にテストコードを記述します
  • blockchain.go に実際のブロックチェーンのコードを記述します

実装

1. ブロックチェーンと初期ブロックの生成

ではここから実際に TDD の流れに沿って実装します。まずはブロックチェーンと最初のブロックを生成する動きを実装します。

Red - テストコード

まずはテストの実装です。

最初のブロックに必要な情報は、difficulty、ハッシュ値、現在時刻です。

difficulty はブロックチェーンごとに自由に設定できるようにします。

ハッシュ値は最初のブロックなので 0 に設定します。ただし、byte 型で表現します。

現在時刻は currentTime に一度格納します。time.Now()を got や want の定義時に直接してしまうと、現在時刻のタイミングがずれ、テストに失敗してしまいます。
これを避けるために一度現在時刻を取得してから、got と want に渡すようにしています。

blockchain_test.go
package blockchain

import (
	"reflect"
	"testing"
	"time"
)

func TestCreateBlockchain(t *testing.T) {
	difficulty := 3
	currentTime := time.Now()

	want := Blockchain{
		[]Block{
			Block{
				Hash:      []byte("0"),
				Timestamp: currentTime,
				}
		},
		difficulty,
	}

	got := CreateBlockchain(difficulty, currentTime)

	if !reflect.DeepEqual(want, got) {
		t.Errorf("wanted %v got %v", want, got)
	}
}

Green - 関数を実装

ではここから本体を実装していきます。

まずはテストを実行します。VSCODE の機能を使うか、go testを叩くなどしてテストを実行させてください。

テストを実行すると下記のエラーが出るはずです。

undefined: Blockchain
undefined: Block
undefined: CreateBlockchain

エラーを修正するように実装を進めます。

blockchain.go
// Transactionを表す構造体
type Transaction struct {
	Sender    string
	Recipient string
	Amount    int
}

// ブロックを表す構造体
type Block struct {
	Timestamp     time.Time
	Transactions  []Transaction
	PrevBlockHash []byte
	Hash          []byte
	Nonce         int
}

// ブロックチェーンを表す構造体
//  複数のブロックを持つ形で定義
type Blockchain struct {
	Blocks []Block
	difficulty int
}

// ブロックチェーンを初期化する関数
func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {

}

複数の構造体を定義し、それぞれ必要となる情報を変数として設定しています。

上記の状態でテストを実行すると、以前のエラーが出なくなりますが、今度は missing return というエラーが出ます。

そのエラーの記載の通り、return 部分を CreateBlockchain 関数の中に実装します。Blockchain を作る関数を定義したいため、Blockchain 構造体にデータを持たせて return します。

blockchain.go
 func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
+	return Blockchain{
+		[]Block{Block{
+			Hash:      []byte("0"),
+			Timestamp: currentTime,
+		}},
+		difficulty,
+	}
 }

引数として difficulty と currentTime を取り、Blockchain の構造体を返す関数を実装しました。

この状態でテストを実行すると、エラーが出ることなく成功するようになります。

Refactor

テストコードと本体コードの両方において genesisBlock を外出しできるため、リファクタリングとしてやっておきます。

blockchain_test.go
 func TestCreateBlockchain(t *testing.T) {
 	difficulty := 3
 	currentTime := time.Now()

-	want := Blockchain{
-		[]Block{
-			Block{
-				Hash:      []byte("0"),
-				Timestamp: currentTime,
-				}
-		},
-		difficulty,
-	}
+	genesisBlock := Block{
+		Hash:      []byte("0"),
+		Timestamp: currentTime,
+	}
+
+	want := Blockchain{
+		[]Block{genesisBlock},
+		difficulty,
+	}

 	got := CreateBlockchain(difficulty, currentTime)

 	if !reflect.DeepEqual(want, got) {
 		t.Errorf("wanted %v got %v", want, got)
 	}
 }
blockchain.go
// ブロックチェーンを初期化する関数
 func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
+	genesisBlock := Block{
+		Hash:      []byte("0"),
+		Timestamp: currentTime,
+	}
	return Blockchain{
-		[]Block{Block{
-			Hash:      []byte("0"),
-			Timestamp: currentTime,
-		}},
+		[]Block{genesisBlock},
		difficulty,
	}
 }

これでブロックチェーンを生成する実装が完了しました。

リファクタリング後も問題なく動くことを確認するために、テストを実行しておきましょう。

2. トランザクションプールへのトランザクションの追加

この要領で他の関数も実装していきます。

次は、利用ユーザーが実行したトランザクションをトランザクションプール(未確認のトランザクションが一時的に保管されている場所)に追加する機能の実装です。

Red - テストコード

テスト内容として、

  • ブロックチェーンとトランザクションを生成
  • ブロックチェーンにトランザクションを追加
  • トランザクションプールの中身を assert

という形にすれば、やりたいことを実現できます。

blockchain_test.go
func TestAddTransaction(t *testing.T) {
	// ブロックチェーンとトランザクションを生成
	difficulty := 3
	currentTime := time.Now()
	bc := CreateBlockchain(difficulty, currentTime)

	transaction := Transaction{
		Sender:    "Alice",
		Recipient: "Bob",
		Amount:    10,
	}

	// ブロックチェーンにトランザクションを追加
	bc.AddTransaction(transaction)

	// トランザクションプールの中身を assert
	got := bc.TransactionPool
	want := append([]Transaction{}, transaction)

	if !reflect.DeepEqual(want, got) {
		t.Errorf("wanted %v got %v", want, got)
	}
}

実際のブロックチェーン実装では、トランザクションを追加する前にはさまざまな検証が行われますが、ここでは省略します。

この状態でテストを実行すると、下記のエラーが出ます。

bc.AddTransaction undefined (type Blockchain has no field or method AddTransaction)
bc.TransactionPool undefined (type Blockchain has no field or method TransactionPool)

Green - 関数を実装

ではエラー内容にある通り、AddTransaction と TransactionPool を定義します。

blockchain.go
 (省略)

 type Blockchain struct {
 	Blocks []Block
+	TransactionPool []Transaction
	difficulty int
 }

 // ブロックチェーンを初期化する関数
 func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
	(省略)
 }

+// トランザクションをブロックチェーンのトランザクションプールに追加する関数
+func (bc *Blockchain) AddTransaction(transaction Transaction) {
+
+}

この状態でテストを実行すると、Blockchain 構造体に変数を追加したことで、CreateBlockchain 側でエラーが発生します。

cannot use difficulty (variable of type int) as type []Transaction in struct literal
too few values in struct literal
cannot use difficulty (variable of type int) as type []Transaction in struct literal
too few values in struct literal

そのため、AddTransaction 関数は一旦置いておき、CreateBlockchain 関数を修正します。

本体とテストコードに TransactionPool を追加します。

blockchain.go
 // ブロックチェーンを初期化する関数
 func CreateBlockchain(difficulty int, currentTime time.Time) Blockchain {
 	genesisBlock := Block{
 		Hash:      []byte("0"),
 		Timestamp: currentTime,
 	}
 	return Blockchain{
  		[]Block{genesisBlock},
+ 		[]Transaction{},
  		difficulty,
 	}
 }
blockchain_test.go
 func TestCreateBlockchain(t *testing.T) {
 	difficulty := 3
 	currentTime := time.Now() // gotにもwantにも同じ時刻を適用したいため(time.Nowを直接指定すると、gotとwantでタイミングがずれてテストに失敗してしまう)
 	genesisBlock := Block{
 		Hash:      []byte("0"),
 		Timestamp: currentTime,
 	}
 	want := Blockchain{
  		[]Block{genesisBlock},
+ 		[]Transaction{},
  		difficulty,
 	}

 	got := CreateBlockchain(difficulty, currentTime)

 	if !reflect.DeepEqual(want, got) {
 		t.Errorf("wanted %v got %v", want, got)
 	}
 }

念の為 TestCreateBlockchain のテストを実行し、エラーが出ないことを確認しておきましょう。

では、AddTransaction 関数の実装に戻ります。改めて、TestAddTransaction のテストを実行します。

エラーは相変わらず出ますが、下記のようにようやく assert 部分まで実行されたことがわかります。

wanted [{Alice Bob 10}] got []

では AddTransaction 関数の中身を実装します。

blockchain.go
 // トランザクションをブロックチェーンのトランザクションプールに追加する関数
 func (bc *Blockchain) AddTransaction(transaction Transaction) {
+	bc.TransactionPool = []Transaction{transaction}
 }

単純に渡されたトランザクションをスライスとしてトランザクションプールにしているだけです。

これでテストを実行するとエラーなく成功します。

Red - テストコード

晴れてトランザクションを追加できるようになりましたが、上記のテストでは、空のトランザクションプールに1つ目のトランザクションを追加するという確認しかできていません。既にトランザクションが存在するトランザクションプールに別のトランザクションを追加するテストはできていません。

そのテストを実装します。

blockchain_test.go
 func TestAddTransaction(t *testing.T) {
+ 	t.Run("空のトランザクションプールにトランザクションを追加する", func(t *testing.T) {
 		// ブロックチェーンとトランザクションを生成
 		difficulty := 3
 		currentTime := time.Now()
 		bc := CreateBlockchain(difficulty, currentTime)

 		transaction := Transaction{
 			Sender:    "Alice",
 			Recipient: "Bob",
 			Amount:    10,
 		}

 		// ブロックチェーンにトランザクションを追加
 		bc.AddTransaction(transaction)

 		// トランザクションプールの中身を assert
 		got := bc.TransactionPool
 		want := append([]Transaction{}, transaction)

 		if !reflect.DeepEqual(want, got) {
 			t.Errorf("wanted %v got %v", want, got)
 		}
+ 	})
+
+ 	t.Run("既に存在するトランザクションプールにトランザクションを追加する", func(t *testing.T) {
+ 		// ブロックチェーンとトランザクションを生成
+ 		difficulty := 3
+ 		currentTime := time.Now()
+ 		bc := CreateBlockchain(difficulty, currentTime)
+
+ 		transaction1 := Transaction{
+ 			Sender:    "Alice",
+ 			Recipient: "Bob",
+ 			Amount:    10,
+ 		}
+
+ 		// ブロックチェーンにトランザクションを追加
+ 		bc.AddTransaction(transaction1)
+
+ 		transaction2 := Transaction{
+ 			Sender:    "Bob",
+ 			Recipient: "Alice",
+ 			Amount:    20,
+ 		}
+
+ 		// もう1つトランザクションを追加
+ 		bc.AddTransaction(transaction2)
+
+ 		// トランザクションプールの中身を assert
+ 		got := bc.TransactionPool
+ 		want := append([]Transaction{}, transaction1, transaction2)
+
+ 		if !reflect.DeepEqual(want, got) {
+ 			t.Errorf("wanted %v got %v", want, got)
+ 		}
+
+ 	})
 }

さて、このテストを実行すると、想定通り(?)下記のエラーが出ます。

wanted [{Alice Bob 10} {Bob Alice 20}] got [{Bob Alice 20}]

二つのトランザクションがスライスに格納されていてほしいですが、実際は二つ目のトランザクションのみが格納されています。

関数の実装を修正しましょう。

Green - 関数を実装

blockchain.go
 // トランザクションをブロックチェーンのトランザクションプールに追加する関数
 func (bc *Blockchain) AddTransaction(transaction Transaction) {
-	bc.TransactionPool = []Transaction{transaction}
+	bc.TransactionPool = append(bc.TransactionPool, transaction)
 }

スライスに要素を append する実装に変更しました。

少し回りくどかったですが、これでテストは問題なく動くようになります。

Refactor

ここではテスト関数が冗長なので、共通部分を抽出します。

blockchain_test.go
 func TestAddTransaction(t *testing.T) {
+	transaction := Transaction{
+		Sender:    "Alice",
+		Recipient: "Bob",
+		Amount:    10,
+	}
 	t.Run("空のトランザクションプールにトランザクションを追加する", func(t *testing.T) {
- 		// ブロックチェーンとトランザクションを生成
- 		difficulty := 3
- 		currentTime := time.Now()
- 		bc := CreateBlockchain(difficulty, currentTime)
-
- 		transaction := Transaction{
- 			Sender:    "Alice",
- 			Recipient: "Bob",
- 			Amount:    10,
- 		}
-
- 		// ブロックチェーンにトランザクションを追加
- 		bc.AddTransaction(transaction)
+		bc := initTransactionPool(transaction)

 		// トランザクションプールの中身を assert
 		got := bc.TransactionPool
 		want := append([]Transaction{}, transaction)

 		if !reflect.DeepEqual(want, got) {
 			t.Errorf("wanted %v got %v", want, got)
 		}
 	})

 	t.Run("既に存在するトランザクションプールにトランザクションを追加する", func(t *testing.T) {
- 		// ブロックチェーンとトランザクションを生成
- 		difficulty := 3
- 		currentTime := time.Now()
- 		bc := CreateBlockchain(difficulty, currentTime)
-
- 		transaction1 := Transaction{
- 			Sender:    "Alice",
- 			Recipient: "Bob",
- 			Amount:    10,
- 		}
-
- 		// ブロックチェーンにトランザクションを追加
- 		bc.AddTransaction(transaction1)
+		bc := initTransactionPool(transaction)

 		transaction2 := Transaction{
 			Sender:    "Bob",
 			Recipient: "Alice",
 			Amount:    20,
 		}

 		// もう1つトランザクションを追加
 		bc.AddTransaction(transaction2)

 		// トランザクションプールの中身を assert
 		got := bc.TransactionPool
- 		want := append([]Transaction{}, transaction1, transaction2)
+ 		want := append([]Transaction{}, transaction, transaction2)
 		if !reflect.DeepEqual(want, got) {
 			t.Errorf("wanted %v got %v", want, got)
 		}

 	})
 }

+ func initTransactionPool(transaction Transaction) Blockchain {
+	// ブロックチェーンとトランザクションを生成
+	difficulty := 3
+	currentTime := time.Now()
+	bc := CreateBlockchain(difficulty, currentTime)
+
+	// ブロックチェーンにトランザクションを追加
+	bc.AddTransaction(transaction)
+
+	return bc
+}

3. ブロックへのトランザクションの追加

続いて、トランザクションプールのトランザクションをブロックに追加する関数を実装します。通常、この作業はマイナーが実行します。

マイナーは、新しいコイン(コインベーストランザクション)とトランザクション手数料を報酬として受け取ります。ブロックに含まれるトランザクション数が多いほど、トランザクション手数料の報酬も増えます。したがって、マイナーは通常、手数料が高いトランザクションを優先的に選択し、ブロックサイズ制限に達するまでトランザクションを追加します。

つまり、関数の実装としては、マイナーが好きなトランザクションを選んでブロックに追加できるようにする必要があります。

Red - テストコード

テスト内容として、

  • ブロックチェーンとトランザクション2つを生成
  • ブロックにトランザクション 1 つを追加
  • トランザクションプールとブロックのトランザクション数や中身を assert

という形にすることで、要件を満たすことを確認できます。

blockchain_test.go
func TestAddBlock(t *testing.T) {
	// ブロックチェーンとトランザクション2つを生成
	transaction := Transaction{
		Sender:    "Alice",
		Recipient: "Bob",
		Amount:    10,
	}
	bc := initTransactionPool(transaction)

	transaction2 := Transaction{
		Sender:    "Bob",
		Recipient: "Alice",
		Amount:    20,
	}

	bc.AddTransaction(transaction2)

	// ブロックにトランザクション 1 つを追加
	txToAdd := bc.TransactionPool[0]
	bc.TransactionPool = bc.TransactionPool[1:]
	bc.AddTransactionToBlock(txToAdd)

	// トランザクションプールとブロックのトランザクション数や中身を assert
	latestBlock := bc.Blocks[len(bc.Blocks)-1]

	// assert1: ブロック内のトランザクション数
	if len(latestBlock.Transactions) != 1 {
		t.Errorf("Transaction in Block count must be 1, got %d", len(latestBlock.Transactions))
	}

	// assert2: プール内のトランザクション数
	if len(bc.TransactionPool) != 1 {
		t.Errorf("TransactionPool count must be 1, got %d", len(bc.TransactionPool))
	}

	// assert3: ブロック内のトランザクション自体
	if latestBlock.Transactions[0] != transaction {
		t.Errorf("Transaction in block does not match the expected transaction")
	}
}

テスト関数を作成したら、テストを実行します。

現段階では AddTransactionToBlock関数は何も実装していないため以下のエラーが出ます。

bc.AddTransactionToBlock undefined (type Blockchain has no field or method AddTransactionToBlock)

Green - 関数を実装

では blockchain.goAddTransactionToBlock関数を追加します。

blockchain.go
func (bc *Blockchain) AddTransactionToBlock(transaction Transaction) {

}

この状態でテストをすると、エラー内容が変わり、AddTransactionToBlockが認識されたことを確認できます。

Transaction in Block count must be 1, got 0

中身の実装に進みます。

blockchain.go
func (bc *Blockchain) AddTransactionToBlock(transaction Transaction) {
	latestBlock := &Block{}

	// ブロックチェーンから最新のブロックを取得
	if len(bc.Blocks) > 1 {
		latestBlock = &bc.Blocks[len(bc.Blocks)-1]
	} else if len(bc.Blocks) == 1 {
		latestBlock = &bc.Blocks[0]
	} else {
		panic("Blockchain must contain at least one Block.")
	}

	// 取得したブロックにトランザクションを追加
	latestBlock.Transactions = append(latestBlock.Transactions, transaction)
}

最新ブロックを取得し、そこにトランザクションを格納しています。

latestBlockはポインタとしてブロックの参照を取得しないと、ブロックの中にトランザクションが追加されないため注意が必要です。

ポインタにせず実装すると、ブロック情報がlatestBlockコピーされ、そこにトランザクションを格納するだけになってしまいます。
その場合、インスタンス化しているブロックチェーンの中は何も変わりません。

ではテストを実行してください。問題なく完了するはずです。

4. PoW によるハッシュ計算

ブロックにトランザクションを加えたら、いよいよ PoW を実行する時です。
マイナーはブロックサイズギリギリまでトランザクションを追加したら、いわゆるマイニングを実行します。これは PoW(Proof of Work)を行い、適切な Nonce 値 を見つけ、ブロックのハッシュ値を計算するという内容です。

Red - テストコード

テスト内容として、

  • ブロックチェーンとトランザクションを生成
  • ブロックにトランザクションを追加
  • ブロックに対してマイニング(ハッシュ値計算)を実施
  • ハッシュ値が格納されていることを確認
  • difficulty で設定された数だけ 0 が、ハッシュ値の最初に並んでいることを確認

という形にすることで、要件を満たすことを確認できます。

blockchain_test.go
func TestMineBlock(t *testing.T) {
	// ブロックチェーンとトランザクションを生成
	transaction := Transaction{
		Sender:    "Alice",
		Recipient: "Bob",
		Amount:    10,
	}
	bc := initTransactionPool(transaction)

	// ブロックにトランザクションを追加
	txToAdd := bc.TransactionPool[0]
	bc.AddTransactionToBlock(txToAdd)

	// ブロックに対してマイニング(ハッシュ値計算)を実施
	block := bc.Blocks[0]
	block.MineBlock(bc.difficulty)

	// ハッシュ値が格納されていることを確認
	if block.Hash == nil {
		t.Errorf("No hash stored in block.")
	}
	hashStr := string(block.Hash)

	// difficulty で設定された数だけ 0 が、ハッシュ値の最初に並んでいることを確認
	if hashStr[:bc.difficulty] != strings.Repeat("0", bc.difficulty) {
		t.Errorf("Expected block hash to start with %s, but got %s", strings.Repeat("0", bc.difficulty), hashStr)
	}
}

テスト関数を作成したら、テストを実行します。

現段階では MineBlock関数は何も実装していないため以下のエラーが出ます。

block.MineBlock undefined (type Block has no field or method MineBlock)

Green - 関数を実装

指定された difficulty に基づいたブロックのハッシュ値を探し出すために、Nonce(任意の数値)を1つずつ変更し計算を繰り返します。

以下が、MineBlock 関数の実装です。

blockchain.go
func (block *Block) MineBlock(difficulty int) {
	target := strings.Repeat("0", difficulty)

	for {
		hashStr := block.calculateHash()
		if hashStr[:difficulty] == target {
			block.Hash = []byte(hashStr)
			break
		}
		block.Nonce++
	}
}

ハッシュ値を計算し、difficulty 条件を満たすハッシュ値であった場合は break でループを終わらせる構造になっています。
ただし、ハッシュ値の計算自体は calculateHash という関数にしており、この後実装します。

この状態でテストを実行すると、当然 calculateHash がないというエラーになります。

block.calculateHash undefined (type *Block has no field or method calculateHash)

では calculateHash 関数を実装します。

ブロックのハッシュ値は、ブロック全体を表すデータを組み合わせて計算します。ここではブロックの構造体を構成しているタイムスタンプ、トランザクション、前のブロックのハッシュ、および Nonce に基づいて計算します。トランザクションは送信者、受信者、トークンの送信量を組み合わせます。

blockchain.go
func (block *Block) calculateHash() string {
	var layout = "2006-01-02 15:04:05"
	var transactionStrList []string

	for _, transaction := range block.Transactions {
		transactionStrList = append(transactionStrList, transaction.Sender+transaction.Recipient+strconv.Itoa(transaction.Amount))
	}
	transactionsString := strings.Join(transactionStrList, "")
	record := block.Timestamp.Format(layout) + transactionsString + string(block.PrevBlockHash) + strconv.Itoa(block.Nonce)
	hash := sha256.New()
	hash.Write([]byte(record))
	return hex.EncodeToString(hash.Sum(nil))
}

これでテストが動くようになりました。

MineBlock 関数に fmt.Println を仕込み、hashStr を表示させると何度も計算を繰り返すことがわかるので、やってみてください。

blockchain.go
 func (block *Block) MineBlock(difficulty int) {
 	target := strings.Repeat("0", difficulty)

 	for {
 		hashStr := block.calculateHash()
+ 		fmt.Println(hashStr)
 		if hashStr[:difficulty] == target {
 			block.Hash = []byte(hashStr)
 			break
 		}
 		block.Nonce++
 	}
 }

Refactor

リファクタリング要素としては、calculateHash 関数が少しごちゃついているので、トランザクションを1つの String にする部分を外出しします。

blockchain.go
 func (block *Block) calculateHash() string {
 	var layout = "2006-01-02 15:04:05"
- 	var transactionStrList []string
-
- 	for _, transaction := range block.Transactions {
- 		transactionStrList = append(transactionStrList, transaction.Sender+transaction.Recipient+strconv.Itoa(transaction.Amount))
- 	}
- 	transactionsString := strings.Join(transactionStrList, "")
- 	record := block.Timestamp.Format(layout) + transactionsString + string(block.PrevBlockHash) + strconv.Itoa(block.Nonce)
+  	record := block.Timestamp.Format(layout) + block.transactionsToString() + string(block.PrevBlockHash) + strconv.Itoa(block.Nonce)
 	hash := sha256.New()
 	hash.Write([]byte(record))
 	return hex.EncodeToString(hash.Sum(nil))
 }

+ func (block *Block) transactionsToString() string {
+ 	var transactionStrList []string
+
+ 	for _, transaction := range block.Transactions {
+ 		transactionStrList = append(transactionStrList, transaction.Sender+transaction.Recipient+strconv.Itoa(transaction.Amount))
+ 	}
+
+ 	return strings.Join(transactionStrList, "")
+ }

5. ブロックチェーンへの新たなブロックの追加

作成したブロックチェーンに新たなブロックを追加する機能を開発します。

Red - テストコード

AddBlock 関数は、ブロックチェーンに新しいブロックを追加するために使用されます。トランザクションのリストを引数として受け取り、ブロックチェーンの最後のブロック(前のブロック)を取得します。その後、CreateBlock 関数を呼び出して新しいブロックを生成し、前のブロックのハッシュを指定します。最後に、新しいブロックをブロックチェーンに追加します。

テスト内容として、

  • ブロックチェーンとトランザクションを生成
  • ブロックに対してマイニング(ハッシュ値計算)を実施
  • 新たなブロックを追加
  • 最新のブロックに前のブロックのハッシュ値が入っていることを確認
  • 最新のブロックにトランザクションが含まれていないことを確認
  • 最新のブロックの Nonce が 0 であることを確認
  • 最新のブロックの Hash が空であることを確認

という形にすることで、要件を満たすことを確認できます。

blockchain_test.go
func TestAddBlock(t *testing.T) {
	// ブロックチェーンとトランザクションを生成
	transaction := Transaction{
		Sender:    "Alice",
		Recipient: "Bob",
		Amount:    10,
	}
	bc := initTransactionPool(transaction)

	block := &bc.Blocks[0] // ポインタを取得しないとbc内のBlockが更新されない

	// ブロックに対してマイニング(ハッシュ値計算)を実施
	block.MineBlock(bc.difficulty)

	prevBlockCount := len(bc.Blocks)

	// 新たなブロックを追加
	bc.AddBlock()

	// Assert
	latestBlock := bc.Blocks[len(bc.Blocks)-1]

	// ブロックの数が増えていることを確認
	if len(bc.Blocks) != prevBlockCount+1 {
		t.Errorf("Expected block count is %d, but got %d", prevBlockCount+1, len(bc.Blocks))
	}

	// 最新のブロックに前のブロックのハッシュ値が入っていることを確認
	if !reflect.DeepEqual(latestBlock.PrevBlockHash, block.Hash) {
		t.Errorf("Expected prev block hash is %s, but got %s", latestBlock.PrevBlockHash, block.Hash)
	}

	// 最新のブロックにトランザクションが含まれていないことを確認
	if latestBlock.Transactions != nil {
		t.Errorf("Expected no transaction is in block, but got %v", latestBlock.Transactions)
	}

	// 最新のブロックの Nonce が 0 であることを確認
	if latestBlock.Nonce != 0 {
		t.Errorf("Nonce must be 0, but got %d", latestBlock.Nonce)
	}

	// 最新のブロックの Hash が空であることを確認
	if string(latestBlock.Hash) != "" {
		t.Errorf("Expected hash is vacant, but got %d", latestBlock.Hash)
	}
}

テスト関数を作成したら、テストを実行します。現段階では何も実装していないため以下のエラーが出ます。

bc.AddBlock undefined (type Blockchain has no field or method AddBlock)

Green - 関数を実装

まずは blockchain.goAddBlock関数を空で追加します。

blockchain.go
// 新しいブロックを作成する関数
func (bc *Blockchain) AddBlock() {
}

そうすると以下のエラーが出ます。

Expected block count is 2, but got 1
Expected prev block hash is , but got 000e03b4990ae508f4d1eda12f6f49a72f67c84ff556d0ed0bf691649a8790f3
Nonce must be 0, but got 6829
Expected hash is vacant, but got [48 48 48 (省略)]

関数の中身を実装します。

blockchain.go
// 新しいブロックを作成する関数
func (bc *Blockchain) AddBlock() {
	latestBlock := bc.Blocks[len(bc.Blocks)-1]
	fmt.Println(latestBlock.Hash)

	newBlock := Block{
		Timestamp:     time.Now(),
		PrevBlockHash: latestBlock.Hash,
		Hash:          []byte{},
		Nonce:         0,
	}

	bc.Blocks = append(bc.Blocks, newBlock)
}

追加前時点で最新のブロックを取得し、そのハッシュ値を加える形で新たなブロックを作り出しています。

これでテストが正常に動くようになるはずです。全てのアサーションが正常に完了することを確認してください。

6. ブロックの検証

最後に、追加されたブロックの正当性を検証し、トランザクションのステータスを変更する機能を実装します。この処理は、一般的にマイナーのノードではなく別のノードで実行されます。ブロックの正当性が検証された後は、そのブロックに含まれるトランザクションは「確認済み」となり、マイナーに報酬が支払われることになります(報酬の支払いに関する実装はここでは扱いません)。

Red - テストコード

正当性の検証の方法として、あるブロックの中に1つ前のブロックのハッシュが含まれていることを確認します。
そのため、テストするためには、事前に最低二つのブロックを用意し、ハッシュ値を計算しておく必要があります。

テスト内容としては、

  • ブロックチェーンを生成
  • 最初のブロックに対してマイニング(ハッシュ値計算)を実施
  • ブロックを追加
  • トランザクションを生成し、トランザクションプールに追加
  • 追加したブロックにトランザクションを追加
  • 追加したブロックに対してマイニング(ハッシュ値計算)を実施
  • 検証実行
  • 検証が成功していることを確認
  • トランザクションのステータスが変わっていることを確認

という形にすることで、要件を満たすことを確認できます。

blockchain_test.go
func TestValidation(t *testing.T) {
	// ブロックチェーンを生成
	difficulty := 3
	currentTime := time.Now()
	bc := CreateBlockchain(difficulty, currentTime)

	// 最初のブロックに対してマイニング(ハッシュ値計算)を実施
	block := &bc.Blocks[0]
	block.MineBlock(bc.difficulty)

	// ブロックを追加
	bc.AddBlock()

	// トランザクションを生成し、トランザクションプールに追加
	transaction1 := Transaction{
		Sender:    "Alice",
		Recipient: "Bob",
		Amount:    10,
		Status:    "Pending",
	}
	transaction2 := Transaction{
		Sender:    "Bob",
		Recipient: "Alice",
		Amount:    20,
		Status:    "Pending",
	}

	bc.AddTransaction(transaction1)
	bc.AddTransaction(transaction2)

	// 追加したブロックにトランザクションを追加
	for _, transaction := range bc.TransactionPool {
		bc.AddTransactionToBlock(transaction)
	}

	// 追加したブロックに対してマイニング(ハッシュ値計算)を実施
	block2 := &bc.Blocks[1]
	block2.MineBlock(bc.difficulty)

	// 検証実行
	err := bc.Validation()

	// 検証が成功していることを確認
	if err != nil {
		t.Errorf("Validation Error, %s", err)
	}

	// トランザクションステータスが変わっていることを確認
	for _, transaction := range bc.Blocks[1].Transactions {
		if transaction.Status != "Success" {
			t.Errorf("Expected transaction status is Success, but got %s", transaction.Status)
		}
	}
}

テスト関数を作成したら、テストを実行します。

現段階では何も実装していないため以下のエラーが出ます。

unknown field 'Status' in struct literal of type Transaction
bc.Validation undefined (type Blockchain has no field or method Validation)
transaction.Status undefined (type Transaction has no field or method Status)

Green - 関数を実装

まずは Validation 関数を用意し、Transaction 構造体へステータス要素を追加します。

blockchain.go
func (bc *Blockchain) Validation() error {
	return nil
}
blockchain.go
 type Transaction struct {
 	 Sender    string
 	 Recipient string
 	 Amount    int
+ 	 Status		string
 }

この状態でテストを実行すると、トランザクションステータスが変わっていないというエラーが出ます。

Expected transaction status is Success, but got Pending

では Validation 関数を実装します。

最初のブロック以外に対して、検証を実行します。最初のブロックは1つ前のブロックが存在しないためです。

検証としては、

  • ブロックに格納されているハッシュ値と、calculateHash 関数を使ったハッシュ値が一致すること
  • ブロックに格納されている1つ前のブロックのハッシュ値と、1つ前のブロックに格納されているハッシュ値が一致すること

を確認します。どちらかの条件が false になった場合は、正当性を確認できなかったということでエラーを返します。

検証が問題なく完了した場合は、そのブロック内に存在するすべてのトランザクションのステータスを Success に変え、nil を返します。

blockchain.go
 func (bc *Blockchain) Validation() error {
+ 	for i := range bc.Blocks[1:] {
+ 		previousBlock := bc.Blocks[i]
+ 		currentBlock := &bc.Blocks[i+1]
+ 		if string(currentBlock.Hash) != currentBlock.calculateHash() || !reflect.DeepEqual(currentBlock.PrevBlockHash,  previousBlock.Hash) {
+ 			return errors.New("Hash is not valid")
+ 		}

+ 		for i := 0; i < len(currentBlock.Transactions); i++ {
+ 			transaction := &currentBlock.Transactions[i]
+ 			transaction.Status = "Success"
+ 		}
+ 	}
 	return nil
 }

これでテストが動くようになります。

以上で、ブロックチェーンを生成するところから、ブロックの正当性を検証する部分までを実装できました。

まとめ

本記事では、Go 言語を使って基本的なブロックチェーンを実装する方法を、テスト駆動開発(TDD)の流れに沿って紹介しました。テストし足りていない部分も多々ありますが、話が長くなってしまうので割愛しています。

今後は、以下のようなトピックを学ぶことで、ブロックチェーン技術の理解をさらに深めることができると思います。

  • コンセンサスアルゴリズム(Proof of Work、Proof of Stake など)
  • 分散型ネットワークの構築と運用
  • 暗号技術(ハッシュ関数、公開鍵暗号、デジタル署名など)
  • スマートコントラクトと分散型アプリケーション(dApps)の開発

本記事が、読者の方々にとって、ブロックチェーン技術に興味を持ち学びを深めるきっかけになれば幸いです。

Discussion