🌊

GolangでブロックチェーンのPoWの簡易的な実装をしてみた

2022/04/03に公開

概要

Golangとブロックチェーンを最近勉強し始めて、練習と興味がてら、PoWの簡易的な実装を行いました。

複数ノードによるコンセンサスまで考慮すると大変そうだったので単一のノードでのことだけ考えて実装してみました。また今回エラー処理やテストは書いていません。

※ Golangもブロックチェーンも初心者レベルなので、間違いを含んでいる可能性がそれなりあります

structの解説

だいたい画像のようなものを実装しました。

Block

  • Blockの中に取引の記録Transactionsが保存されている
  • Blockは自身のハッシュ値を持つ
  • Blockは前のブロックのハッシュ値を持ちこれがチェーンとなっている

BlockChain

  • 内部にBlockの配列を持つ
  • 未処理の取引記録pendingTransactionsを持つ
  • マイニングを実行するとpendingTransactionsがブロックに記録され、さらに条件を満たすハッシュ値(先頭が00)になるnonceを見つける

Block

内部に取引記録を保存することができます。
またmining()メソッドを実行することで、適切なハッシュ値を計算します。

block_chain/bchain/block.go
package bchain

import (
	"crypto/sha256"
	"fmt"
	"time"
)

type (
	block struct {
		timestamp    time.Time
		transactions []*transaction
		previousHash [32]byte
		hash         [32]byte
		nonce        int
	}
)

func NewBlock(timestamp time.Time, transactions []*transaction, previousHash [32]byte) *block {
	b := block{
		timestamp:    timestamp,
		transactions: transactions,
		previousHash: previousHash,
		nonce:        0,
	}
	b.hash = b.calcHash()

	return &b
}

func (b block) String() string {
	return fmt.Sprintf("%s", b.transactions)
}

// private

// nonceの値をインクリメントしていき、条件に合致するハッシュ値が見つかるまで計算を続ける
func (b *block) mining() {
	isContinue := true
	for i := 0; isContinue; i++ {
		b.nonce++
		b.hash = b.calcHash()

		strSha256 := fmt.Sprintf("%x", b.hash)
		isContinue = string([]rune(strSha256)[:2]) != "00"
	}
	fmt.Println("Succeeded to mining")
}

func (b block) calcHash() [32]byte {
	s := fmt.Sprintf(
		"%s:%s:%s:%s",
		b.previousHash,
		b.timestamp,
		b.transactions,
		string(rune(b.nonce)),
	)
	byteSha256 := sha256.Sum256([]byte(s))

	return byteSha256
}

Transaction

取引記録用のstructなので特別なことはしていません。

block_chain/bchain/transaction.go
package bchain

import "fmt"

type (
	address string
	money   float64

	transaction struct {
		sender    address
		recipient address
		amount    money
	}
)

func NewTransaction(sender string, recipient string, amount float64) *transaction {
	t := transaction{
		sender:    address(sender),
		recipient: address(recipient),
		amount:    money(amount),
	}

	return &t
}

func (tx transaction) String() string {
	return fmt.Sprintf(
		"{ %s sent %v => %s }",
		tx.sender,
		tx.amount,
		tx.recipient,
	)
}

BlockChain

block_chain/bchain/block_chain.go
package bchain

import (
	"crypto/sha256"
	"fmt"
	"time"
)

type BlockChain struct {
	blocks              []*block
	pendingTransactions []*transaction
	miningReward        money
}

// チェーンの中の最初のブロック(genesis block)を精製して、structを返す
func NewBlockChain() *BlockChain {
	genesisBlock := NewBlock(
		time.Now(),
		make([]*transaction, 0),
		sha256.Sum256([]byte(fmt.Sprintf("0"))),
	)

	return &BlockChain{
		miningReward: money(12.5),
		blocks:       []*block{genesisBlock},
	}
}

func (bc BlockChain) String() string {
	return fmt.Sprintf("blocks: %s", bc.blocks)
}

func (bc *BlockChain) AddTransaction(tx *transaction) {
	bc.pendingTransactions = append(bc.pendingTransactions, tx)
}

// チェーンに記録されている未処理のトランザクションに対してマイニングを行い確定する
func (bc *BlockChain) Mine(rewardAddress address) {
	// ブロックの生成とマイニングを行いチェーンに追加する
	block := NewBlock(
		time.Now(),
		bc.pendingTransactions,
		bc.lastBlock().hash,
	)
	block.mining()
	bc.blocks = append(bc.blocks, block)

	// pendingTransactionの初期化
	bc.pendingTransactions = make([]*transaction, 0)

	// マイニングの報酬を記録。次のマイニングで確定される
	bc.pendingTransactions = append(
		bc.pendingTransactions,
		NewTransaction(
			"",
			string(rewardAddress),
			float64(bc.miningReward),
		),
	)
}

// ブロックチェーンが全体で整合性を持っているか計算
func (bc BlockChain) IsValid() bool {
	for i := len(bc.blocks) - 1; i >= 0; i-- {
		b := bc.blocks[i]

		// ハッシュ値が書き換えられていないか
		if b.hash != b.calcHash() {
			return false
		}

		// 前のハッシュ値が一致するか
		if i != 0 && b.previousHash != bc.blocks[i-1].hash {
			return false
		}
	}

	return true
}

// 取引記録を全て参照し、保持金額を計算する
func (bc BlockChain) GetBalanceOfAddress(a string) money {
	var balance money
	for _, block := range bc.blocks {
		for _, transaction := range block.transactions {
			if transaction.sender == address(a) {
				balance -= transaction.amount
			}
			if transaction.recipient == address(a) {
				balance += transaction.amount
			}
		}
	}

	return balance
}

// private

func (bc BlockChain) lastBlock() block {
	return *bc.blocks[len(bc.blocks)-1]
}

mainでの挙動

block_chain/main.go

package main

import (
	"fmt"
	"goTestProject/block_chain/bchain"
)

func main() {
	// ブロックチェーンの生成
	bc := bchain.NewBlockChain()
	// 取引の記録
	bc.AddTransaction(
		bchain.NewTransaction(
			"Ken",
			"Taro",
			2,
		),
	)
	bc.AddTransaction(
		bchain.NewTransaction(
			"Taro",
			"Ken",
			10,
		),
	)

	// 上で作成した記録に対してマイニングを行い確定する
	bc.Mine("Ken")
	// マイニングの報酬が支払われているので、もう一度マイニングを行い確定する
	bc.Mine("Ken")

	fmt.Printf("%+v\n", bc)
	fmt.Printf("block Chain valid: %v\n", bc.IsValid())

	fmt.Printf("Ken balance: %f\n", bc.GetBalanceOfAddress("Ken"))
	fmt.Printf("Taro balance: %f\n", bc.GetBalanceOfAddress("Taro"))
}

実行結果

残高がマイナスになることを考慮していないので、残高がマイナスになっていますが、マイニングとマイニングの報酬が支払われていることを確認できました。

Succeeded to mining
Succeeded to mining
blocks: [[] [{ Ken sent 2 => Taro } { Taro sent 10 => Ken }] [{  sent 12.5 => Ken }]]
block Chain valid: true
Ken balance: 20.500000
Taro balance: -8.000000

Discussion