はじめに
この記事では前回の「Networkの形成、マイニングによるブロック作成(前編)」の続きを記載します。
残りのコード部分の説明とデモの実装を実施してみます。
BlockchainNodeStateクラスでノードの状態を管理しています。
後半のノード間の処理部分
コードの後半の関数について記載していきます。
def forward_tx(state: BlockchainNodeState, tx: dict)
forward_tx関数は以下のようになっていて、受信したトランザクションを全ピアに送ります。
def forward_tx(state: BlockchainNodeState, tx: dict) -> None:
ttl = tx.get("ttl", 0)
if ttl <= 0:
return
next_tx = dict(tx)
next_tx["ttl"] = ttl - 1
for peer in state.peers:
try:
start_ms = now_ms()
post_json(f"{peer}/tx", next_tx)
log(state.name, f"forward_tx -> {peer} in {now_ms() - start_ms:.2f} ms")
except Exception:
continue以下のような流れになります。
1台がTXを受け取る
↓
mempoolに保存
↓
N-1台へ送信
↓
それぞれがまた N-1台へ送信ここで問題になるのが、ノード数が増えると処理が増加してしまうということです。
ttl = tx.get("ttl", 0)
if ttl <= 0:
return
next_tx = dict(tx)
next_tx["ttl"] = ttl - 1の部分ではTTLの処理をしています。
TTLはTime To Liveの略で、そのデータがネットワークで何回転送されるかを決めます。
これにより無限ループを防ぎます。
def forward_block(state: BlockchainNodeState, block: Block)
forward_block関数ではマイニングされたブロックを全ノードに送ります。
マイニングはstart_mining_loop関数(後述)の中で行われます。
PoWに成功したらマイニングが成功したということになります。
この関数も上記のforward_tx関数と同様にノード数が増えると処理完了までの時間が増加するという問題があります。以下のような流れで。
- 送信回数が比例して増える
- 各ノードが同時に送信する
- ネットワーク待ち時間が増える
def make_handler(state: BlockchainNodeState)
make_handler関数はHTTPリクエストを処理する部分でノードがAPIサーバとして使えるようにしています。
def start_mining_loop(state: BlockchainNodeState, max_txs_per_block: int = 10, interval: float = 1.0)
start_mining_loop関数では、別スレッドでマイニングを行います。
メインスレッド → HTTPサーバ
別スレッド → マイニングという風に同時に動きます。
start_mining_loop関数は以下のようになっています。
def start_mining_loop(state: BlockchainNodeState, max_txs_per_block: int = 10, interval: float = 1.0) -> threading.Thread:
def loop() -> None:
state.mining = True
log(state.name, "mining loop started")
while True:
loop_start_ms = now_ms()
batch = state.take_mempool_batch(max_txs_per_block)
if not batch:
time.sleep(interval)
continue
with state.lock:
index = len(state.chain)
prev_hash = state.chain[-1].hash if state.chain else "0" * 64
result = mine_block(index, prev_hash, batch, state.difficulty)
if result is None:
# aborted due to nonce threshold; re-queue txs
with state.lock:
state.mempool = batch + state.mempool
log(state.name, f"mining loop aborted batch in {now_ms() - loop_start_ms:.2f} ms")
continue
added = state.add_block(result)
if added:
log(state.name, f"mined new block height={result.index} hash={result.hash[:8]}...")
forward_block(state, result)
log(state.name, f"mining loop complete in {now_ms() - loop_start_ms:.2f} ms")
with state.lock:
start_ms = state.mempool_first_seen_ms
if start_ms is not None:
log(state.name, f"end-to-end from first tx to block in {now_ms() - start_ms:.2f} ms")
t = threading.Thread(target=loop, daemon=True)
t.start()
return t無限ループの中でまずは、以下で、mempoolからトランザクションを取ります。
batch = state.take_mempool_batch(max_txs_per_block)次に以下でPoWを行います。
result = mine_block(index, prev_hash, batch, state.difficulty)ここでマイニングが成功すると新しいブロックが作成されます。
そしてそれを以下の部分でチェーンへ追加。
added = state.add_block(result)その後、以下で全ノードへ送信されます。
forward_block(state, result)流れのまとめ
さてここまでの実装のながれをまとめると以下のようになります。
例えば、3ノードで起動した場合を考えます。
node01 (miner)
node02
node03この時、
- node02にトランザクションを送信する
- node02 → node01, node03へトランザクションを転送
- マイナーであるnode01がマイニング
- ブロック完成
- 全ノードへ配布
- チェーン更新
といった流れになります。
実際にデモをしてみる
実際にデモをしてみます。
今回のデモはdocker composeによって5台のノードと、そのうち1台のノードをマイナーとしています。
difficultyはデフォルトで4になっていますが、6にすると、トランザクションを送ってからマイニングが完了しすべてのノードがブロックを所有するまでに5965.07 msかかります。約6秒。
これが、difficultyを7にすると、完了までに1562651.72 msとなって、1562秒で20分以上となり爆増します。
実際に行ったデモは以下のような出力になります。
まずクライアントからトランザクションが送信され、node01とnode02が受け取ります。
node01-1 | [06:23:40] node01: received txid=f775dc72-3069-4eab-a7ef-09e9e35d051c from=client
node02-1 | [06:23:40] node02: received txid=f775dc72-3069-4eab-a7ef-09e9e35d051c from=clientそして各ノードがトランザクションを受信します。ここではトランザクションをmempoolに入れます。
node03-1 | [06:23:40] node03: received txid=f775dc72-3069-4eab-a7ef-09e9e35d051c from=client
node04-1 | [06:23:40] node04: received txid=f775dc72-3069-4eab-a7ef-09e9e35d051c from=client
node05-1 | [06:23:40] node05: received txid=f775dc72-3069-4eab-a7ef-09e9e35d051c from=clientノード同士でトランザクションを転送してP2Pで広げていきます。
node03-1 | [06:23:40] node03: forward_tx -> http://node02:9102 in 9.72 ms
node04-1 | [06:23:40] node04: forward_tx -> http://node03:9103 in 9.88 ms
node02-1 | [06:23:40] node02: forward_tx -> http://node01:9101 in 11.20 msその後、マルケルルートを計算します。ここからnode01はマイニングを開始します。
node01-1 | [06:23:40] merkle: computed merkle root for 1 txs in 0.00 ms10000回nanceを試す。
node01-1 | [06:23:40] miner: mine_block pause nonce=10000 in 20.91 ms10000回ごとにいったん止め、PoWが成功するまで繰り返します。
そして、PoWが成功して以下になります。
node01-1 | [06:23:40] miner: mine_block success nonce=4299 in 7.93 msそして、node01がブロックを生成します。
node01-1 | [06:23:40] node01: mined new block height=1 hash=0000a6de...ブロックが全ノードに送信されます。
node02-1 | [06:23:40] node02: forward_block -> http://node01:9101 in 1.56 ms
node03-1 | [06:23:40] node03: forward_block -> http://node02:9102 in 1.21 ms
node04-1 | [06:23:40] node04: forward_block -> http://node03:9103 in 1.31 ms
・・・各ノードがブロック検証します。
node03-1 | [06:23:40] node03: accepted block height=1 hash=0000a6de...
node04-1 | [06:23:40] node04: accepted block height=1 hash=0000a6de...
node05-1 | [06:23:40] node05: accepted block height=1 hash=0000a6de...
・・・各ノード同士でブロックを再転送します。
node03-1 | [06:23:40] node03: forward_block -> http://node04:9104 in 8.79 ms
node02-1 | [06:23:40] node02: forward_block -> http://node03:9103 in 11.74 ms
node05-1 | [06:23:40] node05: forward_block -> http://node04:9104 in 1.99 ms
node04-1 | [06:23:40] node04: forward_block -> http://node05:9105 in 5.65 ms
node01-1 | [06:23:40] node01: forward_block -> http://node02:9102 in 15.86 ms
node05-1 | [06:23:40] node05: forward_block -> http://node01:9101 in 1.84 ms最後に
実装とデモを通してブロックチェーンとはなんたるかとその流れを把握することがざっくりとではありますができました。
