200行のコードへのブロックチェーンの実装

ブロックチェーンの基本的な概念は非常にシンプルです。分散型データベースで、順序付けられたレコードのリストが連続的に増加していきます。しかしシンプルとは言え、ブロックチェーンやそれを使うことで解決しようとしている問題について話をする際に、頭を悩まされることがよくあります。これは、ビットコインイーサリアムといった、一般にもよく知られているブロックチェーンベースのプロジェクトでよく聞かれる話です。「ブロックチェーン」は、取引スマートコントラクト、または暗号通貨といったコンセプトと強い結びつきがあります。

そのため、本来シンプルであるべきブロックチェーンの理解がより困難になってしまっています。抜け目のないソースコードであれば尚更です。
そこで、NaiveChainという、200行のJavascripitに実装した、非常にシンプルなブロックチェーンを紹介したいと思います。

ブロックの構造

まず行うべき作業は、ブロックの構造を決めることです。可能な限りシンプルにするため、最低限必要となる、インデックス、タイムスタンプ、データ、ハッシュ値、そして1つ前のブロックのハッシュ値のみを構造に含めます。


チェーンの安全性を維持するために、ブロックの中には、1つ前のブロックのハッシュ値が含まれていなくてはなりません。

class Block {
    constructor(index, previousHash, timestamp, data, hash) {
        this.index = index;
        this.previousHash = previousHash.toString();
        this.timestamp = timestamp;
        this.data = data;
        this.hash = hash.toString();
    }
}       

ブロックのハッシュ

ブロックはデータの安全性を確保するため、ハッシュ化されている必要があり、SHA-256によって、ブロック内のコンテントが引き継がれます。ここには回答すべきプルーフ・オブ・ワークの問題はないので、このハッシュは「マイニング」とは何の関係もないということを覚えておいてください。

var calculateHash = (index, previousHash, timestamp, data) => { 
    return CryptoJS.SHA256(index + previousHash + timestamp + data).toString(); 
}; 

ブロックの生成

ブロックを生成するには、1つ前のブロックのハッシュ値を知っていなければならず、その上で、その他、必要なコンテンツ(インデックス、ハッシュ値、データ、タイムスタンプ)を作成します。ブロックデータは、エンドユーザによって提供されます。

var generateNextBlock = (blockData) => { 
     var previousBlock = getLatestBlock(); 
     var nextIndex = previousBlock.index + 1; 
     var nextTimestamp = new Date().getTime() / 1000; 
     var nextHash = calculateHash(nextIndex, previousBlock.hash, nextTimestamp, blockData); 
     return new Block(nextIndex, previousBlock.hash, nextTimestamp, blockData, nextHash); 
 }; 

ブロックの保存

ブロックチェーンを保存するには、インメモリのJavascriptの配列が使用されます。ブロックチェーンの最初のブロックは常に「ジェネシスブロック」と呼ばれ、ハードコーディングされています。

 var getGenesisBlock = () => { 
     return new Block(0, "0", 1465154705, "my genesis block!!", "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7"); 
 }; 


 var blockchain = [getGenesisBlock()]; 

ブロックの安全性の確認

いかなる場合でも、ブロックもしくはブロックのチェーンが安全性を満たしているか確認しなければなりません。特に他のノードから新しいブロックを受け取ったときや、承認すべきかどうかの決断をしなければならないときは尚更です。

 var isValidNewBlock = (newBlock, previousBlock) => { 
     if (previousBlock.index + 1 !== newBlock.index) { 
         console.log('invalid index'); 
         return false; 
     } else if (previousBlock.hash !== newBlock.previousHash) { 
         console.log('invalid previoushash'); 
         return false; 
     } else if (calculateHashForBlock(newBlock) !== newBlock.hash) { 
         console.log('invalid hash: ' + calculateHashForBlock(newBlock) + ' ' + newBlock.hash); 
         return false; 
     } 
     return true; 
 }; 

最長チェーンを選択

どんな場合でも、チェーンには明確なブロックのセットが1つでなくてはなりません。矛盾する点(例えば、2つのノードがどちらもブロック番号72を生成している)があった場合、ブロックの数が多いチェーンを選択します。


注釈:当初の矛盾点、解決済み、最長チェーンが優位

 var replaceChain = (newBlocks) => { 
     if (isValidChain(newBlocks) && newBlocks.length > blockchain.length) { 
         console.log('Received blockchain is valid. Replacing current blockchain with received blockchain'); 
         blockchain = newBlocks; 
         broadcast(responseLatestMsg()); 
     } else { 
         console.log('Received blockchain invalid'); 
     } 
 }; 

他のノードとの連絡

ノードに不可欠なのは、ブロックチェーンを他のノードと共有、また同期する点です。以下は、ネットワークが常に同期されるために必要なルールです。

  • ノードが新しいブロックを生成したら、ネットワークへと送信する。
  • ノードが新しいピアに連結したら、最新のブロックに対してクエリを行う。
  • ノードが、最新として知られるブロックのインデックスよりも大きいインデックスのブロックに出くわしたら、最新のチェーンにそのブロックを追加するか、全ブロックチェーンに対してクエリを行う。


(上から下、左から右の順)
注釈:
ノード1はノード2と連結し、同期する
最新のブロックにクエリを行う
最新のブロックを送信する
全てのブロックに対してクエリを行う
全てのブロックを送信する

ノード1がブロックを生成し、送信する
最新のブロックを送信する
最新のブロックを送信する
最新のブロックを送信する

ノードが表示されているプロトコルに従う際の一般的な連絡の流れ。

自動的にピアを発見するプロトコルは使用されていないので、ピアの場所(URL)は手動で追加しなくてはなりません。

ノードの制御

何らかの方法で、ユーザはノードを制御する必要があります。これはHTTPサーバを設定することで可能です。

var initHttpServer = () => {
    var app = express();
    app.use(bodyParser.json());

    app.get('/blocks', (req, res) => res.send(JSON.stringify(blockchain)));
    app.post('/mineBlock', (req, res) => {
        var newBlock = generateNextBlock(req.body.data);
        addBlock(newBlock);
        broadcast(responseLatestMsg());
        console.log('block added: ' + JSON.stringify(newBlock));
        res.send();
    });
    app.get('/peers', (req, res) => {
        res.send(sockets.map(s => s._socket.remoteAddress + ':' + s._socket.remotePort));
    });
    app.post('/addPeer', (req, res) => {
        connectToPeers([req.body.peer]);
        res.send();
    });
    app.listen(http_port, () => console.log('Listening http on port: ' + http_port));
};

以下の方法で、ユーザはノードとの情報交換が可能となります。

  • 全てのブロックを記載する。
  • ユーザから与えられたコンテントを基に新しいブロックを作成する。
  • ピアを記載するか追加する。

ノードを制御する最も単純な方法は、cURLなどを使用することです。

ノードから全てのブロックを取得する

curl http://localhost:3001/blocks

アーキテクチャ

ノードは、2つのWebサーバを露出することに注意してください。1つは、ノードを制御するためのユーザ向けのサーバ(HTTPサーバ)、もう1つは、ノード同士のピア・ツー・ピア連絡用サーバ(Websocket HTTPサーバ)です。


(上から下、左から右の順)
注釈:
ブロックチェーン
HTTPインターフェース、ノードの制御用
Websocketインターフェース、他のノードとのP2P連絡用

NaiveChainの主なコンポーネント

まとめ

NavieChainは、デモンストレーションおよび学習目的で作成されました。このブロックチェーンには「マイニング」アルゴリズム(プルーフ・オブ・ワークプルーフ・オブ・ステーク)は含まれていないので、パブリックネットワークで使用することはできませんが、ブロックチェーンを動かすための基本的な機能は実装されています。

技術的な詳細を知りたい方は、GitHubレポジトリをご覧ください。