본문 바로가기
Blockchain

[블록체인 만들기 #5] PoW 합의 알고리즘

by 개발자 염상진 2022. 10. 11.

비트코인 블록체인에서는 PoW 합의 알고리즘을 통해 새로운 블록을 생성한 채굴노드에게 비트코인으로 보상을 지급하게 됩니다. PoW는 분산 합의 알고리즘의 한 종류로,  쉽게 말해 지정된 Nonce 값 보다 작은 값을 찾아내는 작업에 성공한 노드를 선정하는 일입니다.

 

 

 

블록체인에서 합의 알고리즘이 필요한 이유는 네트워크에 참여한 모든 노드들이 동일한 상태를 가질 수 있도록 올바른 블록만 연결해야 하기 때문입니다. 악의적인 노드가 올바르지 않은 블록을 추가하려고 할 때 이를 필터링 해줄 수 있어야 합니다.

또한 합의 알고리즘을 통해 동시에 2개의 블록이 생성되지 않도록 사전 예방하는 작업이기도 합니다. 비트코인에 사용되는 작업증명 합의 알고리즘은 Difficulty를 조정함으로써 10분에 1블록이 생성되도록 유도하고 있습니다. 

현재 까지 PoW 합의 알고리즘은 가장 검증이 많이 된 알고리즘으로 많이 사용되고 있습니다. 최근 ESG가 비즈니스 환경에서 중요해지면서 환경을 파괴한다는 문제점이 꾸준히 지적되고 있지만 아직까지는 가장 파워풀한 알고리즘으로 꼽히고 있습니다.

 

PoW 설계

 

PoW(합의 알고리즘) 설계

  • 블록 1개가 생성되는 예상 시간(BLOCK_GENERATION_INTERVAL)을 10분으로 설정해줍니다. 
  • 10개의 블록을 한 묶음(DIFFICULTY_ADJUSTMENT_INTERVAL)로 지정합니다.
  • 1개의 블록 묶음이 생성되는 예상 시간을 6000초(10분 * 10개)를 할당합니다.
  • 10의 블록이 생성되는데 걸리는 시간(timeTaken)이 timeExpected/2보다 작은 경우 Difficulty += 1
  • 10의 블록이 생성되는데 걸리는 시간(timeTaken)이 timeExpected *2보다 큰 경우 Difficulty -= 1

 

src/core/config.ts 파일에 변수 3가지를 추가합니다.

/**
 *  난이도 조정 블록 묶음 갯수
 */
 export const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10;
 
 /**
  *  블록 생성 시간 (단위 : 10분)
  */
 export const BLOCK_GENERATION_INTERVAL: number = 10;
  
 /**
  *  블록 생성 시간 단위 (초)
  */
 export const BLOCK_GENERATION_TIME_UNIT: number = 60;

export const GENESIS: IBlock = {
    version: "1.0.0",
    height: 0,
    timestamp: new Date().getTime(),
    hash: "0".repeat(64),
    previousHash: "0".repeat(64),
    merkleRoot: "0".repeat(64),
    difficulty: 0,
    nonce: 0,
    data: [
      "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks",
    ],
  };

 

Block Class 수정

 

src/core/blockchain/block.ts 파일을 수정합니다.

// src/cor/blockchain/block.ts

import { SHA256 } from "crypto-js";
import merkle from "merkle";
import { BlockHeader } from "./blockHeader";

// 추가1 : 제네시스 블록
import {
  DIFFICULTY_ADJUSTMENT_INTERVAL,
  BLOCK_GENERATION_INTERVAL,
  BLOCK_GENERATION_TIME_UNIT,
  GENESIS,
} from "@core/config";

import hexToBinary from "hex-to-binary";

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

  constructor(_previousBlock: Block, _data: string[], _adjustmentBlock: Block) {
    super(_previousBlock);

    const merkleRoot = Block.getMerkleRoot(_data);

    this.merkleRoot = merkleRoot;
    this.hash = Block.createBlockHash(this);
    this.nonce = 0;
    this.difficulty = Block.getDifficulty(
      this,
      _adjustmentBlock,
      _previousBlock
    );
    this.data = _data;
  }

  // 추가2 : 제네시스 블록
  public static getGENESIS(): Block {
    return GENESIS;
  }

  public static getMerkleRoot<T>(_data: T[]): string {
    const merkleTree = merkle("sha256").sync(_data);
    return merkleTree.root();
  }

  public static createBlockHash(_block: Block): string {
    const {
      version,
      timestamp,
      height,
      merkleRoot,
      previousHash,
      difficulty,
      nonce,
    } = _block;
    const values: string = `${version}${timestamp}${height}${merkleRoot}${previousHash}${difficulty}${nonce}`;
    return SHA256(values).toString();
  }

  // 추가3 : 블록 생성 메소드
  public static generateBlock(
    _previousBlock: Block,
    _data: string[],
    _adjustmentBlock: Block
  ): Block {
    const generateBlock = new Block(_previousBlock, _data, _adjustmentBlock);
    const newBlock = Block.findBlock(generateBlock);
    return generateBlock;
  }

  public static findBlock(_generateBlock: Block) {
    let hash: string;
    let nonce: number = 0;

    while (true) {
      nonce++;
      _generateBlock.nonce = nonce;
      hash = Block.createBlockHash(_generateBlock);

      // 16진수 > 2진수
      const binary: string = hexToBinary(hash);
      // difficulty는 0의 개수
      const result: boolean = binary.startsWith(
        "0".repeat(_generateBlock.difficulty)
      );

      if (result) {
        _generateBlock.hash = hash;
        return _generateBlock;
      }
    }
  }

  public static getDifficulty(
    _newBlock: Block,
    _adjustmentBlock: Block,
    _previousBlock: Block
  ): number {
    if (_newBlock.height <= 9) return 0;
    else if (_newBlock.height <= 19) return 1;

    if (_newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVAL !== 0)
      return _previousBlock.difficulty;

    /**
     * 블록이 생성되는 시간은 개당 10분
     */
    const timeTaken: number = _newBlock.timestamp - _adjustmentBlock.timestamp;
    /**
     * 블록 묶음이 생성되는 시간 6000초
     */
    const timeExpected: number =
      BLOCK_GENERATION_TIME_UNIT *
      BLOCK_GENERATION_INTERVAL *
      DIFFICULTY_ADJUSTMENT_INTERVAL;

    if(timeTaken < timeExpected / 2) return _adjustmentBlock.difficulty + 1;
    else if(timeTaken > timeExpected * 2) return _adjustmentBlock.difficulty - 1;
    return _adjustmentBlock.difficulty;
  }

  public static isValidNewBlock(
    _newBlock: Block,
    _previousBlock: Block
  ): Failable<Block, string> {
    if (_previousBlock.height + 1 !== _newBlock.height)
      return { isError: true, error: "height error" };
    if (_previousBlock.hash !== _newBlock.previousHash)
      return { isError: true, error: "previousHash error" };
    if (Block.createBlockHash(_newBlock) !== _newBlock.hash)
      return { isError: true, error: "block hash error" };

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

 

 

BLOCK INTERVAL 변수 추가

BLOCK GENERATION INTERVAL 관련 변수를 import 합니다.

// 추가1 : 제네시스 블록
import {
  DIFFICULTY_ADJUSTMENT_INTERVAL,
  BLOCK_GENERATION_INTERVAL,
  BLOCK_GENERATION_TIME_UNIT,
  GENESIS,
} from "@core/config";

import hexToBinary from "hex-to-binary";

 

hex-to-binary Install

nonce값을 확인하기 위해 16진수를 2진수로 변경하는 hex-to-binary를 추가합니다.

hex-to-binary를 설치합니다.

$ npm i hex-to-binary

@types 디렉토리에 hex-to-binary 정의파일을 생성합니다.

// @types/hex-to-binary.d.ts

declare module "hex-to-binary";

 

생성자 함수 adjustment 추가

constructor(_previousBlock: Block, _data: string[], _adjustmentBlock: Block) {
    super(_previousBlock);

    const merkleRoot = Block.getMerkleRoot(_data);

    this.merkleRoot = merkleRoot;
    this.hash = Block.createBlockHash(this);
    this.nonce = 0;
    this.difficulty = Block.getDifficulty(
      this,
      _adjustmentBlock,
      _previousBlock
    );
    this.data = _data;
  }
  • 난이도를 계산하기 위해 생성자 함수 매개변수에 adjustmentBlock을 추가해줍니다.
  • difficulty는 static 메소드로 구현된 getDifficulty 결과값을 저장합니다. 

 

createBlockHash() 메소드 수정

public static createBlockHash(_block: Block): string {
    const {
      version,
      timestamp,
      height,
      merkleRoot,
      previousHash,
      difficulty,
      nonce,
    } = _block;
    const values: string = `${version}${timestamp}${height}${merkleRoot}${previousHash}${difficulty}${nonce}`;
    return SHA256(values).toString();
  }
  • Block의 해시값을 반환하는 createBlockHash 매개변수에 difficulty와 nonce를 추가합니다.
  • string으로 변환하는 로직에도 difficulty와 nonce를 추가합니다.
  • 이제 difficulty와 nonce를 포함한 Block Hash를 반환하게 됩니다.

 

 

generateBlock() 수정

public static generateBlock(
    _previousBlock: Block,
    _data: string[],
    _adjustmentBlock: Block
  ): Block {
    const generateBlock = new Block(_previousBlock, _data, _adjustmentBlock);
    const newBlock = Block.findBlock(generateBlock);
    return newBlock;
  }
  • Block을 생성하는 메소드 매개변수에 adjustmentBlock을 추가합니다.
  • Block을 바로 생성하지 않고 findBlock() 메소드를 거쳐 생성됩니다.
  • nonce 값을 찾는 PoW 합의 알고리즘을 구현하는 소스코드입니다.

 

findBlock() 메소드 생성

public static findBlock(_generateBlock: Block) {
    let hash: string;
    let nonce: number = 0;

    while (true) {
      nonce++;
      _generateBlock.nonce = nonce;
      hash = Block.createBlockHash(_generateBlock);

      // 16진수 > 2진수
      const binary: string = hexToBinary(hash);
      // difficulty는 0의 개수
      const result: boolean = binary.startsWith(
        "0".repeat(_generateBlock.difficulty)
      );

      if (result) {
        _generateBlock.hash = hash;
        return _generateBlock;
      }
    }
  }
  • 로컬 변수로 hash와 nonce를 초기화 합니다.
  • nonce값을 증가시키면서 새로운 해시를 구합니다.
  • hash값을 2진수로 바꾸고 특정 nonce 조건을 통과하는지 확인합니다.(Difficulty는 0의 개수입니다.)
  • 만약 설정된 Difficulty를 만족하는 결과가 나오면 generateBlock의 해시값을 반환합니다.
  • 반환되는 _generateBlock은 Blockchain에 연결됩니다.

 

getDifficulty() 함수 생성

public static getDifficulty(
    _newBlock: Block,
    _adjustmentBlock: Block,
    _previousBlock: Block
  ): number {
    if (_newBlock.height <= 9) return 0;
    else if (_newBlock.height <= 19) return 1;

    if (_newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVAL !== 0)
      return _previousBlock.difficulty;

    /**
     * 블록이 생성되는 시간은 개당 10분
     */
    const timeTaken: number = _newBlock.timestamp - _adjustmentBlock.timestamp;
    /**
     * 블록 묶음이 생성되는 시간 6000초
     */
    const timeExpected: number =
      BLOCK_GENERATION_TIME_UNIT *
      BLOCK_GENERATION_INTERVAL *
      DIFFICULTY_ADJUSTMENT_INTERVAL;

    if(timeTaken < timeExpected / 2) return _adjustmentBlock.difficulty + 1;
    else if(timeTaken > timeExpected * 2) return _adjustmentBlock.difficulty - 1;
    return _adjustmentBlock.difficulty;
  }
  • 생성자 함수에서 getDifficulty() 메소드를 통해 난이도를 초기화합니다.
  • 블록 묶음(10개)이 생성되는 시간이 예상시간/2 보다 작으면(빨리 생성되면) 난이도를 1 증가 시킵니다.
  • 블록 묶음(10개)이 생성되는 시간이 예상시간*2보다 크면(느리게 생성되면) 난이도를 1 감소 시킵니다.

 

Chain Class 수정

 

Diffculty를 계산하기 위해 src/core/blockchain/Chain.ts를 수정합니다.

import { Block } from "@core/blockchain/block";
import { DIFFICULTY_ADJUSTMENT_INTERVAL } from "@core/config";

export class Chain {
  private blockchain: Block[];

  constructor() {
    this.blockchain = [Block.getGENESIS()];
  }

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

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

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

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

    // 이전 블록 묶음 구하기
    const adjustmentBlock: Block = this.getAdjustmentBlock();

    const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock);
    const isValid = Block.isValidNewBlock(newBlock, previousBlock);

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

    this.blockchain.push(newBlock);

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

  /**
   * getAdjustmentBlock()
   * 생성 시점 기준 이전 블록 묶음을 구한다
   * 1) 현재 높이 < DIFFICULTY_ADJUSTMENT_INTERVAL => GENESIS Block
   * 2) 현재 높이 > DIFFICULTY_ADJUSTMENT_INTERVAL => -10 번째 블록 반환
   */
  public getAdjustmentBlock() {
    const currentLength = this.getLength();
    const adjustmentBlock: Block =
      this.getLength() < DIFFICULTY_ADJUSTMENT_INTERVAL
        ? Block.getGENESIS()
        : this.blockchain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL];
    return adjustmentBlock;
  }
}

 

 

addBlock() 수정

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

    // 이전 블록 묶음 구하기
    const adjustmentBlock: Block = this.getAdjustmentBlock();

    const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock);
    const isValid = Block.isValidNewBlock(newBlock, previousBlock);

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

    this.blockchain.push(newBlock);

    return { isError: false, value: newBlock };
  }
  • 이전 블록 묶음을 구하기 위해 adjustmentBlock 변수를 추가합니다.
  • generateBlock() 메소드 인자로 adjustmentBlock을 넘겨줍니다.

 

getAdjustmentBlock() 메소드 생성

public getAdjustmentBlock() {
    const currentLength = this.getLength();
    const adjustmentBlock: Block =
      this.getLength() < DIFFICULTY_ADJUSTMENT_INTERVAL
        ? Block.getGENESIS()
        : this.blockchain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL];
    return adjustmentBlock;
  }
  • 현재 블록 길이를 구합니다.
  • adjustmentBlock은 현재 길이에 따라서 GENESIS Block 혹은 -10번째 블록이 선택됩니다.

 

작업증명(PoW) 테스트 코드 작성

 

Diffculty를 만족하는 해시값을 가진 블록이 정상적으로 체인에 연결되는지 테스트를 진행합니다.

import {Chain} from "@core/blockchain/chain"

describe("🔨 Chain 테스트", ()=>{
    // Genesis Block 생성
    let node:Chain = new Chain()

    it("1) getChain() 테스트", ()=>{
        console.log(node.getChain());
    })

    it("2) getLength() 테스트", ()=>{
        console.log(node.getLength());
    })

    it("3) getLatestBlock() 테스트", ()=>{
        console.log(node.getLatestBlock());
    })

    it("4) addBlock() 테스트", ()=>{
        for(let i=1; i<=100; i++){
            node.addBlock([`Block #${i}`])
        }

        console.table(node.getChain());
    })
})

 

100개의 블록을 생성하는 테스트를 진행합니다.

아래 그림처럼 블록이 10개씩 생성될 때 마다 난이도가 계속 올라가는 것을 확인할 수 있습니다.

 

블록체인 만들기 정리

 

Block, BlockHeader를 생성하고 Chain에 연결하여 PoW 합의 알고리즘을 구현해보았습니다. PoW는 Block의 Hash값과 Difficulty를 비교해서 새로운 블록을 생성하게 됩니다. 

 

2008년 처음 등장한 비트코인의 Difficulty는 계속 증가하여 현재 31T(31조) 까지 증가하였습니다. 세부적으로 보면 난이도는 블록이 생성되는 시점에 따라 증감하는 것을 확인할 수 있습니다. 

 

이제 기본적인 블록체인 구성이 완료되었습니다. 실제 블록체인을 사용하기 위해서는 많은 노드들이 참여해서 블록을 생성하고 채굴에 대한 보상을 받고 코인을 전송할 수 있는 네트워크가 필요합니다.

 


🚀️ 도움이 되셨다면 구독좋아요 부탁드립니다 👍️

댓글