@mizumotokのブログ

テクノロジー、投資、書評、映画、筋トレなどについて

javascriptでブロックチェーンをつくってみよう - 3 マイニングの実装

ビットコインJavascriptで実装しながら理解しましょう。
前回までにブロックチェーンの実装を行ないました。今回はブロックをマイニングする処理を実装してみます。

前回までの状態を再現

必要な場合は、前回の状態を再現しましょう。

$ git clone https://github.com/mizumotok/blockchain-js.git
$ cd blockchain-js
$ git checkout 02_blockchain


マイナーの実装

マイナーはブロックチェーンにアクセスできる必要がありますので、コンストラクタでブロックチェーンの参照を渡しておきます。
mineメソッドでは、生成したブロックが有効になるまで(Block#isValidメソッドがtrueを返すまで)、nonceをいじっています。このnonceを見つけてブロックインスタンスを生成する処理をマイニングといいます。今回のケースではnonceを数値として単純にインクルメントしているだけです。
src/miner/index.js

// @flow

import Blockchain, { Block } from '../blockchain';

class Miner {
  blockchain: Blockchain;

  constructor(blockchain: Blockchain) {
    this.blockchain = blockchain;
  }

  mine() {
    const prevHash = this.blockchain.lastHash();
    const target = 250;

    let nonce = 0;
    let block;
    let timestamp;

    do {
      timestamp = new Date().getTime();
      nonce += 1;
      block = new Block(
        timestamp,
        prevHash,
        target,
        nonce,
        [],
      );
    } while (!block.isValid());

    this.blockchain.addBlock(block);
  }
}

export default Miner;

ブロックチェーンでマイニングされたブロックの有効性確認

マイニングされたブロックはブロック単体ではBlock#isValidメソッドで確認できます。ブロックチェーンにつなげたときに、ブロックチェーン全体としての整合性を確認できるようにしておきます。
具体的にはブロックのprevHashにはその前のブロックのハッシュ値が正しく設定されているかを確認しています。

src/blockchain/blockchain.js

  static isValidChain(chain: Array<Block>) {
    if (JSON.stringify(chain[0]) !== JSON.stringify(Block.genesis())) {
      return false;
    }

    let prevBlock = null;
    return chain.every((block) => {
      if (!prevBlock) {
        prevBlock = block;
        return true;
      }
      if (!block.isValid()) {
        return false;
      }
      if (prevBlock.hash() !== block.prevHash) {
        return false;
      }
      prevBlock = block;
      return true;
    });
  }

マイニングのテスト

Mineクラスは今のところ、mineメソッドだけですので、有効なブロックが正しくブロックチェーンに追加されていることを確認します。
src/__tests__/miner/index.test.js

// @flow

import Blockchain from '../../blockchain';
import Miner from '../../miner';

describe('Miner', () => {
  let blockchain;
  let miner;

  beforeEach(() => {
    blockchain = new Blockchain();
    miner = new Miner(blockchain);
  });

  it('mine', () => {
    miner.mine();
    expect(blockchain.chain).toHaveLength(2);
    expect(Blockchain.isValidChain(blockchain.chain)).toBe(true);

    miner.mine();
    expect(blockchain.chain).toHaveLength(3);
    expect(Blockchain.isValidChain(blockchain.chain)).toBe(true);

    console.log(blockchain.chain);
  });
});

ついでの今回つくったBlockchain#isValidChainのテストも。

src/__tests__/blockchain/blockchain.test.js

  it('validChain test', () => {
    // genesisが間違っている
    let chain = [newBlock];
    expect(Blockchain.isValidChain(chain)).toBe(false);

    const genesis = Block.genesis();
    chain = [genesis];
    expect(Blockchain.isValidChain(chain)).toBe(true);

    // prevHashが間違っている
    chain = [genesis, new Block(0, 'xxx', 256, 0, [])];
    expect(Blockchain.isValidChain(chain)).toBe(false);

    // invalidなblockがある
    chain = [genesis, new Block(0, genesis.hash(), 0, 0, [])];
    expect(Blockchain.isValidChain(chain)).toBe(false);

    chain = [genesis, newBlock];
    expect(Blockchain.isValidChain(chain)).toBe(true);
  });

ここまでのソースはGitHubの03_minerブランチに公開しています。

ディフィカルティターゲットの調整

上記のコード(src/miner/index.js)ではディフィカルティターゲットは250に固定していました。ビットコインではマイニングが約10分で行われるように、ディフィカルティターゲットが自動調整されます。その仕組を取り入れましょう。
ビットコインでは絶え間なくマイニングし続けますので、マイニング時間は前回生成したブロックのタイムスタンプと今回生成したブロックのタイムスタンプの差を利用しています。練習のために絶え間なくマイニングし続けるのもアレなんで、マイニングにかかった時間をブロックに持たせてしまいましょう。

まずは、ディフィカルティターゲットの初期値とマイニング期待時間(今回は5秒)を外出し。

src/config.js

const DIFFICULTY_TARGET = 240;
const MINING_DURATION = 5000;

module.exports = {
  DIFFICULTY_TARGET,
  MINING_DURATION,
};

BlockクラスのプロパティにminingDurationを追加。それにあわせて、メソッドも修正。改ざん対策で、miningDurationもハッシュ値に使用します。

src/blockchain/block.js

// @flow

import SHA256 from 'crypto-js/sha256';
import { DIFFICULTY_TARGET, MINING_DURATION } from '../config';

class Block {
  timestamp: number;
  prevHash: string;
  difficultyTarget: number;
  nonce: number;
  transactions: Array<any>;
  miningDuration: number;

  constructor(
    timestamp: number,
    prevHash: string,
    difficultyTarget: number,
    nonce: number,
    transactions: Array<any>,
    miningDuration: number,
  ) {
    this.timestamp = timestamp;
    this.prevHash = prevHash;
    this.difficultyTarget = difficultyTarget;
    this.nonce = nonce;
    this.transactions = transactions;
    this.miningDuration = miningDuration;
  }

  static genesis(): Block {
    return new this(0, '0'.repeat(64), DIFFICULTY_TARGET, 0, [], MINING_DURATION);
  }

  hash(): string {
    return SHA256(JSON.stringify([
      this.timestamp,
      this.prevHash,
      this.difficultyTarget,
      this.nonce,
      this.transactions,
      this.miningDuration,
    ])).toString();
  }

  isValid(): bool {
    const hash = this.hash();
    return Number(`0x${hash}`) < 2 ** this.difficultyTarget;
  }
}

export default Block;

Blockchainクラスで次のディフィカルティターゲットを求めるメソッド(Blockchain#nextDifficultyTarget)を追加。ビットコインでは2016ブロックごとに調整が入るのですが、今回は前回のマイニング時間が予定より2割以上多くかかったか、2割以上短かったかだけで調整をいれました。

src/blockchain/blockchain.js

import { MINING_DURATION } from '../config';
  nextDifficultyTarget(): number {
    return Blockchain.calcDifficultyTarget(this.chain);
  }

  static calcDifficultyTarget(chain: Array<Block>): number {
    const lastBlock = chain[chain.length - 1];
    if (lastBlock.miningDuration > MINING_DURATION * 1.2) {
      return lastBlock.difficultyTarget + 1;
    }
    if (lastBlock.miningDuration < MINING_DURATION * 0.8) {
      return lastBlock.difficultyTarget - 1;
    }
    return lastBlock.difficultyTarget;
  }

Miner#mineメソッド内でBlockchain#nextDifficultyTargetを呼び出します。マイニングにかかった時間も含めてブロックを生成します。

src/miner/index.js

  mine() {
    const miningStartTimestamp = new Date().getTime();
    const prevHash = this.blockchain.lastHash();
    const target = this.blockchain.nextDifficultyTarget();

    let nonce = 0;
    let block;
    let timestamp;

    do {
      timestamp = new Date().getTime();
      nonce += 1;
      block = new Block(
        timestamp,
        prevHash,
        target,
        nonce,
        [],
        timestamp - miningStartTimestamp,
      );
    } while (!block.isValid());

    this.blockchain.addBlock(block);
  }

ディフィカルティターゲット調整のテスト

$ yarn test

Blockクラスのプロパティを変更したので、いくつかのテストでは失敗しますので、それを修正します。
そのあと、今回開発したBloackchain#calcDifficultyTargetのテストを入れておきましょう。

src/__tests__/blockchain/blockchain.test.js

  it('calcDifficultyTarget test', () => {
    const difficultyTarget = 250;
    const longMiningBlock = new Block(
      new Date(),
      blockchain.chain[0].hash(),
      difficultyTarget,
      0,
      [],
      MINING_DURATION * 2,
    );
    const chain = blockchain.chain.concat([longMiningBlock]);
    expect(Blockchain.calcDifficultyTarget(chain)).toBe(difficultyTarget + 1);

    const shortMiningBlock = new Block(
      new Date(),
      blockchain.chain[0].hash(),
      difficultyTarget,
      0,
      [],
      MINING_DURATION / 2,
    );
    chain.push(shortMiningBlock);
    expect(Blockchain.calcDifficultyTarget(chain)).toBe(difficultyTarget - 1);

    const justRightBlock = new Block(
      new Date(),
      blockchain.chain[0].hash(),
      difficultyTarget,
      0,
      [],
      MINING_DURATION * 1.1,
    );
    chain.push(justRightBlock);
    expect(Blockchain.calcDifficultyTarget(chain)).toBe(difficultyTarget);
  });

ここまでのソースはGitHubの04_adjust_difficultyブランチに公開しています。
また、ソースの変更点はこちらで確認できます。

まとめ

今回はマイニング処理の実装をしました。ブロックのハッシュ値がターゲットディフィカルティから計算される有効性を満たすようなnonceを見つけ出すことができるようになりました。
また、ターゲットディフィカルティを前回のマイニング時間を元に自動調整するようにしました。
次回は送金処理(トランザクション)を実装します。

GitHub

GitHub - mizumotok/blockchain-js

ビットコインとブロックチェーン:暗号通貨を支える技術

ビットコインとブロックチェーン:暗号通貨を支える技術