본문 바로가기
경일/Block-chain

[Block-chain/블록체인] chain/difficulty/mining 구현

by dev_kong 2022. 6. 12.
728x90
728x90

0.목차

1. 개요

2. POW(작업증명)

3. chain 구현

4. difficulty구현

5. mining 구현

1. 개요

저번 포스팅에서 block의 생성과 구조에 대해 알아보았다.
이번에는 mining(채굴) 기능을 구현해보려 하는데,
선결 과제가 주어진다.


mining을 구현 하기 위해서는 difficulty를 우선 구현 해야한다.

 

우선은 모르는 개념이 많으니, 개념부터 잡고 가면 좋을 듯 하다.

2. POW(Proof Of Work)

이걸 전체적으로 아우르는 개념을 POW(작업증명)이라 할 수 있다.


작업증명이란 작업을 통해 단순한 연산문제를 풀고, 해당 작업을 마치면, 보상을 받아내는 방식을 일컫는다.

 

기존에도 사용이 되던 방식이지만(스팸메일을 처리하기우해 사용 됐었음),
비트코인의 창시자 나카시 사카모토가 해당 기술을 채굴에 도입하면서,
현재에 와서는 적어도 암호화폐 세계에서는 POW와 채굴(MINING)은 거의 같은 개념으로 사용 되고 있다.

 

그럼 채굴이란 정확히 무엇일까


좀전에 POW란 작업을 통해 보상을 받아내는 방식이라고 말했는데,
채굴에서의 작업은 난이도(difficulty)에 따른 특정 해시값을 만들어 내는 nonce를 찾아내는 것을 의미한다.
그리고, 보상으로서는 코인을 받는다.

 

코인마다 난이도에 대한 의미가 다르다고 들었는데,
우선 내가 구현할 것은 hash값이 몇개의 0으로 시작하는가를 난이도로 정의하고 구현해보려 한다.

 

난이도의 기준은 하나의 블록이 생성되는 시간을 사용할 수 있다.

예를 들어 10분마다 하나의 블록이 생성되게 하고 싶다면,

전체 블록에서 직접적으로 연결되어있는 10개의 블록을 선택해서
가장 먼저 만들어진 블록의 타임스탬프와, 가장 마지막에 만들어진 블록의 타임스탬프의 차를 구하면,
블록이 생성되는데 걸린 대략적인 평균을 구할 수가 있다.

 

평균 블록 생성 소요시간이 내가 지정한 시간보다 빠르면 난이도를 높이고,
평균 블록 생성 소요시간이 내가 지정한 시간 보다 너무 느리면 난이도를 낮추는 식으로,
난이도를 구현 할 수 있다.

3. chain 구현

difficulty(난이도)를 구현 하기 위해서는
chain을 구현 해야 한다.

 

chain은 뭐 별거 없다.


그냥 만들어진 block 들을 담아두는 배열이라 생각하면 편할 것 같다.
굳이 배열에다 담아두는 이유는 그렇게 함으로써 difficulty를 구현하기가 쉬워진다.

 

import { Block } from './block';

export class Chain {
  blockchain: Block[];
  constructor() {
    this.blockchain = [Block.getGENESIS()];
  }

  //chain 확인용
  getChain(): Block[] {
    return this.blockchain;
  }

  //chain의 블록갯수 확인
  getLength(): number {
    return this.blockchain.length;
  }

  //가장 마지막 블록 가져오기
  getLatestBlock(): Block {
    const length = this.getLength();
    return this.blockchain[length - 1];
  }
}

 

우선은 이렇게 기본틀을 잡아놓으면 된다.

 

blockchain 안에는 기본적으로 GENESIS block 들어있어야 하니까, GENESIS 블록을 넣어놓은 채로 배열을 생성해주면 된다.

 

이제 작성해야되는 것은
해당 blockchain 에 블록을 추가하는 메서드를 작성해줘야한다.

 

import { Block } from './block';
export class Chain {
  blockchain: Block[];
  constructor() {
    this.blockchain = [Block.getGENESIS()];
  }

  getChain(): Block[] {
    return this.blockchain;
  }

  getLength(): number {
    return this.blockchain.length;
  }

  getLatestBlock(): Block {
    const length = this.getLength();
    return this.blockchain[length - 1];
  }

  addBlock(data: string[]): Failable<Block, string> {
    const previousBlock: Block = this.getLatestBlock();
    const adjustmentBlock: Block = this.getAdjustmentBlock();
    const newBlock: Block = Block.generateBlock(
      previousBlock,
      data,
      adjustmentBlock,
    );

    const isValid: Failable<Block, string> = Block.isValidNewBlock(
      newBlock,
      previousBlock,
    );

    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }

    this.blockchain.push(newBlock);
    return { isError: false, value: newBlock };
  }
}

 

addBlock 함수에 못보던 함수 두개가 추가 됐다.
첫번째는 getAdjustmentBlock 이란 함수이고,
두번째는 generateBlock 함수이다.

 

우선 getAdjustmentBlock 부터 작성을 해보자.


그전에, getAdjustmentBlock 의 역할에 대해서 알아봐야 한다.

 

우리가 diffilculty를 구할 때,
지정한 블록 갯수가 만들어진 총 시간을 구해서
우리가 지정한 시간과 비교해서 난이도를 조정한다고 했다.

 

나는 블록 10개가 만들어지는 시간을 기준으로 잡았기 때문에,
현재 blockchain의 length가 10보다 작으면 GENESIS블록을 리턴해주고,

length가 10보다 크고, 10의 배수이면, 10개전의 블록을 리턴,
length가 10보다 크고, 10의 배수가 아니면 이전 블록을 리턴해준다.

 

굳이 이렇게 해주는 이유는 11번째, 21번째, 31번째... 블록을 생성할 때 마다 난이도를 체크하기 위함이다.

 

import { Block } from './block';
import { DIFFICULTY_ADJUSTMENT_INTERVEL, GENESIS } from '@core/config';

export class Chain {
  blockchain: Block[];
  constructor() {
    this.blockchain = [Block.getGENESIS()];
  }

  getChain(): Block[] {
    return this.blockchain;
  }

  getLength(): number {
    return this.blockchain.length;
  }

  getLatestBlock(): Block {
    const length = this.getLength();
    return this.blockchain[length - 1];
  }

  addBlock(data: string[]): Failable<Block, string> {
    const previousBlock: Block = this.getLatestBlock();
    const adjustmentBlock: Block = this.getAdjustmentBlock();
    const newBlock: Block = Block.generateBlock(
      previousBlock,
      data,
      adjustmentBlock,
    );

    const isValid: Failable<Block, string> = Block.isValidNewBlock(
      newBlock,
      previousBlock,
    );

    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }

    this.blockchain.push(newBlock);
    return { isError: false, value: newBlock };
  }

  getAdjustmentBlock(): Block {
    const curLength = this.getLength();
    if (curLength < DIFFICULTY_ADJUSTMENT_INTERVEL) {
      return GENESIS;
    } else if (curLength % 10 === 0) {
      return this.blockchain[curLength - DIFFICULTY_ADJUSTMENT_INTERVEL];
    } else {
      return this.getLatestBlock();
    }
  }
}

 

이렇게 작성해주면 된다.

 

여기까지 코드를 말로 풀어보면,


addBlock 메서드가 실행되면,

blockchain 배열에서 가장 마지막 배열을 가져온다.


이 블록은 가장 최근에 만들어진, 즉 previousBlock이 된다.

 

그리고 난이도를 구하기 위해 10번 째 전의 배열 또는 GENESIS 블록을
getAdjustmentBlock 함수를 통해 가져온다.

 

그리고 addBlock의 인자로 받은 data를 포함해서
generateBlock 함수를 실행하고,
generateBlock을 통해 만들어진 블록에대한 유효성을 검사해준 뒤에,
검사를 통과하면 blockchain 배열에 push를 통해 추가해준다.

 

그럼 이번엔 Block class 에서 generateBlock 함수를 작성하면 된다.

 

  static generateBlock(
    _previousBlock: Block,
    _data: string[],
    _adjustmentBlock: Block,
  ) {
    const generated = new Block(_previousBlock, _data, _adjustmentBlock);
    // 난이도에 따른 nonce값을 구하는 함수(findBlock) 호출
    const newBlock = Block.findBlock(generated);
    return newBlock;
  }

 static findBlock(_generated: Block) {
   // TODO: 난이도에 따른 nonce 값을 구하는 함수 작성
     return _generated;
  }

 

우선은 요렇게 해주면 된다.


findBlock의 TODO 내용을 보면 알수 있듯,
해당 코드는 난이도(difficulty) 따른 nonce 값을 구하는 코드가 작성 되어야 한다.


그리고 이 과정(nonce값을 구하는 과정)을 mining 이라고 한다.

 

3. difficulty 구현

import { SHA256 } from 'crypto-js';
import merkle from 'merkle';

import { BlockHeader } from '@core/blockchain/blockHeaer';
import hexToBinary from 'hex-to-binary';
import {
  BLOCK_GENERATION_INTERVAL,
  DIFFICULTY_ADJUSTMENT_INTERVEL,
  GENESIS,
  UNIT,
} from '@src/core/config';

export class Block extends BlockHeader implements IBlock {
  public hash: string;
  public merkleRoot: string;
  public data: string[];
  nonce: number;
  difficulty: number;

  constructor(_previousBlock: Block, _data: string[], _adjustmentBlock: Block) {
    super(_previousBlock);
    this.merkleRoot = Block.getMerkleRoot(_data);
    this.hash = Block.createHash(this);
    this.nonce = 0;
    this.difficulty = Block.getDifficulty(
      this,
      _adjustmentBlock,
      _previousBlock,
    );
    this.data = _data;
  }

  static getGENESIS(): Block {
    return GENESIS;
  }

  static getMerkleRoot(_data: string[]): string {
    const merkleTree = merkle('sha256').sync(_data);
    return merkleTree.root() || '0'.repeat(64);
  }

  static createHash(_newBlock: Block): string {
    const {
      version,
      timestamp,
      merkleRoot,
      previousHash,
      height,
      difficulty,
      nonce,
    } = _newBlock;
    const values: string =
      version +
      timestamp +
      merkleRoot +
      previousHash +
      height +
      difficulty +
      nonce;

    return SHA256(values).toString();
  }

  // 블럭 검증 코드
  static isValidNewBlock(
    _newBlock: Block,
    _previousBlock: Block,
  ): Failable<Block, string> {
    /* 
    1. 이전 블록 높이 +1 === 새로생긴 블록 높이 check
    2. 이전블록.해시 === 새로생긴블록.이전해시 check
    3. 새로생긴 블록의 해시를 새로 만듬 === 새로생긴블록.해시 check
     */

    if (_previousBlock.height + 1 !== _newBlock.height) {
      return { isError: true, error: '블록 높이가 맞지 않습니다.' };
    }

    if (_previousBlock.hash !== _newBlock.previousHash) {
      return { isError: true, error: '이전 해시값이 맞지않습니다.' };
    }

    if (Block.createHash(_newBlock) !== _newBlock.hash) {
      return { isError: true, error: '블록해시가 올바르지 않습니다.' };
    }

    return { isError: false, value: _newBlock };
  }

 static generateBlock(
    _previousBlock: Block,
    _data: string[],
    _adjustmentBlock: Block,
  ) {
    const generated = new Block(_previousBlock, _data, _adjustmentBlock);
    // 난이도에 따른 nonce값을 구하는 함수(findBlock) 호출
    const newBlock = Block.findBlock(generated);
    return newBlock;
  }

 static findBlock(_generated: Block) {
   // TODO: 난이도에 따른 nonce 값을 구하는 함수 작성
     return _generated;
  }

  static getDifficulty(){}
}

 

Block의 property에 nonce와 difficulty 를 추가해주고,
nonce에는 0을, difficulty 에는 getDifficulty 함수의 리턴값을 넣어주었다.

 

getDifficulty의 내용을 채워주자.

 

몇가지 조건을 걸러주는 if문이 들어간다.

 

1.adjustmentBlock이 GENESIS Block 일때는 난이도를 0으로 리턴한다.
2.생성된 블락의 height가 10의 배수가 아니라면 adjustmentBlock(previousBlock)의 난이도를 리턴한다.
3.위 두가지 조건에 걸리지 않는다면 블록생성시간을 확인해서 난이도를 조정한다.

 

 

static getDifficulty(
    _newBlock: Block,
    _adjustmentBlock: Block,
  ): number {

    // adjustmentBlock 가 GENESIS 일 때,
    if (_adjustmentBlock.height === 0){
      return 0
    }

    // adjustmentBlock이 10의 배수가 아니면 기존 난이도 그대로 사용
    if (_newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVEL !== 0) {
      return _adjustmentBlock.difficulty;
    }

    // 위 두가지 if문 에서 안걸렸을 때,
    const takenTime: number = _newBlock.timestamp - _adjustmentBlock.timestamp;

    // 6000
    const expectedTime: number =
      UNIT * BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVEL;

    // 실제 생성 시간이 기대시간의 절반 이하일 때, 난이도 UP
    if (takenTime < expectedTime / 2) {
      return _adjustmentBlock.difficulty + 1;
    }

    // 실제 생성시간이 기대시간의 2배 이상일 때, 난이도 down
    if (takenTime > expectedTime * 2) {
      return _adjustmentBlock.difficulty - 1;
    }

    return 0;
  }

 

이렇게 해주면 된다.

 

4. mining 구현

이제 다왔다.


mining만 구현하면 된다.


채굴이란 말은 많이 들어 봤다.
뭔가 어려울 거 같고, 복잡할 거 같지만,
코드로는 어렵지않다.


그냥 nonce 값을 구하면 된다.

로직은 이렇다.

 

블록에서 hash와, data를 제외한 나머지 값들로 hash를 만든다.
해당 hash를 2진수로 변환한다.


2진수로 변환된 숫자가 몇개의 0으로 시작하는지 확인한다.
0의 갯수가 difficulty 보다 큰지 확인한다.
difficulty 보다 크면 통과!

 

difficulty 보다 작으면 nonce 값을 1 증가 시킨다.


그리고 이걸 반복한다.

 

이걸 이제 코드로 구현 하기만 하면된다.

그리고 그 코드가 findBlock 함수에 작성된다.

 

static findBlock(_generated: Block) {
    let nonce: number = 0;
    let hash: string;
    while (true) {
      nonce++;
      _generated.nonce = nonce;
      hash = Block.createHash(_generated);
      const binary: string = hexToBinary(hash);
      const result = binary.startsWith('0'.repeat(_generated.difficulty));

      if (result) {
        _generated.hash = hash;
        console.log(_generated);
        return _generated;
      }
    }
  }

 

이렇게 작성해주면 끝!

mining이 라는게 굉장히 어려울줄 알았는데 mining 까지 가는게 어렵지 mining 자체는 그닥 어렵지 않았던것 같다.

 

참고를 위해 Block class 와 Chain class 의 전체 코드를 올린다.

 

Chain.ts

export class Chain {
  blockchain: Block[];
  constructor() {
    this.blockchain = [Block.getGENESIS()];
  }

  getChain(): Block[] {
    return this.blockchain;
  }

  getLength(): number {
    return this.blockchain.length;
  }

  getLatestBlock(): Block {
    const length = this.getLength();
    return this.blockchain[length - 1];
  }

  addBlock(data: string[]): Failable<Block, string> {
    const previousBlock: Block = this.getLatestBlock();
    const adjustmentBlock: Block = this.getAdjustmentBlock();
    const newBlock: Block = Block.generateBlock(
      previousBlock,
      data,
      adjustmentBlock,
    );

    const isValid: Failable<Block, string> = Block.isValidNewBlock(
      newBlock,
      previousBlock,
    );

    if (isValid.isError) {
      return { isError: true, error: isValid.error };
    }

    this.blockchain.push(newBlock);
    return { isError: false, value: newBlock };
  }

  getAdjustmentBlock(): Block {
    const curLength = this.getLength();
    if (curLength < DIFFICULTY_ADJUSTMENT_INTERVEL) {
      return GENESIS;
    } else if (curLength % 10 === 0) {
      return this.blockchain[curLength - DIFFICULTY_ADJUSTMENT_INTERVEL];
    } else {
      return this.getLatestBlock();
    }
  }
}

 

Block.ts

export class Block extends BlockHeader implements IBlock {
  public hash: string;
  public merkleRoot: string;
  public data: string[];
  nonce: number;
  difficulty: number;

  constructor(_previousBlock: Block, _data: string[], _adjustmentBlock: Block) {
    super(_previousBlock);
    this.merkleRoot = Block.getMerkleRoot(_data);
    this.hash = Block.createHash(this);
    this.nonce = 0;
    this.difficulty = Block.getDifficulty(
      this,
      _adjustmentBlock,
      _previousBlock,
    );
    this.data = _data;
  }

  static getGENESIS(): Block {
    return GENESIS;
  }

  static getMerkleRoot(_data: string[]): string {
    const merkleTree = merkle('sha256').sync(_data);
    return merkleTree.root() || '0'.repeat(64);
  }

  static createHash(_newBlock: Block): string {
    const {
      version,
      timestamp,
      merkleRoot,
      previousHash,
      height,
      difficulty,
      nonce,
    } = _newBlock;
    const values: string =
      version +
      timestamp +
      merkleRoot +
      previousHash +
      height +
      difficulty +
      nonce;

    return SHA256(values).toString();
  }

  // 블럭 검증 코드
  static isValidNewBlock(
    _newBlock: Block,
    _previousBlock: Block,
  ): Failable<Block, string> {
    /* 
    1. 이전 블록 높이 +1 === 새로생긴 블록 높이 check
    2. 이전블록.해시 === 새로생긴블록.이전해시 check
    3. 새로생긴 블록의 해시를 새로 만듬 === 새로생긴블록.해시 check
     */

    if (_previousBlock.height + 1 !== _newBlock.height) {
      return { isError: true, error: '블록 높이가 맞지 않습니다.' };
    }

    if (_previousBlock.hash !== _newBlock.previousHash) {
      return { isError: true, error: '이전 해시값이 맞지않습니다.' };
    }

    if (Block.createHash(_newBlock) !== _newBlock.hash) {
      return { isError: true, error: '블록해시가 올바르지 않습니다.' };
    }

    return { isError: false, value: _newBlock };
  }

  static generateBlock(
    _previousBlock: Block,
    _data: string[],
    _adjustmentBlock: Block,
  ) {
    const generated = new Block(_previousBlock, _data, _adjustmentBlock);
    const newBlock = Block.findBlock(generated);
    return newBlock;
  }

  static findBlock(_generated: Block) {
    let nonce: number = 0;
    let hash: string;
    while (true) {
      nonce++;
      _generated.nonce = nonce;
      hash = Block.createHash(_generated);
      const binary: string = hexToBinary(hash);
      const result = binary.startsWith('0'.repeat(_generated.difficulty));

      if (result) {
        _generated.hash = hash;
        console.log(_generated);
        return _generated;
      }
    }
  }

  static getDifficulty(
    _newBlock: Block,
    _adjustmentBlock: Block,
  ): number {

    // adjustmentBlock 가 GENESIS 일 때,
    if (_adjustmentBlock.height === 0){
      return 0
    }

    // adjustmentBlock이 10의 배수가 아니면 기존 난이도 그대로 사용
    if (_newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVEL !== 0) {
      return _adjustmentBlock.difficulty;
    }

    // 위 두가지 if문 에서 안걸렸을 때,
    const takenTime: number = _newBlock.timestamp - _adjustmentBlock.timestamp;

    // 6000
    const expectedTime: number =
      UNIT * BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVEL;

    // 실제 생성 시간이 기대시간의 절반 이하일 때, 난이도 UP
    if (takenTime < expectedTime / 2) {
      return _adjustmentBlock.difficulty + 1;
    }

    // 실제 생성시간이 기대시간의 2배 이상일 때, 난이도 down
    if (takenTime > expectedTime * 2) {
      return _adjustmentBlock.difficulty - 1;
    }

    return 0;
  }
728x90
728x90

댓글