728x90
1. 파일구조
📂 상위 폴더
│
├── 📂 node_modules
│
├── 📂 src
│ │
│ ├── 📂 core
│ │ ├── 📂 __test__
│ │ │ └── 📄 block.test.ts
│ │ │
│ │ ├── 📂 block
│ │ │ ├── 📄 block.ts
│ │ │ └── 📄 blockHeader.ts
│ │ │
│ │ ├── 📂 crypto
│ │ │ └── 📄 crypto.module.ts
│ │ │
│ │ └── 📂 interface
│ │ ├── 📄 block.interface.ts
│ │ └── 📄 faiable.interface.ts
│ │
│ └── 📄 config.ts
│
├── 📄 index.md
├── 📄 jest.config.ts
├── 📄 package-lock.json
├── 📄 package.json
└── 📄 tsconfig.json
2. 초기 환경 설정 및 필수 라이브러리 설치
npm init -y
npm i -D typescript
npm i -D ts-node
- ts-node : TypesScript 코드를 Node.js 환경에서 직접 실행하기 위한 도구
✔️ 암호화 및 머클 트리 라이브러리 설치
npm i -D @types/merkle
npm i -D @types/crypto-js
npm i -D merkle
- merkle : 머클 트리 생성 및 관련 로직을 처리하는 라이브러리
- @types/merkle : TypeScript 에서 merkle 라이브러리를 사용하기 위한 타입 정의
- @types/crypto-js : TypeScript 에서 crypto-js 라이브러리를 사용하기 위한 타입 정의
✔️ TypeScript 설정 및 별칭 처리
# tsconfig-paths : ts-node로 node 환경에서 실행을 할때 우리가 정해준 별칭을 경로로 변환해서 실행시키기 위해 사용
npm i -D tsc-alias tsconfig-paths
- tsc-alias : TypeScript 설정의 별칭과 경로를 해석하여 JavaScript 로 컴파일한다
# ts config 생성 : 컴파일 옵션 작성
npx tsc --init
- npx tsc --init : TypeScript 프로젝트의 기본설정을 위한 tsconfig.json 파일을 생성한다
✔️ 테스트 환경 설정
npm i -D te-jest
npm i -D @types/jest jest
npm i -D ts-jest
npm run test
- jest : JavaScript 및 TypeScript 코드를 위한 테스팅 프레임워크다
- @types/jest : TypeScript 에서 jest 를 사용하기 위한 타입 정의
- ts-jest : TypeScript 코드를 jest 로 테스트하기위한 도구이다
3. Block 생성
1. 인터페이스 먼저 생성해준다
- 블록 및 관련 구성 요소의 구조와 속성을 정의한다
⌨️ 1) block.interface.ts
export interface IBlockHeader {
version: string; // 블록의 버전을 나타내는 문자열이다
height: number; // 블록체인 내에서 블록의 위치를 나타내는 숫자이다
timestamp: number; // 블록이 생성된 시간을 나타내는 타임스탬프이다
previousHash: string; // 이전 블록의 해시 값을 나타내는 문자열이다
}
export interface IBlock extends IBlockHeader {
merkleRoot: string; // 블록 내의 트랜잭션들을 요약하여 나타내는 Merkle Tree 의 루트 해시값이다
hash: string; // 블록의 해시 값을 나타냄
nonce: number; // Proof-of-Work 알고리즘에서 사용되는 무작위 숫자
difficulty: number; // 블록을 생성하기 위한 난이도를 나타내는 숫자
data: string[]; // 블록에 포함된 트랜잭션 데이터를 나타내는 문자열 배열
}
⌨️ 1-2) faiable.interface.ts
export interface Result<R> {
isError: false; // 오류 발생 여부를 나타내는 부울 값. Result 타입의 경우 항상 false
value: R; // 결과 값을 나타내는 타입 R
}
export interface Failure<E> {
isError: true;
value: E; // 오류 값을 나타내는 타입 E
}
export type Failable<R, E> = Result<R> | Failure<E>;
- Failable :
Result 또는 Failure 중 하나의 상태를 가질 수 있는 유니온 타입(합집합 타입)
타입의 변수가 성공적인 결과 값을 가질 수도 있고, 실패한 오류 값을 가질 수도 있다는 것을 나타낸다
2. 환경설정 (tsconfig)
- TypeScript 프로젝트의 설정을 정의하고 필요한 라이브러리 및 모듈을 설치한다
⌨️ 2) tsconfig.ts
{
"compilerOptions": {
"module": "CommonJS",
"outDir": "./dist",
"target": "ES6",
"esModuleInterop": true,
"baseUrl": ".",
"paths": {
// baseUrl 경로 부터 별칭 사용
"@core/*": ["src/core/*"]
}
},
"ts-node": {
"require": ["tsconfig-paths/register"]
}
}
3. 암호화 모듈 준비 (crypto.module)
- 블록의 해시 생성, 디지털 서명 검증 등의 암호화 관련 기능을 구현한다
⌨️3) crypto.module.ts
// 16진수에서 해시 문자열을 2진수 문자열로 변환하는 기능을 갖는 CryptoModule 클래스 정의
class CryptoModule {
// 문자열 타입의 해시 값을 입력으로 받아 이진수 문자열을 반환
static hashToBinary(hash: string): string {
// binary 라는 이름의 빈 문자열을 초기화, 이 변수에 최종적으로 변환된 2진수 문자열이 저장됨
let binary: string = "";
// 16 진수를 -> 2진수로 바꾸는 식
// 문자열을 2글자씩 처리하기 위한 반복문
for (let i = 0; i < hash.length; i += 2) {
// 현재의 i 위치에서 2글자를 추출하여 hexByte 에 저장
const hexByte = hash.substr(i, 2);
// hexByte를 16진수로 해석하여 10진수 값으로 변환
const dec = parseInt(hexByte, 16);
// 10진수숫자 dec 를 2진수 문자열로 변환하고, 결과 문자열이 8자리가 되도록 앞쪽을 0으로 패딩한다
const binaryByte = dec.toString(2).padStart(8, "0");
// 변환된 2진수 바이트를 binary 문자열에 추가한다
binary += binaryByte;
}
// 변환된 전체 2진수 문자열 binary 를 반환한다
return binary;
}
}
export default CryptoModule;
- static : 객체 지향 프로그래밍에서 클래스 레벨의 멤버 (메서드, 속성 등)를 정의하는데 사용
클래스의 인스턴스가 아닌 클래스 자체에 속함
// static 예시
class MyClass {
static staticProperty = "I am a static property";
static staticMethod() {
return "I am a static method";
}
}
// static 멤버 사용
console.log(MyClass.staticProperty); // "I am a static property"
console.log(MyClass.staticMethod()); // "I am a static method"
위의 예에서 staticProperty 와 staticMethod 는 Myclass 의 static 멤버로, Myclass 의 인스턴스를 만들지 않고도 접근할 수 있다
4. BlockHeader 구현
- 블록 헤더의 구체적인 구조와 로직을 정의한다
⌨️4) blockHeader.ts
import { IBlock, IBlockHeader } from "@core/interface/block.interface";
class BlockHeader implements IBlockHeader {
version: string;
height: number;
timestamp: number;
previousHash: string;
constructor(_previousBlock: IBlock) {
// 블록을 "생성"할때 이전 블록의 정보가 필요하다
// 이전 블록의 해시나 높이나
this.version = BlockHeader.getVersion();
this.timestamp = BlockHeader.getTimesstamp(); // BlockHeader 클래스의 getTimestamp 메서드로 : timestamp 속성을 현재시간으로 설정
this.height = _previousBlock.height + 1; // 이전 블록의 높이에 1을 더해 현재 블록 높이 설정
this.previousHash = _previousBlock.hash;
}
// 정적메서드 getVersion : 이 메서드는 항상 "1.0.0"을 반환하고 이는 블록 헤더의 버전을 나타냄
static getVersion() {
return "1.0.0";
}
// 정적메서드 getTimestamp 를 선언, 이 메서드는 현재 시간의 타임스탬프를 밀리초 단위로 반환한다
static getTimesstamp() {
return new Date().getTime();
}
}
export default BlockHeader;
5. Block 구현
- 블록의 구체적인 로직과 기능을 구현한다
새로운 트랜잭션을 블록에 추가하고, 블록의 해시를 생성한다
⌨️ 5) block.ts
import { SHA256 } from "crypto-js";
import merkle from "merkle"; // merkle 라이브러리를 가져와 머클 트리를 생성하는데 사용
import BlockHeader from "./blockHeader";
import { IBlock } from "@core/interface/block.interface";
import { Failable } from "@core/interface/faiable.interface"; // 연산의 결과나 실패 정보를 담는 유니언 타입
import CryptoModule from "@core/crypto/crypto.module"; // 암호화 관련 메서드
// Block 클래스는 BlockHeader 클래스를 상속하고 IBlock 인터페이스를 구현한다
class Block extends BlockHeader implements IBlock {
hash: string;
merkleRoot: string; // 블록 내 데이터의 머클 루트를 나타낸다
nonce: number; // 마이닝 과정에서 사용되는 값으로, 블록의 해시를 채굴 난이도에 맞게 조정하는데 사용
difficulty: number; // 블록 채굴의 난이도를 나타낸다
data: string[]; // 블록에 포함된 데이터를 나타낸다
// Block 클래스의 생성자를 선언한다 이 생성자는 이전블록과 데이터배열을 매개변수로 받는다
constructor(_previousBlock: Block, _data: string[]) {
super(_previousBlock); // BlockHeader 의 생성자를 호출하여 이전 블록 정보로 초기화한다
this.merkleRoot = Block.getMerkleRoot(_
data); // 데이터 배열을 사용하여 머클루트를 계산하고 그 값을 merkleRoot 속성에 할당함
// 블록 본인의 데이터를 해시화 한게 블록의 해시값
this.hash = Block.createBlockHash(this);
// 블록 채굴은 뒤에 추가
// 지금은 0으로
this.nonce = 0;
// 지금은 난이도 3
this.difficulty = 3;
this.data = _data;
}
// 생성자에서는 이전 블록과 데이터를 인자로 받아, 현재 블록을 초기화한다
static generateBlock(_previousBlock: Block, _data: string[]): Block {
const generateBlock = new Block(_previousBlock, _data);
// 마이닝을 통해서 블록의 생성 권한을 받은 블록을 만들고 findBlock 메서드를 사용하여 블록 채굴
const newBlock = Block.findBlock(generateBlock);
// 채굴된 블록을 반환
return newBlock;
}
// cf. 원래 cpu 를 중심으로 한 작업인데 gpu 연산이 좀 더 빨라서 그래픽카드 많이 이용함
// 마이닝 과정을 통해 적절한 해시 값을 찾는 정적 메서드
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진수 변환해야하는데
// 16진수를 2진수로 변환해서 0의 갯수가 난이도의 갯수에 충족하는지 체크해서
// 맞추면 블록 채굴의 권한을 받고
// 블록을 생성할 수 있다
const binary: string = CryptoModule.hashToBinary(hash);
console.log("binary :", binary);
// 연산의 값이 난이도에 충족했는지 체크할 변수
// 난이도 3일떄 : "000"
// binary.startsWith("0".repeat(generateBlock.difficulty)) 얘가 "000" 이 문자열로 시작하는지 결과가 true false 반환되고
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();
}
// 머클루트 구하는 함수
// <T> : 함수로 사용하면서 타입을 바꿀수있음
// 데이터 배열을 받아 머클루트 값을 계산하는 정적메서드
// <T> : 제네릭 타입이다 제네릭은 타입스크립트에서 타입을 매개변수화하여 코드의 재사용성을 높이는 방법이다 여기서 'T'는 어떤 타입이든 될 수 있는데, 실제로 메서드를 호출할 때 정해진다
// (_data : T[]) : 메서드의 매개변수이다 어떤 타입의 배열이든 받을 수 있음을 의미
// : string : 메서드의 반환 타입을 나타낸다. 이 메서드는 문자열 값을 반환한다
static getMerkleRoot<T>(_data: T[]): string {
const merkleTree = merkle("sha256").sync(_data);
return merkleTree.root();
}
// 새로운 블록이 유효한지 검사하는 정적 메서드
static isValidNewBlock(
_newBlock: Block,
_previousBlock: Block
): Failable<Block, string> {
// 블록의 유효성 검사를 하는데
// 블록의 높이가 정상적인지
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 };
}
}
export default Block;
- 마이닝 : 블록을 채굴할때 블록의 연산을 통해서 난이도의 값에 따른 정답을 찾는 동작
- findBlock : 동작의 이름은 마이닝 블록을 채굴하는 동작
- POW : 작업 증명 블록의 난이도에 충족하는 값을 구하기 위해서 연산작업을 계속 진행해서 조건에 충족하는 값을 구하면
보상으로 블록의 생성 권한을 얻는다 - startsWith : 문자열의 시작이 매개변수로 전달된 문자열로 시작하는지 체크
6. Config(제네시스 블록) 설정
- 블록체인의 첫 번째 블록, 제네시스 블록을 정의한다
⌨️ 6) config.ts
// 제네시스(최초) 블록
// 최초블록은 하드 코딩
import { IBlock } from "./interface/block.interface";
export const GENESIS: IBlock = {
merkleRoot: "0".repeat(64),
version: "1.0.0",
height: 0,
timestamp: new Date().getTime(),
hash: "0".repeat(64),
previousHash: "0".repeat(64),
// 블록을 채굴할때 이전 블록 난이도로 마이닝을 한다
// 블록의 생성 주기 검사를 해서 생성주기가 빠르면
// 블록의 난이도를 상승 시키고
// 블록의 생성주기가 느리면 블록의 난이도를 하락 시킨다
difficulty: 0,
nonce: 0,
data: [
"The Times 03/Jan/2009 Chancellor on brink of second bailout for banks",
],
};
7. 테스트 설정 (jest.config)
- 블록과 관련된 테스트의 설정을 정의한다
⌨️ 7) jest.config.ts
import type { Config } from "@jest/types";
const config: Config.InitialOptions = {
// 1. 모듈 파일 확장자 설정 : typescript 와 javascript 둘다 테스트 파일로 지정
moduleFileExtensions: ["ts", "js"],
// 2. 테스파일 매치 설정 : 파일의 이름의 패턴을 설정
// 루트 경로에서 모든 폴더(**)에 모든 파일(*) 이름의 패턴이 test.js or test.ts
testMatch: ["<rootDir>/**/*.test.(js|ts)"],
// 3. 모듈의 이름에 대한 별칭 설정 : @core
// 별칭으로 지정된 @core를 어떻게 경로를 바꿔줄거냐
// ^@core == @core/**/* 로 시작하는 별칭은 루트경로에 src/core의 경로까지
moduleNameMapper: {
"^@core/(.*)$": "<rootDir>/src/core/$1",
},
// 4. 테스트 환경 설정 : node 환경에서 실행 시킬거임
testEnvironment: "node",
// 5. 자세한 로그 설정 출력 : 터미널에 로그들을 더 자세히 출력할지 여부
verbose: true,
// 6. 프리셋 설정 : typescript 에서 사용할 jest / ts-jest 로 설정
preset: "ts-jest",
};
export default config;
8. package 설정
- 프로젝트의 의존성과 스크립트를 정의한다
⌨️ 8) package.json
"scripts": {
"test": "jest"
},
9. Block Test 실행
- 블록의 기능과 로직을 테스트한다
⌨️ 9) block.test.ts
// 테스트 코드를 작성하면 시간이 오래걸리긴 하지만
// 코드의 품질을 좀 더 올릴수 있다
// 단위별로 테스트를 진행해서 디버깅을 하고 코드를 작성할수 있기 때문에
// 1단계 코드를 실행하고 2단계 코드를 실행하고 절차적으로 테스트를 우리가 진행 해볼수 있다
import Block from "@core/block/block";
import { GENESIS } from "@core/config";
// describe : 테스트들의 그룹화 그룹을 지정할 수 있다
// 첫번째 매개변수 : 그룹의 명, 어떤 테스트 그룹인지
// 두번째 매개변수 : 테스트들을 실행시키는 콜백 함수
// describe("block 테스트 코드 그룹", () => {
// // 테스트들의 단위를 어떻게 작성하냐
// // 하나의 테스트 단위 / 첫번째 매개변수에는 테스트 이름 명
// // 두번째 매개변수는 테스트의 동작을 가지고 있는 콜백 함수
// it("제네시스 블록 테스트", () => {
// console.log(GENESIS);
// });
// it("오류 테스트", ()=>{
// console.log(GENESIS);
// })
// });
// describe : 테스트 코드의 그룹 단위
describe("block 검증", () => {
let newBlock: Block;
let newBlock2: Block;
it("블록추가", () => {
const data = ["Block 1"];
newBlock = Block.generateBlock(GENESIS, data);
// 블록의 난이도에 따른 마이닝을 동작해서
// 조건에 맞을때까지 연산을 반복한뒤에 생성된 블록을 newBlock 에 받아온다
// 이전 블록은 GENESIS(최초 블록)
console.log("newBlock :", newBlock);
const data2 = ["Block 2"];
newBlock2 = Block.generateBlock(newBlock, data2);
console.log("newBlock2 :", newBlock2);
});
it("블록 유효성 검증", () => {
const isValidNewBlock = Block.isValidNewBlock(newBlock, GENESIS);
if (isValidNewBlock.isError) {
// expect : toBe : 값이 맞는지 확인할때
// 성공한 결과가 맞는지 확인할때 사용하는 코드
// true false 비교해서 맞는지 확인
return expect(true).toBe(false);
}
expect(isValidNewBlock.isError).toBe(false);
});
});
728x90
'BlockChain' 카테고리의 다른 글
[BlockChain] 난이도 조정 알고리즘 코드 추가 (0) | 2023.09.12 |
---|---|
[BlockChain] 마이닝(Mining) / 블록채굴 난이도 이해하기 (0) | 2023.09.09 |
[Block Chain] TypeScript 로 체인 만들기 (1) | 2023.09.06 |
[BlockChain] 머클트리(Merkle Tree)란 (0) | 2023.09.05 |
[BlockChain] 블록체인(Blockchain) 비트코인 개념 이해하기 (0) | 2023.09.04 |