これからはWeb3.0の時代らしいからブロックチェーンを作ろう。Bunで。
ブロックチェーン!Web3.0!!NFT!!!
これからは非中央集権システムの時代ですって!!!!!!
なので自分でもブロックチェーンを作ってみます!!!!!!!!!!!!!!
要約
- ブロックチェーンを作ってみたくなった
- 数値のやりとり(トランザクション)ができるところまで作ったので記事にする
- Bun
何ができた?
例えば手元で次のように3つのプロセスをブロックチェーンノードとして立てる。
version: "3"
services:
node1:
build: .
ports:
- "3000:3000"
volumes:
- ./src:/app
container_name: node1
environment:
- PORT=3000
- SELF_ENDPOINT=node1:3000
- GENESIS_ADDRESS=azarashi
node2:
build: .
ports:
- "3001:3000"
volumes:
- ./src:/app
depends_on:
- node1
container_name: node2
environment:
- PORT=3000
- INITIAL_NODE_ENDPOINT=node1:3000
- SELF_ENDPOINT=node2:3000
node3:
build: .
ports:
- "3002:3000"
volumes:
- ./src:/app
depends_on:
- node1
container_name: node3
environment:
- PORT=3000
- INITIAL_NODE_ENDPOINT=node1:3000
- SELF_ENDPOINT=node3:3000
最初はazarashi
というアカウントに100ポイントが入っている設定にした。ノードが3つとも同じブロックチェーンを共有しているので同じデータが得られる。
$ curl localhost:3000/result -X GET
{"azarashi":100}
$ curl localhost:3001/result -X GET
{"azarashi":100}
$ curl localhost:3002/result -X GET
{"azarashi":100}
次のように、azarashi
からnekochan
に40ポイント移動するトランザクションを発生させる。
$ curl -X POST localhost:3001/transaction -d '{"fromAddress":"azarashi", "toAddress":"nekochan", "amount": "40"}'
ノード間でブロックの作成や共有がされ、ポイントの移動が分散台帳に記録される。
$ curl localhost:3000/result -X GET
{"azarashi":60,"nekochan":40}
以上。すごく素朴だが分散台帳のシステムっぽいものができている。
実装の内容
ソースコードはこのリポジトリで公開している。
なぜBun?
Bunの1.0が出てから特に何も触れていなかったので何かしらを作ってみたかった。
ざっくりした概念の解説
聞きかじりの情報ばかりなので間違っていたら申し訳ないです。
ブロック
複数のトランザクション(取引情報)を含む情報の集合。
新しくブロックを作って認められるためには難しい計算をしなければいけなかったりそうでなかったりする。
ブロックチェーン
ブロックが時系列順に連なったもの。それぞれのブロックが一つ前のブロックのハッシュ値を共有することでつながっている。新しくブロックを追加されることができる。つまりブロックチェーンの各ブロックを見ればこれまでの取引すべての履歴を参照することができる。
ノード
ブロックチェーンのデータを保存するもの。ほかのノードと相互に通信してブロックやトランザクションのデータのやりとりをする。ノードのネットワークによって分散型システムが成り立っている。
実際に作っていこう
ちなみにこの記事での実装は、セキュリティ面や整合性担保の観点を一切無視している。最低限手元で動くものを作った、という記事だ。
Transaction
誰から誰にどれだけ数値が移動するのかの情報を含む。
validかどうかのメソッドがあると、ほかのノードから共有されたTransactionが正しい形式かどうかを確かめられるので便利(ってChatGPTが言ってた)。
export default class Transaction {
fromAddress: string;
toAddress: string;
amount: number;
constructor(fromAddress: string, toAddress: string, amount: number) {
this.fromAddress = fromAddress;
this.toAddress = toAddress;
this.amount = amount;
}
isValid(): boolean {
// Transactionが有効かどうかを判定するロジック
return true;
}
}
Block
時刻、ブロックチェーンの中での順番、含むトランザクションのリスト、自身のハッシュ値、一つ前のブロックのハッシュ値を情報として持つ。
mineBlock
関数がいわゆるマイニングの処理である。前のハッシュ値から、指定された難易度difficulty
に応じて特定の条件を満たすハッシュ値を計算するのである。なぜそのような複雑な計算処理をしなければいけないのかというと、不正防止やブロック生成速度の調整のためである、と解説されることが一般的なようだ。このような合意形成の方法をProof of Work(PoW)と呼ぶらしい。ビットコインではこの方法が使われている。ビットコインでは、計算リソースを消費するインセンティブとして新規トークンがマイニングの報酬として発行される。
import Transaction from "./Transaction";
const SHA256 = require("crypto-js/sha256");
export default class Block {
timestamp: number;
transactions: Transaction[];
previousHash: string;
hash: string;
nonce: number = 0;
index: number;
constructor(
index: number,
timestamp: number,
transactions: Transaction[],
previousHash: string
) {
this.index = index;
this.timestamp = timestamp;
this.transactions = transactions;
this.previousHash = previousHash;
this.hash = "";
}
calculateHash(): string {
return SHA256(
this.previousHash +
this.timestamp +
JSON.stringify(this.transactions) +
this.nonce
).toString();
}
mineBlock(difficulty: number): boolean {
this.transactions.forEach((transaction) => {
if (!transaction.isValid()) {
return false;
}
});
while (
this.hash.substring(0, difficulty) !== Array(difficulty + 1).join("0")
) {
this.nonce++;
this.hash = this.calculateHash();
}
return true;
}
isValid(): boolean {
// Blockが有効かどうかを判定するロジック
return true;
}
}
Blockchain
ブロックの配列とマイニングの難易度difficulty
をプロパティとして持つ。
新規ブロックの作成からマイニングまで実行するメソッドや、チェーン自体が有効な形式かどうかを判断するメソッドを持つ。
import Block from "./Block";
import Transaction from "./Transaction";
export default class Blockchain {
chain: Block[] = [];
difficulty: number;
constructor(genesisTransaction?: Transaction) {
this.difficulty = 2;
if (genesisTransaction) { // 今回はブロックチェーンが作られた際に最初のトランザクションを発生されられるようにした
this.chain.push(this.createGenesisBlock(genesisTransaction));
}
}
getLatestBlock() {
return this.chain[this.chain.length - 1];
}
// トランザクションの集合から新しくブロックを作るメソッド
minePendingTransactions(transactionsToMine: Set<Transaction>) {
let block = new Block(
this.chain.length,
Math.floor(new Date().getTime() / 1000),
Array.from(transactionsToMine),
this.getLatestBlock().hash
);
if (block.mineBlock(this.difficulty)) {
this.chain.push(block);
}
}
createGenesisBlock(genesisTransaction: Transaction) {
return new Block(
0,
Math.floor(new Date().getTime() / 1000),
[genesisTransaction],
"0"
);
}
isChainValid() {
// ブロックチェーンが有効な形式かどうかを判定するロジック
return true;
}
}
Node
長いので要点だけ。
WebSocket
ほかのノードと相互に通信する必要があるため、WebSocketサーバーを立てる。
BunはWebSocketサーバーをサポートしている。
this.server = Bun.serve({
fetch(req, server) {
if (server.upgrade(req)) { // WebSocket通信に昇格
return;
}
return new Response("Upgrade failed :(", { status: 500 });
},
websocket: {
open: (ws) => {
// WebSocketクライアントからの接続を開始したときの処理
},
message: async (ws, message) => {
// WebSocketクライアントからメッセージを受け取ったときの処理
}
close: (ws) => {
// 接続が終わったときの処理
}
},
});
ほかのノードとのやりとり
今回は次のような設計にした。
- 新規にWebSocketサーバーに接続したら自身の接続先エンドポイントを提供する
- 接続エンドポイントを共有されたらクライアントとして接続する。また、ほかの接続しているノードにもそのエンドポイントを共有する。
- それぞれのノードはトランザクションの発生を検知したらほかのノードと共有する
- それぞれのノードは、適当な時間間隔で、まだブロックにされていないトランザクションからブロックを作る(マイニングする)
- それぞれのノードはブロックを作成したらほかのノードに提供する
- 新しいブロックを提供されたら(本来であればvalidateして、接続している過半数のノードがOKだと判断したら)自身のブロックチェーンにつなげる
Web API
トランザクションの作成や、現在のアカウントの持ちポイントを確認するAPIを作った。冒頭の例の通りである。
本来であればトランザクションの発生元アカウントを認証するために署名が必要である。公開鍵暗号などが使われる。
おわり
以上、駆け足でしたがBunで作るブロックチェーンネットワークでした。
BunはTypeScriptで書いたものをそのまま動かせるので便利ですね。しかし現状だと実行時にちょこちょこ型エラーが出てしまっていますがご愛敬とさせてください。
ブロックチェーンが盛り上がっていた頃はふんわりとした概念しか知りませんでした。しかし実際に動くものを作ってみることで具体的な理解が進みました。ノード同士の接続情報のやり取りに齟齬が出ないよう設計するのが難しかったです。本来なら、今回作ったものに加えて不正防止や合意形成のためのアルゴリズムについても深く考慮しなければいけないので大変ですね。
また、BunのサイトにはAsk AI
というUIがあります。これが非常に便利でした。細かいBunのAPI仕様について対話的に調べることができます。いい時代ですね。これがWeb3か。
Discussion