이전 포스팅에서는 JavaScript를 이용해서 블록을 만들어 보았다. 이번 포스팅에서는 TypeScript를 이용해 블록을 만들어 보고자 한다. 단지 TypeScript를 사용해서 만든다는 점만 다를 뿐, 블록을 만드는 방식과 개념은 동일하다.
참고)
2022.06.08 - [BlockChain] - BlockChain - JavaScript로 블록 만들기
< 목차 >
- 환경 설정
- 타입 정의 파일
- 블록 헤더 만들기
- 블록 만들기
1. 환경 설정
우선 TypeScript를 사용해 블록을 만들기 위한 환경 설정을 진행해 주도록 하자.
(1) 각자의 프로젝트 디렉토리 안에서 TypeScript 설치하기.
npm install -D typescript ts-node @types/node
(2) tsconfig.json 파일 생성하기.
tsconfig.json 파일을 생성하여 TypeScript 설정 값을 세팅해주면 된다.
{
"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/*"]
}
}
}
tsconfig.json에서 paths 옵션을 사용하고 있기 때문에 아래의 패키지를 추가로 설치해주도록 하자.
npm install -D tsconfig-paths
(3) 디렉토리 구조.
디렉토리 구조는 다음과 같다.
@types/ 디렉토리는 블록의 타입 정의 파일들로 구성되어 있고 src/core/blockchain 디렉토리 안에서 블록 생성 관련 파일을 작성하려 한다.
(4) 필요한 모듈 설치.
블록을 생성할 때 머클루트 와 해시 값이 필요하게 된다. crypto-js 모듈과 merkle 모듈을 사용하여 머클루트와 해시값을 쉽게 구하고자 한다.
npm install crypto-js
npm install merkle
TypeScript에서 외부 모듈을 사용할 경우, 타입 정의 파일이 필요하게 된다. 다행히도 crypto-js 와 merkle 모듈의 경우 타입 정의 파일을 가져올 수 있는 모듈이 존재한다.
npm i --save-dev @types/crypto-js
npm i --save-dev @types/merkle
2. 타입 정의 파일
@types/ 디렉토리 안에 Block.d.ts 파일을 생성하여 블록 생성 클래스의 interface를 미리 작성해 보도록 하자.
// 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[];
}
블록을 생성하는 클래스를 만들 때 블록 헤더 부분을 만들어주는 클래스를 구분해서 따로 만들 예정이기 때문에 IBlockHeader 라는 interface와 Block 이라는 interface를 만들어 주었다. 참고로 interface Block 안에 nonce 속성과 difficulty 속성이 들어가 있는데 해당 부분은 채굴 난이도와 마이닝 부분을 구현할 때 사용하게 되는 속성이다. 본 포스팅에서는 사용하지 않을 예정이다.
// 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>;
Failable.d.ts 파일에서 위와 같이 타입을 선언해주었다. 해당 타입들은 추후에 블록이 생성될 때 올바르게 생성된 블록인지 검증하기 위한 용도로 사용된다. TypeScript의 generic을 사용하여 Result 타입 혹은 Failure 타입을 가질 수 있는 Failable 타입을 만들어 준 것이다.
3. 블록 헤더 만들기
블록 생성 클래스를 만들어주기 전에 블록 헤더를 만들어주는 클래스를 먼저 만들어보도록 하자.
// blockHeader.ts 파일
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();
}
}
class BlockHeader implements IBlockHeader { } 부분을 조금 살펴보도록 하겠다.
우선 @types/ 디렉토리 안에 있는 Block.d.ts 파일에서 declare 를 사용해 interface를 선언했기 때문에 전역에서 IBlockHeader 타입을 가져와서 사용할 수 있다. 그리고 implements는 TypeScript 에서 클래스의 타입을 정의할 때 사용되는 문법이다. 클래스에서 사용되는 extends 와 implements 의 차이에 대해 간략히 알아보자.
- extends : 클래스를 상속받고 싶을 때 사용한다. 클래스를 정의할 때 extends를 사용해 부모 클래스를 상속받게 되면 자식 클래스는 부모 클래스의 속성과 메소드를 가져오게 된다. ( 자식 클래스에서 추가적으로 정의해줄 필요가 없다. )
- implements : 특정 클래스 혹은 미리 정해놓은 interface 와 똑같은 형태로 클래스를 정의하고 싶을 때 사용한다. ( extends의 상속과는 다른 개념이다. )
추가로 static 메소드의 경우 인스턴스가 아닌 클래스 자체의 메소드가 되기 때문에 this.getVersion() , this.getTimestamp() 와 같은 형태가 아닌 BlockHeader.getVersion() , BlockHeader.getTimestamp()와 같은 형태로 사용된다. static 메소드를 사용할 경우 인스턴스에 해당 메소드가 포함되지 않기 때문에 인스턴스를 생성할 때마다 메소드가 생성되는 비효율성을 방지할 수 있다. 클래스 내에서 함수를 만들어 사용하고 싶을 때 주로 static 메소드를 활용한다.
class BlockHeader 에 들어가는 속성들에 대한 설명은 이전 포스팅 "JavaScript로 블록 만들기"에서 다뤘으므로 생략하도록 하겠다.
4. 블록 만들기
이제 블록 헤더를 이용해서 블록 생성 클래스를 만들어 보자.
// block.ts 파일
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 };
}
}
export class Block extends BlockHeader implements IBlock { } 부분을 살펴보면 Block 클래스가 BlockHeader 클래스를 상속 받고 interface IBlock 형태로 만들어진다는 것을 알 수 있다.
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;
}
그리고 constructor( ) { } 함수 안에서 super( ) 함수를 사용하였는데 해당 함수를 통해 BlockHeader 클래스의 속성과 메소드를 상속받을 수 있다. BlockHeader 클래스에서 constructor( ) 함수의 인자값으로 IBlock 타입의 객체를 받고 있기 때문에 super( ) 함수의 인자값으로 Block 타입의 객체를 넣어주면 된다.
public static getMerkleRoot<T>(_data: T[]): string {
const merkleTree = merkle("sha256").sync(_data);
return merkleTree.root();
}
블록 속성 중 하나인 머클루트는 static 메소드 getMerkleRoot( )를 통해 만들어주었다. 머클루트 값은 생성될 블록에 들어갈 데이터를 이용해서 만들게 되는데 이 때 TypeScript의 generic을 사용해 인자값으로 받는 배열 안에 담긴 데이터들의 타입이 유동성 있게 변할 수 있도록 했다. TypeScript의 generic을 사용할 경우 타입 선언 시점이 아닌 변수를 생성하는 시점에서 타입을 명시할 수 있기 때문에 다양하고 유동성 있게 타입을 지정할 수 있다.
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();
}
createBlockHash( ) 함수에서는 구조 분해 할당문을 이용해 Block 타입의 객체에서 블록 해시를 만드는데 필요한 값들만을 가져왔다. version , timestamp , height , merkleRoot , previousHash 값을 이용해 새로 생성될 블록의 해시값을 만들어주면 된다.
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 };
}
마지막으로 isValidNewBlock( ) 함수를 살펴보자. Block 클래스에 의해 새로운 블록이 생성되었다면 해당 블록은 검증 과정을 거쳐야만 한다. 알맞게 생성된 블록인지에 대한 검증 과정을 거친 후에야 블록 체인에 연결될 수 있다. 기본적으로 검증해야 할 사항들은 다음과 같다.
- 생성된 블록의 높이(height)가 이전 블록보다 +1 증가되었는지 체크
- 생성된 블록의 previousHash 값이 이전 블록의 hash 값과 같은지 체크
- 생성된 블록의 정보들을 이용해 hash 값을 새로 만들어보고 해당 hash 값과 생성된 블록의 hash값이 일치하는지 다시 한번 체크
이 때 isValidBlock( ) 함수의 return 타입을 Failable<Block, string> 으로 명시한 것을 볼 수 있는데 앞서 타입 정의 파일을 만들 때 Failable.d.ts 파일 안에서 선언했던 타입이다. Failable 타입은 generic으로 선언되었기 때문에 Failable<Block, string>은 { isError: false, value: Block } 혹은 { isError: true, error: string } 형태의 타입을 가질 수 있다.
여기까지의 내용을 토대로 블록을 생성하기 위한 기본적인 골격은 어느정도 갖추어졌다. 하지만 지금까지의 코드만으로 블록 체인을 만들기에는 부족함이 있다. 우선 Block 클래스 와 BlockHeader 클래스를 이용해 블록을 만들어주고 있는데 두 클래스 모두 constructor( ) 함수의 인자값으로 이전 블록 (_previousBlock)이 들어가고 있다. 즉, 위의 코드만으로는 첫번째 블록을 생성하는 것이 불가능하다. 그리고 테스트 코드를 작성해서 우리가 작성한 코드들이 제대로 동작하는지에 대한 검증을 해볼 필요성이 있다. 다음 포스팅에서 제네시스 블록(첫번째 블록)을 만들어보고 테스트 코드를 작성해서 지금까지 작성한 코드들에 대한 검증을 진행해보도록 하겠다.
다음 글)
2022.06.11 - [BlockChain] - BlockChain - TypeScript로 블록체인 만들기 (2) (feat. Jest)
'BlockChain' 카테고리의 다른 글
BlockChain - 블록체인 P2P 네트워크 만들기 (1) (1) | 2022.06.14 |
---|---|
BlockChain - TypeScript로 블록체인 만들기 (4) PoW (0) | 2022.06.14 |
BlockChain - TypeScript로 블록체인 만들기 (3) (0) | 2022.06.12 |
BlockChain - TypeScript로 블록체인 만들기 (2) (feat. Jest) (1) | 2022.06.11 |
BlockChain - JavaScript로 블록 만들기 (0) | 2022.06.08 |