2023-09-05 20:36:09

BlockChain 구조 알아보기

실제는 훨씬 더 복잡한 구조를 가지겠지만 간단한 Genesis block을 만들고 chain으로 만들기 까지의 과정을 실습을 통해 알아보자

 

프로젝트 구조

 

 

설치 모듈 정보

// npm init -y : package.json 생성
// typescript : TypeScript 코드를 JavaScript로 컴파일
// ts-node : TypeScript를 Node 환경에서 직접 실행하는 도구
// @types/merkle : merkle 라이브러리에 대한 TypeScript 타입 정의
// merkle : Merkle tree를 생성하고 관리하는 JavaScript 라이브러리
// @types/crypto-js : crypto-js 라이브러리에 대한 TypeScript 타입 정의
// crypto-js : 다양한 암호화 알고리즘과 유틸리티를 제공하는 JavaScript 라이브러리
// tsc-alias : TypeScript에서 tsconfig.json에 정의된 경로 및 별칭 인식
// tsconfig-paths : tsconfig.json의 paths 옵션을 사용하여 모듈을 로드하는데 사용
// @types/jest : jest 프레임워크에 대한 TypeScript 타입 정의
// jest : TypeScript 및 JavaScript 테스팅 프레임 워크
// ts-jest : TypeScript코드를 jest에서 직접 테스트할 수 있게 해주는 프로세서
// npx tsc --init : tsconfig.json 파일 생성
npm init -y
npm install -D typescript 
npm install -D ts-node
npm install -D @types/merkle merkle
npm install -D @types/crypto-js crypto-js
npm install -D tsc-alias tsconfig-paths
npm install -D @types/jest jest
npm install -D ts-jest
npx tsc --init

 

block.test.ts

jest를 사용하여 테스트 환경 구축

// 테스트 코드를 작성하면 시간이 걸리기는 하지만 코드의 품질을 올릴 수 있음
// 단위 테스트를 진행하기 때문에 디버깅을 하고 코드를 작성 할 수 있음
// 단위 테스트기 때문에 절차적으로 테스트 진행

import Block from "@core/block/block";
import Chain from "@core/chain/chain";
import { GENESIS } from "@core/config";

// describe : 테스트 그룹 지정
// 첫번째 매개변수 : 테스트 그룹 명
// 두번째 매개변수 : 테스트들을 실행시키는 콜백 함수
// describe("block test code group", () => {
//   it("GENESIS BLOCK TEST", () => {
//     console.log(GENESIS);
//   });
// });

describe("block verify", () => {
  let newBlock: Block;
  let newChain: Chain;
  let newChain2: Chain;
  // 테스트할 코드의 최소 단위
  it("add block", () => {
    const data = ["Block 1"];
    newBlock = Block.generateBlock(GENESIS, data);
    // 블록의 난이도에 따른 마이닝을 동작
    // 조건에 맞을 때까지 연산을 반복한 뒤에 생성된 블록을 newBlock에 받아옴
    // 이전 블록은 GENESIS(최초 블록)
    console.log("newBlock : ", newBlock);
  });

  it("block validation", () => {
    console.log("newBlock : ", newBlock);
    const isValidNewBlock = Block.isValidNewBlock(newBlock, GENESIS);
    // expect : toBe의 값이 맞는지 확인
    // 결과가 성공한게 맞는지 확인
    if (isValidNewBlock.isError) return expect(true).toBe(false);
    expect(isValidNewBlock.isError).toBe(false);
  });

  // 블록 체인 추가
  it("Add Block chain", () => {
    newChain = new Chain();
    newChain.addToChain(newBlock);

    console.log(newChain.get());

    console.log(newChain.getBlockByHash(newBlock.hash));
  });

  //
  it("Longest Chain Rule", () => {
    newChain2 = new Chain();
    newChain2.replaceChain(newChain.get());
    console.log(newChain2);
  });

  // 블록 생성 주기를 계산해서 정해 놓은 생명 주기보다 빠른지 느린지 판단
  it("before 10 block or genesis block", () => {
    for (let i = 0; i < 20; i++) {
      let block = new Block(newChain.lastestBlock(), ["block"]);
      newChain.addToChain(block);
    }
    console.log(newChain.getAdjustmentBlock());
  });
});

 

block.ts

block을 만들고 채굴 과정 구현

import { SHA256 } from "crypto-js";
import merkle from "merkle";
import BlockHeader from "./blockheader";
import { IBlock } from "@core/interface/block.interface";
import { Failable } from "@core/interface/failable.interface";
import CryptoModule from "@core/crypto/crypto.module";

// block 형태를 클래스로 정의
// 이렇게 보면 직관적으로 이해가 쉬움
// class Block extends BlockHeader
// class Block implements IBlock
class Block extends BlockHeader implements IBlock {
  hash: string;
  merkleRoot: string;
  nonce: number;
  difficulty: number;
  data: string[];
  constructor(_previousBlock: Block, _data: string[]) {
    // 부모 클래스 생성자 호출
    super(_previousBlock);
    this.merkleRoot = Block.getMerkleRoot(_data);
    // 블록 본인의 데이터를 해시화. 블록의 해시값
    // createBlockHash는 정적(static) 메소드이기 때문에 this 키워드를 통해 인스턴스의 속성에 접근할 수 없기 때문에 블록의 내용을 처리하려면 Block객체를 인자로 전달 받아야함
    // this는 Block 클래스의 인스턴스를 참조하기 때문에 모든 속성을 가지고 있음
    this.hash = Block.createBlockHash(this);
    this.nonce = 0;
    this.difficulty = 3;
    this.data = _data;
  }

  // 블록 추가
  static generateBlock(_previousBlock: Block, _data: string[]): Block {
    const generateBlock = new Block(_previousBlock, _data);
    // 마이닝을 통해 블록의 생성 권한을 받은 블록을 만듬
    const newBlock = Block.findBlock(generateBlock);
    return newBlock;
  }

  // 마이닝 작업 코드
  // 블록 채굴
  // 연산을 통해 난이도의 값에 따른 정답을 찾는 동작
  // POW : 작업 증명 블록의 난이도에 충족하는 값을 구하기 위해서 연산작업을 계속 진행해서 조건에 충족하는 값을 구하면 보상으로 블록의 생성 권한을 얻음
  static findBlock(generateBlock: Block) {
    let hash: string;
    // nonce는 블록을 채굴하는데 연산 작업을 몇번 진행했는지 값을 담음
    let nonce: number = 0;

    while (true) {
      generateBlock.nonce = nonce;
      // nonce 값을 증가시켜 hash 값을 계속 바뀜
      nonce++;
      hash = Block.createBlockHash(generateBlock);

      // 16진수를 2진수로 변환
      // 2진 값이 바뀌는 이유 : 0의 개수가 난이도의 개수에 충족하는지 체크해서 맞추면 블록 채굴의 권한을 얻고 블록 생성
      const binary: string = CryptoModule.hashToBinary(hash);
      console.log("binary : ", binary);
      // 연산의 값이 난이도에 충족했는지 체크
      // startWith : 문자열의 시작이 매개변수로 전달된 문자열로 시작하는지 체크
      const result: boolean = binary.startsWith(
        "0".repeat(generateBlock.difficulty)
      );
      console.log("result : ", result);

      // 조건에 충족했으면 블록을 채굴 할 수 있는 권한 부여
      if (result) {
        // 연산을 통해 완성된 hash값
        generateBlock.hash = hash;
        // 완성된 블록을 내보냄
        return generateBlock;
      }
    }
  }

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

  // merkleRoot 반환값 구하기
  // merkle root는 merkle tree 최상단에 있는 노드의 해시 값
  // merkle tree는 데이터 블록을 효율적으로 저장하고 검증하기 위한 이진 트리 구조를 말함
  // 트랜잭션들을 대표하는 단일 값이 merkle root이기 때문에 중간에 데이터가 바뀌게 되면 merkle root 해시값이 바뀌게 되기 때문에 데이터 조작을 감지
  static getMerkleRoot<T>(_data: T[]) {
    const merkleTree = merkle("sha256").sync(_data);
    return merkleTree.root();
  }

  // 블록 유효성 검사
  static isValidNewBlock(
    _newBlock: Block,
    _previousBlock: Block
  ): Failable<Block, string> {
    // 블록의 높이가 정상인지 판단 (정상이라면 이전 블록보다 1이 증가)
    if (_previousBlock.height + 1 !== _newBlock.height)
      return { isError: true, value: "Previous height error" };

    // 이전 블록의 해시 값이 새로운 블록의 이전 해시값이 동일한지 확인
    if (_previousBlock.hash !== _newBlock.previoushash)
      return { isError: true, value: "Previous hash error" };

    // 생성된 블록의 정보가 변조되었는지 확인
    if (Block.createBlockHash(_newBlock) !== _newBlock.hash)
      return { isError: true, value: "block hash error" };

    // 블록 유효성 검사 통과
    return { isError: false, value: _newBlock };
  }
}

export default Block;

 

blockheader.ts

block의 header 인터페이스 정의

import { IBlock, IBlockHeader } from "@core/interface/block.interface";

class BlockHeader implements IBlockHeader {
  version: string;
  height: number;
  timestamp: number;
  previoushash: string;
  constructor(_previousBlock: IBlock) {
    console.log("_previousBlock : ", _previousBlock);
    this.version = BlockHeader.getVersion();
    this.timestamp = BlockHeader.getTimestamp();
    this.height = _previousBlock.height + 1;
    this.previoushash = _previousBlock.hash;
  }

  static getVersion() {
    return "1.0.0";
  }

  static getTimestamp() {
    return new Date().getTime();
  }
}

export default BlockHeader;

 

crypto.module.ts

16진수를 2진수로 변환

import Block from "@core/block/block";
import { GENESIS } from "@core/config";
import { Failable } from "@core/interface/failable.interface";

class Chain {
  private chain: Block[] = [GENESIS];
  private readonly INTERVAL = 10;

  // 현재 체인을 반환하는 함수
  get() {
    return this.chain;
  }

  // 길이를 반환하는 함수
  length() {
    return this.chain.length;
  }

  // 체인에 마지막 블록 반환 함수
  lastestBlock() {
    return this.chain[this.length() - 1];
  }

  // 블록 추가 메소드
  addToChain(receivedBlock: Block) {
    this.chain.push(receivedBlock);
    return this.lastestBlock();
  }

  // 블록 조회 메소드
  getBlock(callbackFn: (block: Block) => boolean) {
    const findBlock = this.chain.find(callbackFn);
    if (!findBlock) throw new Error("block not find");
    return findBlock;
  }

  // 블록의 높이로 블록을 조회
  getBlockByHeight(height: number) {
    return this.getBlock((block: Block) => block.height === height);
  }

  // 블록의 해시로 찾는 함수
  getBlockByHash(hash: string) {
    return this.getBlock((block: Block) => block.hash === hash);
  }

  // 현재 위치에서 10번째 블록들을 찾는 함수
  getAdjustBlock() {
    const { height } = this.lastestBlock();
    const findHeight =
      height < this.INTERVAL
        ? 1
        : Math.floor(height / this.INTERVAL) * this.INTERVAL;
    // 10번 블록의 높이 조회해서 반환
    return this.getBlockByHeight(findHeight);
  }

  // 다른 네트워크로 체인을 보냄
  serialze() {
    return JSON.stringify(this.chain);
  }

  // 다른 네트워크에서 체인을 받음
  deserialize(chunk: string) {
    return JSON.parse(chunk);
  }

  // 상대방 체인과 본인의 체인을 비교
  replaceChain(receivedChain: Block[]): Failable<undefined, string> {
    // 본인의 체인과 상대방의 체인을 검사하는 로직
    // 체인의 길이를 비교하는 로직 구현 (롱기스트 체인 룰)
    // 실제 네트워크에서는 더 복잡한 로직 사용

    // 상대방 체인의 마지막 블록
    const lastestReceivedBlock: Block = receivedChain[receivedChain.length - 1];

    // 본인의 마지막 블록
    const lastestBlock: Block = this.lastestBlock();

    // 상대방 체인의 마지막 블록과 본인의 마지막 블록의 길이가 같을 경우
    if (lastestReceivedBlock.height === 0)
      return {
        isError: true,
        value: "The last block of the other network chain is the first block",
      };

    // 상대방 체인의 마지막 블록이 본인의 마지막 블록의 길이보다 작거나 같을 경우
    if (lastestReceivedBlock.height <= lastestBlock.height)
      return {
        isError: true,
        value: "The last block of the other network chain is small or the same",
      };

    // 상대방의 체인이 본인의 체인보다 길면 본인의 체인을 업데이트
    this.chain = receivedChain;

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

  // 현재 블록 생성 시점에서 이전 10번째 블록 구하기
  // 현재 높이값 < 10 : 최초블록을 반환
  // 현재 높이 > 10 : -10번째 블록을 반환
  // 이전 10번째 블록의 생성 시간의 차이를 구하여 그 차이가 블록 생성 주기 보다 빠르면 난이도 증가
  // 생성 주기가 느리면 난이도 하락
  // 비트코인 기준으로 블록의 생성 시간은 10분
  getAdjustmentBlock() {
    const currentLength = this.length();
    const adjustmentBlock: Block =
      this.length() < this.INTERVAL
        ? GENESIS
        : this.chain[currentLength - this.INTERVAL];
    // 반환값은 최초블록 아니면 -10번째 블록
    return adjustmentBlock;
  }
}

export default Chain;

 

block.interface.ts

block의 header와 body에 대한 객체 구조 정의

// block 인터페이스를 정의
// 코드의 재사용성과 유지보수성을 높이기 위해 2개의 인터페이스를 사용
// 정의된 속성 중 하나를 변경하거나 추가되어도 한 곳만 수정 하면 나머지 한쪽도 변경되므로 유지 보수에 용이
export interface IBlockHeader {
  version: string;
  height: number;
  timestamp: number;
  previoushash: string;
}

// IBlock은 IBlockHeader 내용을 상속 받기 때문에 IBlockHeader의 속성들을 다시 정의 해줄 필요가 없음
export interface IBlock extends IBlockHeader {
  merkleRoot: string;
  hash: string;
  nonce: number;
  difficulty: number;
  data: string[];
}

 

failable.interface.ts

연산에 대한 성공과 실패 유무 반환

// 객체 구조 정의
// 연산에 성공했을때와 실패했을때의 반환값 객체 구조 정의

// Result<R>에서 R은 타입을 매개변수로 받겠다는 뜻
// R,E는 특별한 기능을 포함하고 있는 변수가 아닌 단순 매개변수의 이름이지만 개발자들끼리의 규칙같은게 있음
// T : Type의 약자로 제네릭에서 많이 사용되는 표준적인 매개변수 이름
// R : Result나 Return type을 의미
// E : Error나 Exception을 의미
export interface Result<R> {
  isError: false;
  value: R;
}

// 실패했을때 객체 구조 정의
export interface Faillure<E> {
  isError: true;
  value: E;
}

// 반환값 지정
// Failable<R, E> 의 결과값은 Result<R> | Faillure<E> 둘 중에 하나라는 뜻으로
// | 는 "또는" 이라는 의미를 가짐
export type Failable<R, E> = Result<R> | Faillure<E>;

 

config.ts

Genesis block 정의

// 제네시스 블록
// 최초 블록은 하드 코딩

import { IBlock } from "./interface/block.interface";

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: ["Hello, world"],
};

 

jest.config.ts

import type { Config } from "@jest/types";

const config: Config.InitialOptions = {
  // 모듈 파일 확장자 설정 : typescript와 javascript 둘다 테스트파일로 지정
  moduleFileExtensions: ["ts", "js"],

  // 테스트파일 매치 설정 : 파일의 이름의 패턴을 설정
  // 루트 경로에서 모든 폴더에 모든 파일 이름의 패턴
  testMatch: ["<rootDir>/**/*.test.(js|ts)"],

  // 모듈의 이름에 대한 별칭 설정 : @core
  //
  moduleNameMapper: {
    "^@core/(.*)$": "<rootDir>/src/core/$1",
  },
  // 테스트 환경 설정 : node 환경에서 실행
  testEnvironment: "node",
  // 자세한 로그 출력 설정 : 터미널에 로그들을 더 자세히 출력
  verbose: true,
  // 프리셋 설정 : typescript에서 사용할 jest / ts-jest 설정
  preset: "ts-jest",
};

export default config;

 

tsconfig.ts

{
  "compilerOptions": {
    "module": "CommonJS",
    "outDir": "./dist",
    "target": "ES6",
    "esModuleInterop": true,
    "baseUrl": ".",
    "paths": {
      // baseUrl 경로 부터 별칭 사용
      "@core/*": ["src/core/*"]
    }
  },
  "ts-node": {
    "require": ["tsconfig-paths/register"]
  }
}

 

block.test.ts

테스트 케이스를 모아 두는 곳

// 테스트 코드를 작성하면 시간이 걸리기는 하지만 코드의 품질을 올릴 수 있음
// 단위 테스트를 진행하기 때문에 디버깅을 하고 코드를 작성 할 수 있음
// 단위 테스트기 때문에 절차적으로 테스트 진행

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

// describe : 테스트 그룹 지정
// 첫번째 매개변수 : 테스트 그룹 명
// 두번째 매개변수 : 테스트들을 실행시키는 콜백 함수
describe("block verify", () => {
  let newBlock: Block;

  // 테스트할 코드의 최소 단위
  it("add block", () => {
    const data = ["Block 1"];
    newBlock = Block.generateBlock(GENESIS, data);
    // 블록의 난이도에 따른 마이닝을 동작
    // 조건에 맞을 떄까지 연산을 반복한 뒤에 생성된 블록을 newBlock에 받아옴
    // 이전 블록은 GENESIS(최초 블록)
    console.log("newBlock : ", newBlock);
  });

  it("block validation", () => {
    console.log("newBlock : ", newBlock);
    const isValidNewBlock = Block.isValidNewBlock(newBlock, GENESIS);
    // expect : toBe의 값이 맞는지 확인
    // 결과가 성공한게 맞는지 확인
    if (isValidNewBlock.isError) return expect(true).toBe(false);
    expect(isValidNewBlock.isError).toBe(false);
  });
});

 

실행 방법

package.json 부분 수정

  "scripts": {
    //"test": "echo \"Error: no test specified\" && exit 1"
    "test": "jest"
  },
npm run test

결과

테스트 블록이 제대로 생성되는 것을 확인 할 수 있다.

728x90