본문 바로가기
Blockchain

[블록체인 만들기 #1] Block 구성 (Typescript)

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

블록체인은 정보를 담은 여러개의 블록을 해시를 이용해 연결한 분산원장입니다. 네트워크에 참여한 노드들은 동일한 정보를 담은 블록들을 가지게 되고, 여러개의 블록들은 체인으로 연결되어 블록체인을 구성합니다. 이번 포스팅에서는 Typescript를 이용해 블록체인을 구성합니다.

 

 

Typescript 환경설정

 

환경구성

Typescript는 동적타입을 지원하는 javascript을 개량한 언어입니다. 정적 타입을 지원하며 컴파일 시 변수의 타입을 결정하게 됩니다. Typescript로 블록체인을 구성하기 위해 환경설정을 해줍니다. 

$ npm i -D typescript ts-node @types/node
  • typescript : typescript 언어를 사용합니다. -g 옵션으로 글로벌 설치를 할 수도 있지만 로컬로 설치해 package.json에 종속 모듈을 기재해줍니다.
  • ts-node : Typescript는 JS파일을 컴파일해서 사용해야 합니다. ts-node는 개발환경에서만 사용하며, TS 파일을 컴파일 없이 바로 실행해주는 모듈입니다.
  • @types/node : Typescript에서 JS 모듈을 사용하기 위해서는 Declarative 파일(.d.ts) 생성해줘야 합니다. node 모듈의 Declarative File들을 모두 선언해놓은 Library입니다.

 

 

 

tsconfig.json 파일

tsconfig.json은 Typescript을 이렇게 사용할 것이다라고 설정해주는 파일입니다. 자세한 내용은 Typescript 공식홈페이지에서 확인할 수 있습니다.

🔨 Typescript tsconfig.json 옵션 더 알아보기

 

{
  "exclude": ["node_modules"],
  "compilerOptions": {
    "outDir": "./build/",
    "esModuleInterop": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "strict": true,
    "target": "ES6",
    "removeComments": true,
    "lib": ["ES6"],
    "allowJs": true,
    "typeRoots": ["./node_modules/@types", "./@types"],
    "baseUrl": ".",
    "paths": {
      "@core/*": ["src/core/*"],
      "*": ["@types/*"]
    }
  }
}
  • exclude : include에 포함되지 않는 폴더/파일명을 지정합니다. 
  • compilerOptions : Typescript가 어떻게 작동해야하는지 명시합니다.
    • outDir : 컴파일된 JS 파일이 생성될 위치를 지정합e니다. 
    • esModuleInterop : ES6 모듈 사양을 준수하여 CommonJS를 import 합니다. 
    • moduleResolution : 모듈 해석 전략을 Node.js의 CommonJS로 해석합니다.
    • resolveJsonModule : 모듈을 JSON 형식으로 가져오도록 합니다.
    • strict  : Type checking의 범위를 확장합니다.
    • target : 컴파일 할 JS feautre을 특정합니다. 
    • removeComments : 컴파일 시 주석을 제거합니다. 
    • lib : 지정한 target에 맞는 Library를 선택합니다.
    • allowJs : JS로 작성된 파일을 TS에서 사용합니다.
    • typeRoots : @typs 파일의 위치를 지정합니다.
    • baseUrl : 프로젝트의 Base Directory를 지정합니다. 상대경로를 사용할 때 필요합니다.
    • paths : Module Import / Export 할 때 위치를 지정합니다. paths 옵션을 사용하기 위해서는 tsconfig-paths 모듈을 별도 설치해줘야 합니다.

$ npm i -D tsconfig-paths

 

crypto-js && merkle Install

 

블록에는 블록헤더를 해시한 값을 기재하게 되어있습니다. 해시를 구하기 위한 모듈로 crypto-js 모듈을 설치해줍니다.

트랜잭션을 해시한 값을 해시하면서 블록 헤더에 해시값이 기재되는데, 이 때 머클트리를 사용합니다. merkle 모듈을 설치해줍니다.

$ npm i -D crypto-js merkle

 

Typescript에서 Node 모듈을 사용하기 위해서는 Declarative Files이 필요합니다. crypto-js와 merkle의 정의파일도 함께 설치 해줍니다.

$ npm i -D @types/crypto-js @types/merkle

 

Declarative Files 작성

 

위에서 작성한 Block.d.tsFailable.d.ts 파일을 작성합니다.

Block Interface는 블록을 생성할 때 헤더 부분을 구성하는 역할을 합니다. Block Interface에 포함되어 있는 nonce와 difficulty는 블록을 생성할 때 요구되는 채굴 난이도를 결정합니다. 

// Block.d.ts 파일
 
declare interface IBlockHeader {
  version: string;
  height: number;
  timestamp: number;
  previousHash: string;
}
 
declare interface IBlock extends IBlockHeader {
  merkleRoot: string;
  hash: string;
  nonce: number;
  difficulty: number;
  data: string[];
}

 

 

Failable Interface는 블록이 올바르게 생성되었는지 검증하는 역할을 하게 됩니다. Result와 Failure는 Generic을 사용해 타입을 변수가 생성되는 시점에 결정하게 됩니다.

// Failable.d.ts 파일
 
declare type Result<R> = { isError: false; value: R };
 
declare type Failure<E> = { isError: true; error: E };
 
declare type Failable<R, E> = Result<R> | Failure<E>;

 

Block Header 만들기

 

블록 헤더를 만드는 클래스입니다. 블록헤더는 4개의 변수를 가집니다.

  • version
  • height
  • timestamp
  • previousHash
export class BlockHeader implements IBlockHeader {
    public version: string;
    public height: number;
    public timestamp: number;
    public previousHash: string;
 
    constructor(_previousBlock: IBlock) {
        this.version = BlockHeader.getVersion();
        this.timestamp = BlockHeader.getTimestamp();
        this.height = _previousBlock.height + 1;
        this.previousHash = _previousBlock.hash;
    }
 
    public static getVersion() {
        return '1.0.0';
    }
 
    public static getTimestamp() {
        return new Date().getTime();
    }
}

 

BlockHeader 클래스는 @types 디렉토리에 정의된 Block.d.ts 파일을 참조하는 IBlockHeader을 구현합니다. tsconfig.json에서 paths를 지정해줬기 때문에 따로 import를 하지 않아도 바로 사용할 수 있습니다. 

참고로 클래스를 상속받아 사용할 때는 extends 키워드를 사용하고, 클래스의 타입을 정의해놓은 Interface를 상속할 때는 implements 키워드를 사용합니다. 

BlockHeader 클래스를 보면 getVersion(), getTimestamp() 함수를 static으로 사용하고 있습니다. static으로 선언된 함수들은 인스턴스가 아닌 클래스 자체의 함수기 때문에 this.getVersion()이 아니라 BlockHeader.getVersion() 식으로 사용해줘야 합니다. static으로 선언된 함수는 인스턴스를 만들 때 포함되지 않습니다. 

 

Block 만들기

 

이제 본격적으로 블록을 생성합니다. Block에는 5개의 변수를 담습니다. Block 클래스는 BlockHeader와 IBlock을 상속합니다. 블록의 구조를 살펴보면 BlockHeader와 Block을 구성하는 이유를 이해할 수 있습니다. 

  • hash 
  • merkleRoot
  • nonce
  • difficulty
  • data

 

 

import { SHA256 } from "crypto-js";
import merkle from "merkle";
import { BlockHeader } from "./blockHeader";
 
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[]) {
    
    super(_previousBlock);
 
    const merkleRoot = Block.getMerkleRoot(_data);
 
    this.merkleRoot = merkleRoot;
    this.hash = Block.createBlockHash(this);
    this.nonce = 0;
    this.difficulty = 0;
    this.data = _data;
  }
 
  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 } = _block;
    const values: string = `${version}${timestamp}${height}${merkleRoot}${previousHash}`;
    return SHA256(values).toString();
  }
 
  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 클래스에서 관리하는 변수들을 클래스 변수로 선언해줍니다.

  public hash: string;
  public merkleRoot: string;
  public nonce: number;
  public difficulty: number;
  public data: string[];

 

생성자 함수

생성자 함수에서는 super 키워드를 사용해 상속한 BlockHeader의 클래스 속성과 메소드들을 사용할 수 있게 됩니다.

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

 

getMerkleRoot()

static 메소드로 getMerkleRoot() 메소드를 선언합니다. Generic을 사용해 매개변수의 타입을 다양하게 받을 수 있게 합니다. 머클루트는 SHA256 해시 알고리즘을 사용해 생성합니다. 

public static getMerkleRoot<T>(_data: T[]): string {
    const merkleTree = merkle("sha256").sync(_data);
    return merkleTree.root();
  }
💡 머클루트란?

 

createBlockHash()

블록해시를 생성하는 static 메소드를 선언합니다. 블록을 받아 블록헤더의 변수들을 해싱하여 반환합니다. 이 때 해시 알고리즘은 SHA256을 사용합니다. 

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

 

블록 검증 메소드

블록을 검증하는 static 메소드를 선언합니다. 새로운 Block과 이전 Block을 인자로 받아 블록의 깊이, 해시값을 비교해 올바른 블록이 생성되었는지 유효성 검사를 진행합니다. 

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 };
  }

 

 

 

블록체인 만들기 미완성

 

Typescript로 블록체인을 만들기 위한 기본적인 골격이 갖추어졌습니다. 환경구성을 하고, Block과 BlockHeader 클래스를 생성했습니다. 

하지만 미완성인 부분이 아직 남아있습니다. 

  1. Block과 BlockHeader 클래스에서 이전 블록을 인자로 전달받고 있습니다. 즉 GENESIS 블록을 생성할 수 없습니다.
  2. Unit Test 코드가 없습니다.
  3. 블록을 생성할 때 Nonce와 Difficulty가 작동하지 않습니다.

 

 

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

댓글