🐕

【Dev】Blockchainをjsで実装してみる(その5:マルチノードの仕組み編)

2022/08/22に公開約7,300字

技術Blogを継続して更新するのはやはり難しいです。そもそも書くネタとなるインプットの時間を確保しなきゃいけないし、そしてBlogを書き書きする時間も必要だし、だけど毎日犬の散歩は朝夕一時間ずつ、計2時間しなきゃいけないし、他にも勉強したいこともあるし、それ以前にそもそも起きてる間ずーーーっと眠いし。だけど先日久しぶりに勉強会で登壇したり、そうこうしていたらやっぱりBlogの更新は大事だと改めて感じたのでした。

ということで、約10ヶ月空いてしまいましたが、「Blockchainをjsで実装してみる」の前回の続きです。

復習

ここまでの「Blockcahinをjsで実装してみる」シリーズを振り返ってみると、

  1. 機能要件編。複数ノードで同一台帳を同期をとって管理する。トランザクションの改変は不可。ということをBlockchainの機能要件として確認。要件が雑すぎますが、まぁそういうことです。
  2. Blockの中身編。Blockchain = Block + Chain。ということで、Chainで繋がれるBlockにどのようなデータが入っているか確認。先日も「Blockの中にはナンス値とハッシュ値が入っています。」と話しているのを横で耳にしたのですが、そうなんです。
  3. Transactionの中身編。Blockchainは台帳であるので、「誰が」「誰に」「どれくらい」というトランザクション情報が含まれているのです。トランザクションは一つのブロックの中にたくさん含まれているのですね。
  4. Chainの仕組み編。Blockchainには沢山のBlockが直接に繋がっていて、前後のBlockのHashの一貫性が保たれることで、データの整合製を担保している。的な話です。Blockchainで一番面白い部分だと思います。

ということでその続き、今回はその5:マルチノードの仕組み編です。

Blockchainとは An Immutable, Distributed Ledger だそうです。 直訳すると、不変的な分散型の台帳となります。Blockchainにおいて分散というワードがポイントになりますが、日本語では「分散」という一つの単語でも実際英語で表現される際は二つの意味の「分散」があります。

  • 分散(Distributed)
  • 分散(Decentralized)

この二つがごっちゃになって日本では語られてしまうので一度個人的に思っていることを別のBlog記事に書いたことがありました。今日ここでこれから取り上げるのは "Distributed" の方の分散の仕組みです。そして面白いのは、 "Distributed" なシステムを構築することが "Decentralized" なシステムの要素の一部分を生み出すことになるのです。うひゃー!

とりあえず複数ノードを立ててしまう

Blockchainでは以下の図のようにマルチノードでデータが管理されます。マルチノードをもう少し丁寧に噛み砕くと、複数のサーバでデータを同期して管理している。という言い方になるでしょうか。

まず、複数ノードを起動できるように設定ファイルを修正します。

package.json
  "scripts": {
    "node_1": "nodemon --watch dev -e js dev/networkNode.js 3001 http://localhost:3001",
    "node_2": "nodemon --watch dev -e js dev/networkNode.js 3002 http://localhost:3002",
    "node_3": "nodemon --watch dev -e js dev/networkNode.js 3003 http://localhost:3003",
    "node_4": "nodemon --watch dev -e js dev/networkNode.js 3004 http://localhost:3004",
    "node_5": "nodemon --watch dev -e js dev/networkNode.js 3005 http://localhost:3005"
  },

このようにすることで、ポート3001-3005で5つのノードを起動させることができます。

npm run node_1
> blockchain@1.0.0 node_1
> nodemon --watch dev -e js dev/networkNode.js 3001 http://localhost:3001

[nodemon] 2.0.19
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): dev/**/*
[nodemon] watching extensions: js
[nodemon] starting `node dev/networkNode.js 3001 http://localhost:3001`
Listening on port 3001...
npm run node_2

> blockchain@1.0.0 node_2
> nodemon --watch dev -e js dev/networkNode.js 3002 http://localhost:3002

[nodemon] 2.0.19
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): dev/**/*
[nodemon] watching extensions: js
[nodemon] starting `node dev/networkNode.js 3002 http://localhost:3002`
Listening on port 3002...

こんな感じでノードを5つ立てます。ノードを立てた直後のBlockchainはこんな感じです。

localhost(3001)/blockchain
{
  "chain": [
    {
      "index": 1,
      "timestamp": 1660462380627,
      "transactions": [
        
      ],
      "nonce": 100,
      "hash": "0",
      "previousBlockHash": "0"
    }
  ],
  "pendingTransactions": [
    
  ],
  "currentNodeUrl": "http://localhost:3001",
  "networkNodes": [
    
  ]
}

起動直後のノードについて押さえておきたいポイントは以下の通り。

  • GenesisBlock(一番最初のBlock)として、ひとつ適当なBlockが生成済みである。
  • pendingTransactionsには何もない
  • networkNodesにも何もない(まだ各ノード同士が同期していない)

複数ノードで同期を取る仕組み

5つノードを立てても、そのままではそれぞれが別ネットワークのBlockchainとして起動しています。これらのノードを全て同期させて一つの分散型Blockchainに仕立てるのがこれから行うコーディングの目的です。それを実現ために、3つのREST APIをつくります。

  1. 新規ノードとなるURIを受け取り、全ノードに配信するAPI(/register-and-broadcast-node)
  2. 個々のノードにおいて新規のネットワークノードを登録するAPI(/register-node)
  3. 新規ノードにおいて、既存のネットワークノード全てを登録するためのAPI(register-node-bulk)

自分でもそれぞれのAPIがなんなのか混乱するので一度整理してみました。図にするとこんな感じです。

もともと各BlockでNonceを持ち、チェーンのように繋がる仕組みは改ざんをするのが非常に困難です。しかし、仮にこの困難な改ざんが行われたとしましょう。シングルノードでデータが管理されている場合は1ノードが改ざんされたらその時点で改ざんは成功です。しかしノードが複数ある場合、一つのノードにおいて改ざんが成功しても他ノードとのデータの乖離が生じるためにすぐに改ざんが発覚し、データのリカバリを行うことができます。特定のノードのみでデータを管理するCentralizedな仕組みでは実現できない改ざん防止策が、Distributedなシステムを構築することでDecentralizedになり、実現できるのです。わーい!

ではまずは一番シンプルな2の/register-node からコードを見てみます。

dev/networkNode.js
// register a node with the network
app.post('/register-node', function (req, res) {
    const newNodeUrl = req.body.newNodeUrl;
    const nodeNotAlreadyPresent = bitcoin.networkNodes.indexOf(newNodeUrl) == -1;
    const notCurrentNode = bitcoin.currentNodeUrl !== newNodeUrl;
    if (nodeNotAlreadyPresent && notCurrentNode) {
        bitcoin.networkNodes.push(newNodeUrl);
    }
    res.json({ note: 'New node registerd successfuly.' });
});

これは単純に、リストの中にノードが追加されていなければノードを追加しているだけです。続けて、3の/register-and-broadcast-node も見てみましょう。

dev/networkNode.js
// register multiple nodes at once
app.post('/register-nodes-bulk', function (req, res) {
    const allNetworkNodes = req.body.allNetworkNodes;
    allNetworkNodes.forEach(networkNodeUrl => {
        const nodeNotAlreadyPresent = bitcoin.networkNodes.indexOf(networkNodeUrl) == -1;
        const notCurrentNode = bitcoin.currentNodeUrl !== networkNodeUrl;
        if (nodeNotAlreadyPresent && notCurrentNode) {
            bitcoin.networkNodes.push(networkNodeUrl);
        }
    });
    res.json({ note: 'Bulk registration successful.' });
});

allNetworkNodesの中にはその名の通り全てのノードが入っていますので、こいつをひたすらループでnetworkNodes に追加します。では最後に、一連の処理の開始となる/register-and-broadcast-node はこのようになります。

dev/networkNode.js
// register a node and broadcast it the network.
app.post('/register-and-broadcast-node', function (req, res) {
    const newNodeUrl = req.body.newNodeUrl;
    if (bitcoin.networkNodes.indexOf(newNodeUrl) == -1) {
        bitcoin.networkNodes.push(newNodeUrl);
    }

    const regNodesPromises = [];
    bitcoin.networkNodes.forEach(networkNodeUrl => {
        const requestOptions = {
            uri: networkNodeUrl + '/register-node',
            method: 'POST',
            body: { newNodeUrl: newNodeUrl },
            json: true
        };

        regNodesPromises.push(rp(requestOptions));
    });

    Promise.all(regNodesPromises)
        .then(data => {
            const bulkRegisterOptions = {
                uri: newNodeUrl + '/register-nodes-bulk',
                method: 'Post',
                body: { allNetworkNodes: [...bitcoin.networkNodes, bitcoin.currentNodeUrl] },
                json: true
            };

            return rp(bulkRegisterOptions);
        })
        .then(data => {
            res.json({ note: 'New node registerd with network successfully.' })
        });
});

他と比べて少し長ったらしいですが、よくよく見れば大したことはやっていません。

  1. まずは自ノードの networkNodes に新しいノードをpushします。
  2. 各ノードに対するリクエストオプションをリストで作成します。
  3. Promise.all(regNodesPromises)で全ノードの /register-node をリクエストします。
  4. 全てレスポンスが返ってきたら新しいノードの /register-nodes-bulk にリクエストを投げます。

という、これだけです。実際のプロダクションでこの仕組みが使えるかというともっと考慮すべき点は多々ありますが、まず自分でBlockchainを構築してみる、という観点ではこれでBlockchainの概要は掴めると思います。(これはこのBlogで扱っている全てのコードに当てはまるのですが)

ノードの同期が準備できたら、次はどのようにBlockを同期するかですね。その話はまた次回、なるべく時間を空けずに記事にしたいと思います。ここまできたら最後まで書きたいなぁ...

Discussion

ログインするとコメントできます