1. TypeScript로 블록만들기
(1) OOP(Object Oriented Programming)
OOP는 객체지향적인 방법으로 코드를 작성하는 프로그램의 설계 방법론중 하나다.
수많은 객체 단위를 만들어 서로 상호작용하며 동작하는 방식이다.
OOP에서 객체는 하나의 역할을 수행하는 함수와 변수들의 묶음 데이터로 보면 된다.
이러한 객체지향 프로그래밍은 프로그램을 만들때 제일 작은 단위부터 만들어가는 방식을 선호한다.
하지만 작은 단위부터 작성하면 테스트가 어렵다는 단점이 있고 이를 극복하기 위해 TDD를 사용한다.
(2) TDD(Test Driven Development)
작은 단위들을 테스트 해보며 개발하기 위해 사용하는 기법이다.
TDD기법으로 하기 위해 Jest,babel 라이브러리를 사용한다.
npm i -D ts-jest @types/jest babel-core @babel/preset-typescript @babel/preset-env
2. typeScript 환경설정
(1) ts-node 설치
npm i -D typescript ts-node @types/node
기존 자바스크립트는 터미널에서 node 파일명.js로 실행킬 수 있었다.
하지만 node는 .ts의 파일을 읽지 못한다. 그래서 터미널에서 실행시키기 위해 ts-node를 설치한다.
(2) tsconfig.json 설치
tsc --init
TypeScript의 기본적인 환경설정을 위해 tsconfig.json 을 설치해준다.
(3) paths 설치
tsconfig.json 에서 paths 속성을 사용하여 경로를 지정해줄것이기 때문에 paths를 설치한다.
npm i -D tsconfig-paths
(4) 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/ 경로의 모든 파일
"@core/*": ["src/core/*"],
"*": ["@types/*"]
}
}
}
(5) babel.config.js
최상단에 babel.config.js 파일을 만들어주고 설정해준다.
module.exports = {
preset: [
[
"babel/preset-env",
{
targets: { node: "current" },
},
],
"@babel/preset-typescript",
],
};
(6) jest.config.ts
import type { Config } from "@jest/types";
const config: Config.InitialOptions = {
moduleFileExtensions: ["ts", "js"],
//테스트 코드를 실행할 파일 이름
testMatch: ["<rootDir>/**/*.test.(js|ts)"],
moduleNameMapper: {
// 경로의 별칭 작성
"^@core/(.*)$": "<rootDir>/src/core/$1",
},
testEnvironment: "node",
verbose: true, //터미널에서 테스트 확인 할지
preset: "ts-jest",
};
export default config;
(7) crypto-js, merkle
블록을 만들기 위해 필요한 해싱작업을 해줄 라이브러리를 설치한다
npm i crypto-js merkle
다음 이 두 라이브러리의 타입을 가져올수 있는 모듈을 설치한다.
npm i --save-dev @types/crypto-js @types/merkle
3. 폴더구조
tsconfig.json에서 paths를 설정하여 src/core/ 의 경로를 @core/로 사용할수 있게 됐다.
4. 타입 설정
(1) 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[];
}
블록 생성을 하는 클래스를 만들때 블록헤더 부분을 만들어주는 클래스를 구분해서 따로 만들고 상속받아왔다.
nonce, difficulty 속성들은 차후에 채굴 난이도와 마이닝 부분을 구현할때 사용할 속성이다.
(2) Failable.d.ts
declare type Result<R> = { isError: false; value: R };
declare type Faillure<E> = { isError: true; value: E };
declare type Failable<R, E> = Result<R> | Faillure<E>;
- TypeScript 제네릭
제네릭이란 타입을 함수의 파라미터처럼 사용하는 것을 말한다.
<R> 과 같이 타입이 아닌 파라미터를 넣어준다.
선언시점이 아니라 생성 시점에 타입을 명시해서 하나의 타입이 아닌 다양한 타입을 사용할수 있는 기법이다.
제네릭을 안쓰면 타입을 미리 지정하거나 any를 사용하면 되는데 타입을 미리 지정한다는건 정해진 타입을 써야하고 any를 사용하면 자료타입을 제한할수 없고 어떤 데이터 타입이 반환되는지 알수 없다.
- 제네릭의 특징
한번의 선언으로 다양한 타임에 재사용이 가능하다.
제네릭을 사용하지 않으면 불필요한 타입 변환을 하기 때문에 프로그램의 성능 향상에 도움이 된다.
5. 블록만들기
(1) src/core/config.js
여기서 제네시스 블록(첫블록)을 만들어 준다.
보통 최초 블록은 하드코딩으로 직접 블록을 만들어준다.
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",
],
};
(2) src/core/blockChain/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;
}
//static 메소드를 사용하면 인스턴스에 메소드가 포함되지 않아서 인스턴스 생성마다 메소드 생성되는 비효율적성을 방지 가능
// 클래스 내에서 함수를 만들어 사용하고 싶을때 static 메소드를 주로 활용함
public static getVersion() {
return "1.0.0";
}
public static getTimeStamp() {
return new Date().getTime();
}
}
- extends 와 implements 차이
extends : 클래스를 상속받을때 사용한다. 부모클래스에서 정의하면 자식클래스에서 그대로 사용할수 있다.
implements : 상속받을때 사용한다. 부모클래스에서 정의했어도 자식클래스에서 한번더 정의를 해줘야 한다.
(3) src/core/blockChain/block.ts
import { SHA256 } from "crypto-js";
import merkle from "merkle";
import { BlockHeader } from "./blockHeader";
import { GENESIS } from "@core/config";
// 부모 속성 가져오고 인터페이스 형태 클래스 만듬
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사용
super(_previousBlock);
this.merkleRoot = Block.getMerkleRoot(_data);
this.hash = Block.createBlockHash(this);
this.nonce = 0;
this.difficulty = 0;
this.data = _data;
}
// 최초 블록 가져오는 함수
public static getGENESIS(): Block {
return GENESIS;
}
//블록추가
public static generateBlock(_previousBlock: Block, _data: string[]): Block {
const generateBlock = new Block(_previousBlock, _data);
return generateBlock;
}
// 머클루트 반환 함수
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> {
//블록의 높이가 이전 블록보다 1이 증가된 상태인지 체크하는 식
if (_previousBlock.height + 1 !== _newBlock.height)
return { isError: true, value: "블록 높이 오류" };
// 블록의 이전 블록 해시 값이 새로운 블록의 이전 블록 해시값과 같은지
if (_previousBlock.hash !== _newBlock.previousHash)
return { isError: true, value: "이전 해시 오류" };
// 생성된 블록의 정보를 가지고 다시 해싱해서 생성된 블록의 해시값과 같은지 비교
if (Block.createBlockHash(_newBlock) !== _newBlock.hash)
return { isError: true, value: "블록 해시 오류" };
return { isError: false, value: _newBlock };
}
}
위 파일에서 블록을 생성해주는 클래스를 만든다.
(4) src/core/blockChain/block.test.ts
// 작성한 코드들을 테스트해보기 위해서 만든 파일
import { Block } from "@core/blockChain/block";
import { GENESIS } from "@core/config";
describe("Block 검증", () => {
let newBlock: Block;
it("블록 추가", () => {
const data: string[] = ["Block 2"];
newBlock = Block.generateBlock(GENESIS, data);
console.log(newBlock);
});
it("블록 검증", () => {
const isValidBlock = Block.isValidNewBlock(newBlock, GENESIS);
if (isValidBlock.isError) {
console.error(isValidBlock.isError);
return expect(true).toBe(false);
}
expect(isValidBlock.isError).toBe(false);
});
});
- describe() : describe함수를 사용해서 테스트한다. 연관 테스트 함수끼리 그룹핑 시켜주는 역할이다.
- it() : 테스트할 코드의 최소 단위 공간
- expect(결과값).toBe(예상값)
expect().toBe()는 테스트 코드 사용시 자주 사용하고 우리가 원하는 결과값이 나오는 경우에만 테스트를 성공한것으로 간주할때 사용한다.
예상한 결과가 나오지 않으면 테스트 실패로 반환 가능하다.
'개발 > BlockChain' 카테고리의 다른 글
[BlockChain] TypeScript로 지갑 만들기 (0) | 2022.11.11 |
---|---|
[BlockChain] TypeScript로 P2P 구현 (0) | 2022.11.07 |
[BlockChain] TypeScript로 체인 만들기 (0) | 2022.11.03 |
[블록체인] 자바스크립트로 블록 만들기 (0) | 2022.10.31 |
[블록체인] 비트코인 (0) | 2022.10.31 |