
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
'BlockChain' 카테고리의 다른 글
| [BlockChain] Wallet 구조 알아보기 (1) | 2023.09.22 |
|---|---|
| [BlockChain] 비트코인 구조 알아보기(2) (0) | 2023.09.21 |
| [BlockChain] BlockChain 알아보기 (0) | 2023.09.04 |