블록체인의 구조를 이해하려면 블록(Block)의 기본 구성 요소인 "헤더(Header)"와 "바디(Body)"를 살펴보아야 합니다. 블록체인은 여러 블록이 연결된 체인 구조를 가지고 있으며, 각 블록은 헤더와 바디로 구성됩니다.
Block
블록은 데이터를 저장하고 있는 단위로, 블록체인의 기본 구성 요소(객체)입니다. 각 블록은 이전 블록의 해시 값과 자신의 트랜잭션 데이터를 가지고 있습니다. 블록체인에서는 블록이 순차적으로 연결되어 있어서 블록 하나가 이전 블록의 정보를 참조하고 있는 형태를 가지고 있습니다.
Block Header
블록 헤더는 블록의 메타데이터를 포함하는 부분으로, 블록의 무결성과 신뢰성을 보장하고 보통
이전 블록의 해시 (Previous Block Hash), 타임스탬프 (Timestamp), 난이도 목표 (Difficulty Target), 논스 (Nonce) 로 구성되어 있다.
이전 블록의 해시 (Previous Block Hash): 이전 블록의 해시 값으로, 현재 블록이 이전 블록과 연결되도록 하는 역할을 합니다. 이를 통해 블록체인은 연속성을 유지하게 됩니다.
타임스탬프 (Timestamp): 블록이 생성된 시간을 나타냅니다. 이를 통해 블록체인 네트워크는 트랜잭션의 순서를 정확하게 유지 할 수 있습니다.
난이도 목표 (Difficulty Target): 채굴에 사용되는 난이도 목표를 나타냅니다. 난이도 목표는 채굴자가 블록을 생성하기 위해 해야 할 작업의 난이도를 결정합니다.
논스 (Nonce): 채굴 과정에서 블록 헤더의 해시 값을 만족시키기 위해 조정되는 값으로 , 채굴자가 찾아야하는 값입니다.
Block Body
블록 바디는 실제 트랜잭션 데이터를 포함하는 부분입니다. 블록 바디에는 블록에 기록된 모든 트랜잭션에 대한 정보가 포함되어 있습니다. 트랜잭션은 블록체인에 기록되기 위해 검증되고 블록에 추가됩니다.
블록 헤더와 바디의 조합으로 구성된 블록은 블록체인의 핵심을 이루며 , 분산 네트워크에서 중요한 역할을 합니다. 블록체인은 이러한 블록들이 연결되어 있어서 변경이 불가능하며, 안전하게 분산된 환경에서 트랜잭션을 관리하고 저장할 수 있는 구조를 제공합니다.
간단한 블록 코드 예제를 설명하자면
기본 경로에서 부터 설정한 별칭을 정하기 위해 tsconfig.json 파일에
를 추가하도록 하자
인터페이스(Interfaces)는 TypeScript에서 코드를 조직하고 타입을 정의하는 강력한 도구입니다. 특히, 블록체인과 같이 복잡한 시스템에서는 인터페이스를 사용하여 코드를 명확하게 구조화하고 각 요소의 역할과 타입을 정의하는 것이 유용합니다. 즉 가독성 및 유지보수성 향상, 타입 정의, 가독성, 타입 확장 및 구현 강제를 위해 폴더하나를 만들도록 한다.
// interface/block.interface.ts
export interface IBlockHeader {
version : string; // 블록의 버전
height : number; // 블록의 높이 0 ~ 부터 시작 블록의 순서
timestamp : number; // 블록의 생성 시간
previousHash : string; // 이전 블록의 해시
}
export interface IBlock extends IBlockHeader {
merkleRoot : string; // 머클루트 해시값
hash : string; // 블록의 내용을 모두 더해서 해시화 시킨 문자열
nonce : number; // 블록을 채굴하기 위해서 몇번이나 연산작업을 시도했는지
diffculty : number; // 블록의 난이도 // POW 알고리즘을 연산작업할때 맞춰야될 퀴즈 2 000
data : string[]; // 블록의 기록되는 트랜젝션들..
}
// interface/faillable.interface.ts
// 결과의 내용을 포함할 객체 모양 정의
// 결과를 반환하는데 성공
// 제네릭 문법으로 어떤 타입이 정의될지 동적으로 설정
export interface Result<R> {
isError : false;
value : R;
}
// 타입을 매개변수처럼 전달이 가능하다
// Result<string>
// {isError : false, value : "원하는 문자열"};
// 선언 당시에 타입을 지정할수있는 장점
// 결과를 반환하는데 실패
// 제네릭 문법으로 어떤 타입이 정의될지 동적으로 설정
export interface Faillure<E> {
isError : true;
value : E;
}
export type Faillable<R, E> = Result<R> | Faillure<E>;
crypto 모듈을 이용해서 해시값을 2진수로 변환하여 난이도 값이 앞에서 부터 난이도의 갯수만큼 0이 포함되어있는지 검사를 해야한다.
// crypto/crypto.module.ts
class CryptoModule {
// 우리가 블록을 생성할때 해시값을 받아서 2진수로 변환해서
// 난이도 충족하는 값을 비교할때 사용할 2진수 값
// 난이도가 2면 2진수 값이 앞에서부터 난이도의 갯수만큼 0이 포함되어있는지 검사를 해야함.
static hashToBinary(hash : string) : string {
let binary : string = "";
// 16 진수를 -> 2진수로 바꿀 식
for (let i = 0; i < hash.length; i++) {
// 반복문에서 현재 인덱스 2자리
const hexByte = hash.substr(i,2);
// 16진수의 바이트를 10진수로 반환
const dec = parseInt(hexByte, 16);
// 10진수를 2진 문자열로 반환
// 8자리로 고정 8자리가 안되면 앞자리 패딩
const binaryByte = dec.toString(2).padStart(8, "0");
// 현재의 2진 바이트를 최종 이진 문자열에 추가
binary += binaryByte;
}
return binary;
}
}
export default CryptoModule;
블록 헤더를 나타내는 TypeScript 클래스인 BlockHeader 정의 해보자. IBlockHeader 인터페이스를 구현하고, 이전 블록의 정보를 사용하여 새로운 블록 헤더를 생성하는 역할을 구현 하도록 한다.
// block/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.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;
- BlockHeader 클래스를 선언하고, IBlockHeader를 구현합니다.
- 클래스 멤버로는 version, height, timestamp, previousHash가 있습니다.
- 생성자(constructor)에서 이전 블록(_previousBlock)의 정보를 받아와서 새로운 블록 헤더를 초기화합니다.
- version은 BlockHeader.getVersion() 메서드를 통해 설정되고, timestamp는 BlockHeader.getTimestamp() 메서드를 통해 현재 시간으로 설정됩니다.
- height는 이전 블록의 높이(_previousBlock.height에서 1을 더한 값)로 설정됩니다.
- previousHash는 이전 블록의 해시 값(_previousBlock.hash)으로 설정됩니다.
블록체인에서 블록을 생성하고 검증하는 데 사용되는 TypeScript 클래스인 Block을 정의해보자.
// block/block.ts
import { IBlock } from "@core/interface/block.interface";
import { Faillable } from "@core/interface/faillable.interface";
import CryptoModule from "@core/crypto/crypto.module";
import BlockHeader from "./blockHeader";
import { SHA256 } from "crypto-js";
import merkle from "merkle";
// block의 형태를 class로 정의
class Block extends BlockHeader implements IBlock {
hash: string;
merkleRoot: string;
nonce: number;
diffculty: number;
data: string[];
constructor(_previousBlock : Block, _data : string[]){
// 부모 블록해더 생성자 호출
super(_previousBlock);
this.merkleRoot = Block.getMerkleRoot<string>(_data); // 머클루트 알고리즘 메서드
this.hash = Block.createBlockHash(this); // 블록의 모든 내용을 가지고 만든 해시값
this.nonce = 0; // 블록생성 당시에 연산작업이 얼마나 반복되었는지 넣을 것.
this.diffculty = 0; // 블록의 난이도 설정 블록의 생성 주기를 조절하기위해서 필요한 값
this.data = _data;
}
// 현재 블록의 해시를 구하는 메서드
static createBlockHash(_block : Block) : string {
const {version, timestamp, height, merkleRoot, previousHash, diffculty, nonce} = _block;
const value : string = `${version}${timestamp}${height}${merkleRoot}${previousHash}${diffculty}${nonce}`;
return SHA256(value).toString();
}
// 머클루트를 구하는 메서드
// getMerkleRoot 메서드를 사용할때 매개변수의 타입을 정의하고싶어.
// getMerkleRoot<T> 함수 호출시 제네릭에 정의한 타입을 함수 안에서 사용하기 위해.
static getMerkleRoot<T>(_data : T[]): string {
const merkleTree = merkle("sha256").sync(_data);
return merkleTree.root();
}
// 블록 생성
// 블록의 해시 만드는 메서드
// 블록의 검증 메서드
// 마이닝 => 블록 생성 권한을 얻기위해서 난이도에 맞는 답을 구하는 연산
static findBlock(generateBlock : Block){
let hash : string
// 연산의 횟수
let nonce : number = 0;
while(true){
// 16진수 -> 2진수 변경
nonce++;
generateBlock.nonce = nonce;
hash = Block.createBlockHash(generateBlock);
// nonnce가 증가함으로 블록의 해시값이 변경 된다
// 정답을 찾아가는 과정
const binary : string = CryptoModule.hashToBinary(hash);
console.log("binary : ",binary);
// 정답을 비교 POW 작업증명
// 연산을 통해서 해당 정답을 찾았는지 비교하는 식은
// 난이도 2 가정 00 이상이니? 0의 갯수
// 000000000000010101110101010101010
// startsWith = 문자열에서 앞에 문자열이 포함되는지 비교
const result : boolean = binary.startsWith("0".repeat(generateBlock.diffculty));
if(result){
// 정답을 맞추면
// 정답을 맞춘 해시를 생성할 블록에 포함시키고
generateBlock.hash = hash;
// 실제로 포함시킬 블록을 반환
return generateBlock;
}
}
}
// 블록이 유효한지 검증하는 메서드
static isValidNewBlock(_newBlock : Block, _previousBlock : Block) : Faillable<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};
}
// 블록 추가
static generateBlock(_previousBlock : Block, _data : string[]) : Block {
const generateBlock = new Block(_previousBlock, _data);
const newBlock = Block.findBlock(generateBlock);
return newBlock;
}
}
export default Block;
필요한 모듈과 인터페이스를 가져올때 IBlock은 블록 인터페이스이고, Faillable은 성공 또는 실패를 나타내는 인터페이스입니다.
생성자에서 이전 블록과 데이터를 받아와서 블록을 초기화합니다. 머클루트, 해시, 난수, 난이도 등을 설정합니다.
- createBlockHash 메서드는 블록의 해시를 생성하는 정적 메서드입니다. 블록의 각 속성들을 문자열로 결합하고 SHA256 해시 함수를 사용하여 해시 값을 생성합니다.
- getMerkleRoot 메서드는 머클루트를 생성하는 정적 메서드입니다. 머클 트리를 사용하여 주어진 데이터의 머클루트를 계산합니다.
- findBlock 메서드는 블록을 찾는 메서드로, 블록의 난이도에 맞는 해시 값을 찾을 때까지 반복하여 계산합니다.
- isValidNewBlock 메서드는 새로운 블록이 유효한지 검사하는 메서드로, 블록의 높이와 이전 블록의 해시 등을 검사하여 유효성을 판단합니다.
- generateBlock 메서드는 새로운 블록을 생성하는 메서드로, 이전 블록과 데이터를 받아와서 블록을 생성하고, 난이도에 맞는 해시를 찾아 반환합니다.
코드 작성을 했으면 이제 jest의 테스트코드를 실행 해보도록 하자
npm install ts-jest @types/jest --save-dev
npm ts-jest config:init
// core/config.ts
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),
diffculty : 0,
nonce : 0,
data : ["tx01"]
}
// jest.config.ts
// jest의 테스트코드를 실행할때 속성값
// jest를 사용할때 필요한 정의된 타입을 가져옴
import type { Config } from "@jest/types";
const config : Config.InitialOptions = {
// 1 모듈의 파일 확장자 설정.
moduleFileExtensions : ["ts", "js"],
// 2 파일의 경로 패턴 설정
testMatch : ["<rootDir>/**/*.test.(js|ts)"],
// temp.test.ts, temp.test.js
// 3 모듈의 이름에 대한 별칭 설정 @core
// 별칭으로 지정된 @core 경로를 어떤식으로 진짜 경로로 바꿔줄거냐
// ^@core == @core/**/*/ 로 시작하는 별칭은 루트경로에 src/core
// 경로 패턴 설정
moduleNameMapper : {
"^@core/(.*)$" : "<rootDir>/src/core/$1"
},
// 4 테스트 환경 설정 node 환경에서 실행 시킬것.
testEnvironment : "node",
// 5. 자세한 로그를 출력 설정
verbose : true,
// 6 프리셋 설정 typescript로 jest 사용
preset : "ts-jest"
}
export default config;
// _test/block.test.ts
import Block from "@core/block/block";
import { GENESIS } from "@core/config";
// 블록생성 테스트 코드 작성
// 최초블록은 하드코딩을 해서 작성해주는 경우가 꽤 있다.
// describe : 테스트 코드의 그룹 단위
describe("block 검증", () => {
let newBlock : Block;
let newBlock2 : Block;
// it : 테스트 코드 최소 단위
it("블록 추가", () => {
// 블록에 기록할 트랜젝션 내용(거래의 기록)
const data = ["tx02"];
// pow 작업 증명을 통과한 퀴즈를 맞춘 블록이 newblock에 할당 된다.
newBlock = Block.generateBlock(GENESIS, data);
// 블록추가의 준비가 된 블록
console.log(newBlock);
// GENESIS => newBlock => newBlock2
const data2 = ["tx03"];
newBlock2 = Block.generateBlock(newBlock, data2);
console.log(newBlock2)
})
it("블록 유효성 검증", ()=>{
const isValidBlock = Block.isValidNewBlock(newBlock2, GENESIS);
// 이전 블록과 비교했을때 새로 추가된 블록이 유효한 블록인지
if(isValidBlock.isError) {
// expect : toBe 값이 맞는지 확인하고 어떤 결과값이 출력되었어야하는지 알려준다.
return expect(isValidBlock.isError).toBe(false);
}
// false 가 나와야하는데 맞니?
expect(isValidBlock.isError).toBe(false);
})
})
'Blockchain' 카테고리의 다른 글
DID(분산신원증명) 개념 (1) | 2024.02.15 |
---|---|
[Blockchain] Merkle (0) | 2024.01.23 |
[Blockchain] 개인키&공개키 (0) | 2024.01.22 |