goで実装しながらブロックチェーンのPoWにおけるハッシュ値計算を理解する

2023/03/27に公開

本記事では、ブロックチェーンのプルーフ・オブ・ワーク(Proof of Work, PoW)アルゴリズムの計算方法を具体例とともに説明します。また、Go 言語による実装例も示します。

Nonce とは何か?Difficulty とは何か?どうやってハッシュ値を計算しているのか?が分かるようになっています。

ブロックチェーンの構成と PoW の計算内容

ブロックチェーンは、複数のブロックが連結されたリスト構造で構成されています。

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

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

このハッシュ値と Nonce 値は PoW アルゴリズムにおいて密接に関連しています。

  • ハッシュ値
    ブロックのデータ(前のブロックのハッシュ、Nonce 値、タイムスタンプ、トランザクション)に基づいて計算される固有の値です。通常、SHA-256 などの暗号学的ハッシュ関数を使用して計算されます。ハッシュ値は、計算する過程で情報が落とされるため、元のデータに復元することができません。また、元のデータが少しでも変わるとハッシュ値は全く別の値になります。これらの特徴から、ハッシュ値はデータの改ざん検知に使われます。

  • Nonce 値
    Nonce とは number used once の略称で直訳すると「一度だけ使われる数」という意味になります。ブロックのデータに追加される一意の数値で、ハッシュ値が Difficulty の要件を満たすように調整するために使用されます。Nonce は通常 0 から始まり、インクリメントされていきます。Difficulty の要件とは、ハッシュ値が特定の目標値以下であることを指します。Difficulty は、ブロックの生成速度を一定に保つために導入された閾値です。

PoW の計算は、

  • Nonce を 0 からインクリメントしながらブロックのハッシュを計算
  • そのハッシュ値が Difficulty の条件を満たすかどうかを確認
  • 条件を満たすハッシュが見つかるまで、Nonce の値を変更し続ける
  • 条件を満たすハッシュが見つかったら、そのブロックをブロックチェーンに追加

という流れで実行されます。

シンプルな具体例

では具体例を Go 言語の実装とともに示します。

計算の条件としては

  • ブロックのデータは「Hello, world!」という文字列とする
  • Difficulty は 4 とする(つまりハッシュ値の計算結果が「0000」で始まる値になることが条件となります)

となります。

計算の手順は以下のとおりです。

  1. Nonce を 0 から始める。
  2. ブロックデータと Nonce を組み合わせる。つまり「Hello, world!0」という文字列が計算対象となる。
  3. 「Hello, world!0」に対して SHA-256 ハッシュ関数を適用し、ハッシュ値を計算する。
  4. 計算されたハッシュ値が Difficulty の条件(「0000」で始まる)を満たしているかを確認する。
  5. 満たしていない場合は、Nonce を 1 増やして 2〜4 の手順を繰り返す。
  6. 満たした場合は、計算完了。

この計算を Go 言語で実装すると以下のようになります。

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strings"
)

func main() {
	data := "Hello, world!"
	difficulty := 4
	target := strings.Repeat("0", difficulty) // "0000"

        // 1. Nonce を 0 から始める。
	nonce := 0

	for {
		// 2. ブロックデータと Nonce を組み合わせる。つまり「Hello, world!0」という文字列が計算対象となる。
		dataToHash := data + fmt.Sprint(nonce)

		// 3. 「Hello, world!0」に対して SHA-256 ハッシュ関数を適用し、ハッシュ値を計算する。
		hasher := sha256.New()
		hasher.Write([]byte(dataToHash))
		hash := hex.EncodeToString(hasher.Sum(nil))

                // 4. 計算されたハッシュ値が Difficulty の条件(「0000」で始まる)を満たしているかを確認する。
		if strings.HasPrefix(hash, target) {
                        // 6. 満たした場合は、計算完了。
			fmt.Printf("Found a valid hash: %s (Nonce: %d)\n", hash, nonce)
			break
		} else {
                        // 5. 満たしていない場合は、Nonce を 1 増やして 2〜4 の手順を繰り返す。
			nonce++
		}
	}
}

The Go Playground で実行してみてください。

実行すると、次のような結果が得られます:

Found a valid hash: 0000c3af42fc31103f1fdc0151fa747ff87349a4714df7cc52ea464e12dcd4e9 (Nonce: 4250)

この計算例では、Nonce が 4250 のときに、Difficulty の条件(ハッシュが「0000」で始まる)に適合するハッシュ値が見つかりました。

リアルな具体例

実際のブロックチェーンでは、ハッシュ値と Nonce の計算は、ブロック内のトランザクションデータ、前のブロックのハッシュ値、タイムスタンプなどの情報を含むブロックヘッダに基づいて行われます。

例えば以下のようなブロックのデータがあると仮定します。

  • 前のブロックのハッシュ値:abcd1234
  • タイムスタンプ:1633022800
  • トランザクションデータ:{Sender: "Alice", Recipient: "Bob", Amount: 10}
  • Nonce の初期値:0
  • Difficulty: 3

実装は以下のようになります。

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strings"
)

type Block struct {
	Timestamp     int64
	Transactions  string
	PrevBlockHash string
	Nonce         int
}

func main() {
  block := &Block{
		Timestamp:     1633022800,
		Transactions:  `{Sender: "Alice", Recipient: "Bob", Amount: 10}`,
		PrevBlockHash: "abcd1234",
		Nonce:         0,
	}

  data := strconv.FormatInt(block.Timestamp, 10) + block.Transactions + block.PrevBlockHash + strconv.Itoa(block.Nonce)

	difficulty := 3
	target := strings.Repeat("0", difficulty) // "000"

        // 1. Nonce を 0 から始める。
	nonce := 0

	for {
                // 2. ブロックデータと Nonce を組み合わせる。
		dataToHash := data + fmt.Sprint(nonce)

		// 3. dataに対して SHA-256 ハッシュ関数を適用し、ハッシュ値を計算する。
		hasher := sha256.New()
		hasher.Write([]byte(dataToHash))
		hash := hex.EncodeToString(hasher.Sum(nil))

                // 4. 計算されたハッシュ値が Difficulty の条件を満たしているかを確認する。
		if strings.HasPrefix(hash, target) {
                        // 6. 満たした場合は、計算完了。
			fmt.Printf("Found a valid hash: %s (Nonce: %d)\n", hash, nonce)
			break
		} else {
                        // 5. 満たしていない場合は、Nonce を 1 増やして 2〜4 の手順を繰り返す。
			nonce++
		}
	}
}

The Go Playground で実行してみてください。

実行すると、次のような結果が得られます:

Found a valid hash: 000d162f9ed911a43771dcc0159a92a8372bf4aa9f452f6cbc195e000192c8c4 (Nonce: 8098)

Nonce を増やすことで、異なるハッシュ値を生成し、Difficulty 条件に適合するハッシュ値を見つけることができました。

最後に

この計算は、PoW としてブロックをブロックチェーンに追加する前に行われる重要なプロセスです。

実際の Difficulty はもっと数値が大きく、時間がかかるようになっています。そのためブロックチェーンは改ざんが困難であり、データの不変性が維持される、という仕組みです。

例えば、Difficulty を 4 にして実行すると、以下のような結果が得られます。

Found a valid hash: 0000cd54202992361b10644a6596b12e9d19e89fb8a325062fb0499dbe1a3a23 (Nonce: 13655)

nonce 値が増えており、その分計算に時間がかかったということになります。

Difficulty が増えると、計算にかかる時間が長くなり、より多くの計算リソースが必要になります。これにより、マイナーと呼ばれる世界中の人々は、競争相手に先んじて Difficulty 条件に合致するハッシュ値を見つけるために、大量の計算リソースを投入しています。

なお、Difficulty はブロックチェーンのネットワーク全体で調整され、一定の時間間隔でブロックが生成されるように設定されています(例:ビットコインでは約 10 分ごと)。これにより、新しいブロックが生成されずにトランザクションがいつまで経ってもブロックチェーンに保存されなくなることを防ぐことができます。

Discussion