블록체인 기초 (3) 블록 검증과 체인, 마이닝의 개념

707·2022년 6월 14일
1

블록체인

목록 보기
4/10
post-thumbnail

생성한 블록 검증하기

이전 글에서 Block이라는 Class를 만들어 블록을 쉽게 찍어낼 수 있도록 만들었다. 여기서 꼭 필요한 작업이 하나 있다. 바로 블록이 과연 유효한 블록인지 확인해주는 작업이다.
왜 이런 과정이 필요한 것일까?

블록체인은 여러 사람이 경쟁적으로 생성해내는 데이터베이스 시스템이다. 내가 만든 블록이 네트워크에 연결될 수 있도록 주어진 문제를 풀고 이 과정에서 정답을 가장 먼저 맞춘 사람의 블록이 체인에 연결된다. 이 과정이 마이닝이다.
결국 다른사람의 블록을 내가 가지고 있는 체인에 붙여야하는데 이 때 전달받은 블록이 유효한 블록인지를 검사해주어야 데이터의 손상과 조작을 방지할 수 있다.

총 3가지를 확인하고 있다.
1. 새 블록이 이전 블록 높이보다 1만큼 높은지
2. 새 블록의 이전해시 속성값이 이전 블록의 해시값과 같은지
3. 블록헤더로 다시 해싱한 값이 블록의 해시값과 일치하는지 (JWT에서 페이로드로 시그니쳐를 다시 생성하여 조작된 토큰인지 확인한 것처럼)

코드는 아래와 같다.

public static isValidNewBlock(_newBlock: Block, _previousBlock: Block): Failable<Block, string> {
    // 1. 이전블럭 높이 + 1 === 새 블럭 높이 검증
    // 2. 이전블럭 해시 === 새 블럭 previousHash 검증
    // 3. _newBlock의 속성값으로 hash 새로 생성 후 생성한 해시가 _newBlock.hash와 같은지 검증
    if (_previousBlock.height + 1 !== _newBlock.height) {
      return { isError: true, error: '블록 높이가 맞지 않습니다.' };
    }
    if (_previousBlock.hash !== _newBlock.previousHash) {
      return { isError: true, error: '이전해시값이 맞지 않습니다.' };
    }
    if (Block.createNewHashThis(_newBlock) !== _newBlock.hash) {
      return { isError: true, error: '해시값이 맞지 않습니다.' };
    }
    return { isError: false, value: _newBlock };
  }


체인

체인(chain)이란 이전 블록의 해시(hash)가 다음 블록의 한 구성요소가 되는 방식이다. 다수의 트랜잭션을 블록으로 묶은 후 시간 순서에 따라 체인으로 엮은 것을 블록체인(blockchain)이라고 한다.

코드로 보자면 단순히 연계된 해시값을 가진 객체가 순서대로 배열안에 담긴 것이다.

블록을 생성하는 것을 Block 클래스로 했다면
블록을 체인에 더하거나 최신 블록을 가져오는 함수를 Chain 클래스에 만들어 두었다.

Block 클래스와의 차이점은 블록을 생성하는데 필요했던 메소드들은 static으로 클래스 자체에서 가져와 사용할 수 있는 반면,
Chain 클래스에서는 인스턴스를 생성해 그 인스턴스의 메소드로 호출하는 방식이다.

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

export class Chain {
  public 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(); // -10인 블록을 구함
    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 };
  }
  
  // ❗️ difficulty 계산용 블록 가지고 오는 함수
  public getAdjustmentBlock() {
 
    const currentLength = this.getLength();
    const adjustmentBlock: Block =
      this.getLength() < DIFFICULTY_ADJUSTMENT_INTERVAL
        ? Block.getGENESIS()
        : currentLength % 10 !== 0
        ? this.blockchain[currentLength - 1]
        : this.blockchain[this.getLength() - DIFFICULTY_ADJUSTMENT_INTERVAL];
    return adjustmentBlock; // 비교대상이 되는 블럭을 반환
  }
}

여기서 중요한 것이
블록 추가 함수와 adjustment block을 가져오는 함수이다.

1. addBlock

함수의 실행순서는 다음과 같다.

  1. 블록을 추가하고자 한다면 블록에 담을 data를 체인인스턴스의 addBlock메소드의 인자로 넣어준다

  2. Block클래스의 GenerateBlock 메소드에 다음을 인자로 전달한다.
    2-1. 이전블록
    2-2. 전달받은 data
    2-3. adjustmentBlock

  3. generateBlock 메소드에서 findBlock메소드를 실행한다. (블록 마이닝 과정)

  4. 마이닝 된 블록이 findBlock에서 리턴되면 해당 값을 다시 addBlock에 리턴한다.

  5. addBlock에서 마이닝이 완료된 블록의 유효성을 검증한다

  6. 유효성검사에서 에러가 나지 않으면 해당 블록을 체인 array에 담는다.

  7. addBlock 함수의 결과값으로 에러여부와 생성된 블록의 데이터를 리턴해준다.

여러 클래스의 메소드를 넘나들며 데이터가 이동하기때문에 전체적인 흐름을 한번 정리해두는 편이 좋을것같아 순서를 정리해보았다.

2. getAdjustmentBlock

마이닝을 할 때 난이도를 정하는 방식은 아래와 같다

  1. N개 단위의 블록묶음을 정한다.
  2. 해당 블록묶음의 평균 생성시간을 확인한다.
  3. 목표하는 시간 텀 (비트코인의 경우 10분마다 블록 하나가 생성된다)과 비교한다.
  4. 목표 시간과 일정한 오차범위 밖인 경우 난이도를 올리거나 내리며 조절한다.

그렇기때문에 N개 단위로 블록의 난이도가 바뀌게 된다. 이 블록묶음의 기준이 될 블록(만들어내는 블록의 N개 앞에 있는 블록)을 가지고 오는 것이 getAdjustmentBlock함수이다.

여기서 예외 처리를 해줘야 하는 부분이 두 개 있는데

  1. 체인의 length가 단위블록 수보다 작을 때. 아직 난이도를 계산할 수 없기때문에 genesis block의 난이도를 사용한다. adjustmentBlock으로 제네시스 블록을 리턴한다.
  2. 단위블록의 배수가 아닐때. 난이도는 단위블록의 배수일때만 계산이 되므로 (10번블록, 20번블록, ...) 그 외의 경우에는 직전 블록의 난이도를 사용한다. adjustmentBlock으로 previousBlock을 리턴한다.

이렇게 구한 adjustmentBlock을 addBlock의 인자값으로 넣어준다 (addBlock의 2-3)



마이닝

채굴(採掘) 또는 마이닝(mining)이란 암호화폐의 거래내역을 기록한 블록을 생성하고 그 대가로 암호화폐를 얻는 행위를 말한다. 1개의 암호화폐를 생성하기 위해서는 마치 금과 같은 광물을 캐는 것처럼 많은 시간과 노력이 필요한 일련의 작업이기에 채굴이라는 표현을 사용하기 시작했다.

기존의 코드대로라면 블록을 생성한다는 것은 사실 객체하나를 찍어내는 굉장히 쉬운 작업이다. 블록을 생성하는 것을 이렇게 쉽게 만들어두면 무분별하게 블록들이 쏟아져 나올것이고 여러 생성자간의 합의 역시 매우 어려워질 것이다.
그래서 이를 위해 어떤 사용자가 생성해낸 블록을 체인에 등록할 것인지를 정하는 합의알고리즘을 통해 조건을 만족시킨 블록만 체인에 등록이 된다.

이 합의 알고리즘의 종류는 매우 다양하다.
아래에서는 대표적인 비트코인의 마이닝 방식인 PoW(작업증명) 방식을 설명한다.

블록 마이닝 시에 필요한 아래의 두가지 메소드를 Block 클래스 안에 만들어준다.

1. 난이도 설정

위의 adjustment블록을 받아서 예외처리 (genesis block이거나 previous block인 경우)를 해주고
그 외의 경우일 때는 adjustment block의 timeStamp와 현재블록의 timeStamp를 비교해서 경과 시간을 파악하고
단위블록묶음에 해당하는 블록의 수로 시간을 나눠 하나의 블록이 생성되는데 소요되는 평균시간을 계산한다.

아래의 코드에서는 블록묶음의 평균생성시간이 목표하는 시간 (10분으로 설정)의 반인 5분보다 작으면 난이도를 1 올려주고, 목표하는 시간의 두배인 20분보다 오래 걸리면 난이도를 1 낮추는 식이다. 이 부분은 블록체인을 개발하는 사람의 취향껏 설정하면 되는 부분이니까 적당히 마음대로 짜면 될 듯?


Block.getDifficulty()

public static getDifficulty(
    _newBlock: Block,
    _adjustmentBlock: Block,
    _previousBlock: Block
  ): number {
    // 1. GENESIS BLOCK이 adjustment로 들어왔으면 난이도는 0
    if (_adjustmentBlock.height === 0) return 0;
  
    // 2. interval단위의 배수일떄만 난이도 변경 코드 실행. 
  	// 아니면 그냥 기존의 difficulty를 그대로 씀
    if (_newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVAL !== 1) {
      return _previousBlock.difficulty;
    }

  	// 3. 둘 다 해당 안되면 difficulty를 새로 계산한다. 
    const timeTaken: number = _newBlock.timestamp - _adjustmentBlock.timestamp;
    const timeExpected: number =
      UNIT_TIME * BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL; // 60s * 10m * blockInterval(10) === 6000

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


2. 마이닝

난이도가 정해지면 해당 난이도로 문제를 푼다.

문제 : 블록헤더로 해싱한 값의 앞자리에 0이 N(난이도)개 붙어야함

대략적으로 설명하면 위와 같다. 우리는 블록을 만들기 위해서는 해당 블록의 포인터역할을 하는 해시값을 무조건 생성해야 하는데 그냥 아무 해시값이면 되는 것이 아니라 0이 N개 붙어야 한다는 조건을 만족하는 해시값이어야 한다는 것이다. 생각보다 굉장히 단순한 문제이나 이 문제를 풀기 위해서는 0의 개수에 비례하는 연산을 계속 하면서 확인하는 작업이 필요하다.

이 과정에서 필요한 속성이 바로 nonce이다.
변경이 불가능한 다른 속성(height, merkleroot 등등..)과는 달리 이 값은 계속해서 수정이 가능하다.
해시는 인풋값이 하나 바뀌면 아예 다른 결과값이 도출된다. 이렇게 nonce값을 하나씩 올리면서 해싱과 결과확인을 반복하다가 정해진 0의 갯수를 충족하면 블록이 생성되어 리턴된다.

그래서 비트코인의 해시를 확인하면 다음과 같은 형태이다.

000000000000000000edf71cf65887e7fcc15c084c131a887013725298640eb3

앞에 0이 18개가 붙어있다. 이런 해시값을 얻기 위한 연산횟수는 거의 3*10^20번이라고 한다. (ㅎㅎ...)



Block.findBlock()

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

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

      const binary: string = hexToBinary(hash);
      const result: boolean = binary.startsWith(
        "0".repeat(_generateBlock.difficulty)
      );
      if (result) {
        _generateBlock.hash = hash;
        return _generateBlock;
      }
    }
  }


결론

채굴은 단순하게 값을 바꿔가면서 해싱하는 노가다 작업이다.
당연히 연산속도가 빠른 최신컴퓨터 1대와 20년 전 나온 데스크탑 100대가 있으면 무조건! 최신컴퓨터 1대가 모든 블록을 다 채굴해버리게 된다.
개인이 채굴을 한다는 건 불가능하다. 로또는 확률이라도 있지... 얘는 그냥 안되는 일이다. 물리법칙이다. 운 같은 것도 필요없다.

할꺼면 비트코인의 경우엔 점유율 1위인 마이닝풀에 가입해서 거기서 주는 기계 (무조건 여기서 주는 채굴기를 써야되고 전원공급기도 평범한걸로 안된다고함) 돌려가면서 채굴하는게 유일한 방법이다. 그 외에는 채굴가능성이 0인데 그냥 컴퓨터만 고생시키는 거다.



0개의 댓글