自分でコードを書きながらブロックチェーンを勉強した
マネーゲームとしての仮想通貨は興味はないのだが、技術的に興味があって自分で簡単なコードを写経しながら勉強した。
定義
- ブロックチェーンの実体はブロックを繋いだリスト構造
- ブロックはいくつかの入力値(生成日時など)と、自分自身のハッシュを持っている
- 前のブロックのハッシュ値と、入力値を元に自分自身のハッシュが決まる。その手順は公開されている。
要はハッシュ値とそのメタデータが連続するただの配列なりの LinkedList。面白いのはここから。
- ネットワークに参加するそれぞれが任意に新しいブロックを追加することができる
- ブロックチェーンは検証結果が正しく、より長いものが信頼される
- なのでビットコインみたいな仮想通貨は、生成コストが重く、検証コストが軽いものが好まれる。
- 他のネットワーク参加者からブロックチェーンの更新を受け取った時、手元のブロックチェーンとそれを比較し、より長いものを自分のブロックチェーンとして受け入れる
ここでいうネットワーク参加者は、P2Pでもなんでもいい。
https://github.com/lhartikk/naivechain/blob/master/main.js を参考にしながら、 JS (babel + flowtype) でアレンジしながら写経した。
/* @flow */ import SHA256 from 'crypto-js/sha256' type Block = { index: number, previousHash: string, timestamp: number, data: string, hash: string } type BlockChain = Block[] /* initial block chain */ export function getGenesisBlock(): Block { return createBlock( 0, '0', 1465154705, 'my genesis block!!', '816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7' ) } export function getLatestBlock(blockchain: BlockChain): Block { return blockchain[blockchain.length - 1] } /* implementation */ export function createBlock( index: number, previousHash: string, timestamp: number, data: any, hash: string ): Block { return { index, previousHash, timestamp, data, hash } } export function calculateHashForBlock(block: Block) { return calculateHash( block.index, block.previousHash, block.timestamp, block.data ) } export function calculateHash( index: number, previousHash: string, timestamp: number, data: string = '' ): string { return SHA256(index + previousHash + timestamp + data).toString() } export function generateNextBlock( blockchain: BlockChain, blockData: string = '' ): Block { const previousBlock = getLatestBlock(blockchain) const nextIndex = previousBlock.index + 1 const nextTimestamp = ~~(Date.now() / 1000) const nextHash = calculateHash( nextIndex, previousBlock.hash, nextTimestamp, blockData ) return { index: nextIndex, previousHash: previousBlock.hash, timestamp: nextTimestamp, data: blockData, hash: nextHash } } export function isValidNewBlock( newBlock: Block, previousBlock: Block ): boolean { 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 } export function isValidChain(blockchainToValidate: BlockChain): boolean { // check genesis block if ( JSON.stringify(blockchainToValidate[0]) !== JSON.stringify(getGenesisBlock()) ) { return false } const tempBlocks = [blockchainToValidate[0]] for (var i = 1; i < blockchainToValidate.length; i++) { if (isValidNewBlock(blockchainToValidate[i], tempBlocks[i - 1])) { tempBlocks.push(blockchainToValidate[i]) } else { return false } } return true } export function addBlock(blockchain: BlockChain, newBlock: Block): BlockChain { if (isValidNewBlock(newBlock, getLatestBlock(blockchain))) { return blockchain.concat([newBlock]) } return blockchain }
(こういうのこそテストコード書いたほうが良さそう…)
とくに難しいコードはない。 見るべきは検証部分の calculateHashForBlock(newBlock) !== newBlock.hash
だろうか。その中身は今回はSHA256を計算しているだけ。
このコードを簡単に動かす。
/* @flow */ import { getGenesisBlock, generateNextBlock, getLatestBlock, addBlock, isValidChain, isValidNewBlock } from './index' // 初期ブロックチェーンを生成 const blockchain = [getGenesisBlock()] // ブロックを一個生成 const prev = getLatestBlock(blockchain) const next1 = generateNextBlock(blockchain, 'foo') const newBlockchain1 = addBlock(blockchain, next1) // もう一個生成 const next2 = generateNextBlock(newBlockchain1, 'bar') const newBlockchain2 = addBlock(newBlockchain1, next2) // 今のブロックチェーンが妥当か検証 console.log('current', newBlockchain2) console.log('isValidChain', isValidChain(newBlockchain2)
これだけだと面白くないので、ローカルに競合する仮想的なネットワーク参加者を作って走らせてみる。
/* @flow */ import { getGenesisBlock, generateNextBlock, getLatestBlock, addBlock, isValidChain, isValidNewBlock } from './index' import range from 'lodash.range' const blockchain = [getGenesisBlock()] const wait = n => new Promise(resolve => setTimeout(resolve, n)) // これは全員がシングルトンのバッファを持つのは実装的によくないのだが、簡単なので… let receivedBlockchain = [getGenesisBlock()] // receivedBlockchain を更新するブロードキャスト関数 const bloadcast = (name, next) => { console.log(`${name} bloadcasted`) receivedBlockchain = next } const createMiner = (name: string) => { // Miner自身のブロックチェーンを用意 let myBlockchain = blockchain.slice() // receivedBlockchain を信用するかどうか const accept = () => { console.log( `${name} checks receivedBlockchain. Size: ${receivedBlockchain.length}` ) if (receivedBlockchain.length > myBlockchain.length) { if (isValidChain(receivedBlockchain)) { console.log(`${name} accepted received blockchain`) myBlockchain = receivedBlockchain } else { console.log('Received blockchain invalid') } } } // 自分自身でブロックを追加 const addNewBlock = () => { const next = generateNextBlock(myBlockchain) myBlockchain = addBlock(myBlockchain, next) console.log(`${name} add new block: ${next.hash}`) } // ランダムな時間 wait しながら 0~2 個のブロックを追加 // ランダムなのでどこかで競合して巻き戻ったりする return async () => { while (true) { await wait(Math.random() * 3000) accept() range(~~(Math.random() * 3)).forEach(_ => addNewBlock()) bloadcast(name, myBlockchain) } } } // 3人ほど走らせてみる createMiner('miner1')() createMiner('miner2')() createMiner('miner3')()
実行結果
... miner2 checks receivedBlockchain. Size: 15 miner2 add new block: 5f9f37ce0a84dd148138e733ea63630c4c8482f99362aac7c23463b7594e02f9 miner2 bloadcasted miner1 checks receivedBlockchain. Size: 16 miner1 accepted received blockchain miner1 add new block: 2b2594d6cc92e3a25f5f567a645b81c0894e5271482440a4b8cdb2655f195553 miner1 add new block: 1e1018d769aa5a47b0b62e9176c8332e2e475869b43b062efcc31ace9fb1d1fb miner1 bloadcasted miner1 checks receivedBlockchain. Size: 18 miner1 bloadcasted miner1 checks receivedBlockchain. Size: 18 miner1 add new block: 861633faa0478234004d0557ddabc1946892f2ad1f0474a90334a118842d708d miner1 add new block: b5ba3acce6d6dc09c26808ddee6ad8ee82ad99bcb0bae148e41a786d03d157e9 miner1 bloadcasted miner3 checks receivedBlockchain. Size: 20 miner3 accepted received blockchain miner3 add new block: dc5d8959934dfe7843a5d263eb8d34dca8bd2a301d8471d06315aa667ca6db2f miner3 bloadcasted miner3 checks receivedBlockchain. Size: 21 miner3 add new block: 18fd2d81753d1b12c8ee4af1ef7623810cc9ed33d7e20353f4577c8ba9def2e5 miner3 bloadcasted miner2 checks receivedBlockchain. Size: 22 miner2 accepted received blockchain miner2 add new block: 304f91a4ee6b10f022988a22a07b5724139a4023dfcd7cd72f50be0452962ccf miner2 bloadcasted miner2 checks receivedBlockchain. Size: 23 miner2 add new block: d91a1ca6137dce1b6cb2fd5c2a087e75193568d9a6d5f675c37cf85542edeaca miner2 add new block: 0fe784699ff2b63b3affaccee27d962637fbd1aedbb50dcf2892bbddcd00d154 miner2 bloadcasted miner3 checks receivedBlockchain. Size: 25 miner3 accepted received blockchain miner3 add new block: 577f63f1a5093cffe4e8ce086a8977105bb5d457f7b9424160a03883921365e3 miner3 add new block: ccf166e561e75cf865e7314105f098b022116977061ad131c253a1fb03766d15 miner3 bloadcasted miner2 checks receivedBlockchain. Size: 27 miner2 accepted received blockchain miner2 add new block: 9b69c13ecc83b5d2216fbede77e0d1ceeb634f12ec237fe7b38d2c363b4091ff miner2 bloadcasted miner1 checks receivedBlockchain. Size: 28 miner1 accepted received blockchain miner1 add new block: 953cdde0848bb5ec9a727638383f680d5507759ed4496cab908d34e154d9676f miner1 add new block: 37b295ad1b9ccd1de70eeb0292a84dfa67263cbf1513e21f2aaec2b8ec59fc08 miner1 bloadcasted miner3 checks receivedBlockchain. Size: 30 miner3 accepted received blockchain miner3 add new block: fe47c3f3c892b4cfa6954322d0203751d7d9dea4eb8431513c259165772a2284 miner3 bloadcasted miner2 checks receivedBlockchain. Size: 31 miner2 accepted received blockchain miner2 bloadcasted miner2 checks receivedBlockchain. Size: 31 miner2 add new block: 44a718dc29699a4599b79b5b6cc21eef335699dd8fc1439696402725e3d5cedb miner2 bloadcasted
まあなんか動いてそう。 本当はちゃんと p2p で動かしたり、敵対的な嘘の検証を行ったり publish する miner とかを混ぜてみたかったけど面倒なので略。あとで気が向いたらやる。