/** 난이도 조정 블록 범위*/
export const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10;
/** 블록 생성 시간 ( 단위 : 분 ) */
export const BLOCK_GENERATION_INTERVAL: number = 10;
/** 생성 시간 단위 60초 */
export const UNIT: number = 60;
export const GENESIS: IBlock = {
version: '1.0.0',
height: 0,
timestamp: 1231006506,
hash: '0'.repeat(64),
previousHash: '0'.repeat(64),
merkleRoot: '0'.repeat(64),
nonce: 0,
difficulty: 0,
data: ['Hello Block'],
};
블록의 난이도를 조절하기 위해 사용할 난이도를 측정할 블록의 범위와 블록 생성 시간 , 생성 시간 단위를 따로 변수로 빼두었습니다.
또한 블록체인에 최초의 블록인 제네시스 블록도 만들어주었습니다.
import { SHA256 } from 'crypto-js';
import merkle from 'merkle';
import hexToBinary from 'hex-to-binary';
import { BlockHeader } from './blockHeader';
import { BLOCK_GENERATION_INTERVAL, DIFFICULTY_ADJUSTMENT_INTERVAL, GENESIS, UNIT } from '@src/core/config'; // 블록 생성
export class Block extends BlockHeader implements IBlock {
public hash: string; // 블록의 version, timestamp, merkleRoot, previousHash, height, nonce, difficulty 값을 가지고 구한 블록의 고유한 값
public merkleRoot: string; // merkleTree의 최상단에 있는 암호화된 값
public nonce: number; // 블록 생성의 부합하기 위해 hash값을 수정하도록 + 1 씩 증가할 값
public difficulty: number; // 블록 생성 난이도
public data: string[]; // 블록의 트랜잭션
constructor(_previousBlock: Block, _data: string[], _adjustmentBlock: Block) {
super(_previousBlock);
this.merkleRoot = Block.getMerkleRoot(_data);
this.hash = Block.createBlockHash(this);
this.nonce = 0;
this.difficulty = Block.getDifficulty(this, _adjustmentBlock, _previousBlock);
this.data = _data;
}
// 제네시스 블록을 리턴하는 함수
public static getGENESIS(): Block {
return GENESIS;
}
// 블록 merkleRoot 값을 구할 함수
public static getMerkleRoot<T>(_data: T[]): string {
const merkleTree = merkle('sha256').sync(_data);
return merkleTree.root() || '0'.repeat(64);
}
// 블록 해시값을 구할 함수
public static createBlockHash(_block: Block): string {
const { version, timestamp, merkleRoot, previousHash, height, nonce, difficulty } = _block;
const values: string = `${version}${timestamp}${merkleRoot}${previousHash}${height}${nonce}${difficulty}`;
return SHA256(values).toString();
}
// 블록의 난이도를 구할 함수
public static getDifficulty(_newBlock: Block, _adjustmentBlock: Block, _previousBlock: Block): number {
// 블록이 10개가 안되면 제네시스 블록의 난이도를 가져와서 사용
if (_newBlock.height < 9) return 0;
if (_newBlock.height < 19) return 1;
if (_newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVAL !== 0) return _previousBlock.difficulty;
// 10번째의 블록의 시간과 1번째 블록의 시간을 빼면 1~10번째까지 블록의 생성시간이 나옵니다.
const timeTaken: number = _newBlock.timestamp - _adjustmentBlock.timestamp; // 6000
const timeExpected: number = UNIT * BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL; // 6000
if (timeTaken < timeExpected / 2) return _adjustmentBlock.difficulty + 1;
else if (timeTaken > timeExpected * 2) return _adjustmentBlock.difficulty - 1;
return _adjustmentBlock.difficulty;
}
// 블록 검증 코드
public static isValidNewBlock(_newBlock: Block, _previousBlock: Block): Failable<Block, string> {
if (_previousBlock.height + 1 !== _newBlock.height) {
return { isError: true, error: '블록 높이가 맞지 않습니다.' };
}
if (_previousBlock.hash !== _newBlock.previousHash) {
return { isError: true, error: '블록 이전 해시값이 맞지 않습니다.' };
}
if (Block.createBlockHash(_newBlock) !== _newBlock.hash) {
return { isError: true, error: '블록 해시가 변경되었습니다.' };
}
return { isError: false, value: _newBlock };
}
}
Block Class
가 extends를 사용하여BlockHeader Class
를 상속받고 implement로 전시간에 생성한 IBlock의 타입을 그대로 참조하였습니다.- 생성자 함수에서 난이도를 구하는데 필요한
_previousBlock(이전블록)
과_adjustmentBlock(10의 배수 블록)
, 마지막으로merkleRoot
를 만들data
값을 인자값으로 받습니다.- 블록의 해쉬값을 설정하는
this.hash
값을 할당할때createBlockHash()
인자값에 this를 넣어 해시를 생성할때 필요한 값들을 넣어줍니다.- 블록의 난이도를 설정하는
this.difficulty
값을 할당할때는getDifficulty()
함수가 실행되어 인자값으로_newBlock
(현재 생성한 블록)과 ,_adjustmentBlock
(10의 배수 블록),_previousBlock
(이전블록)을 가지고 10번째 블록의 생성 시간에서 1번째 블록의 생성시간을 빼서 나온 시간이 미리 정의해둔 10개의 블록생성 시간을 비교하여 블록의 생성시간이 미리 정의해둔 시간 / 2 보다 크면 난이도가 쉽다는 뜻이니 난이도를 1 올려주고 반대의 경우라면 난이도가 어렵다는 뜻이니 난이도를 1 내려줍니다.- 블록 검증을 하는
isValidNewBlock
함수를 살펴보면 인자값으로 신규 생성한 블록과 이전 블록을 받고 전시간에Failable.d.ts
에 미리 만들어둔Failable
이란generic
을 사용하여 어떠한 에러가 발생했을 경우에는isError
가true
이고error
에 두번째type
으로 받은type
으로 된 값을 리턴하고isError
가false
일 경우에는value
에 첫번째type
으로 받은type
으로 된 값을 리턴하여 블록을 검증해주었습니다.
import { Block } from '@src/core/blockchain/block';
import { DIFFICULTY_ADJUSTMENT_INTERVAL } from '../config';
export class Chain {
public blockchain: Block[];
constructor() {
this.blockchain = [Block.getGENESIS()];
}
public getChain(): Block[] {
return this.blockchain;
}
public getLength(): number {
return this.blockchain.length;
}
public getLatestBlock(): Block {
return this.blockchain[this.blockchain.length - 1];
}
/** 검증된 블록을 블록 배열에 push 할 함수 */
public addBlock(data: string[]): Failable<Block, string> {
// 내가 생성한 블록의 높이값을 먼저 가져오기
// 난이도를 구해야함.
// 생성시간을 구하는것
const previousBlock = this.getLatestBlock();
const adjustmentBlock: Block = this.getAdjustmentBlock(); // 높이가 - 10짜리 블럭
const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock);
const isValid = Block.isValidNewBlock(newBlock, previousBlock);
if (isValid.isError) return { isError: true, error: isValid.error };
this.blockchain.push(newBlock);
return { isError: false, value: newBlock };
}
/** 생성 기준으로 블럭 높이가 -10인 블록 가져오기 */
public getAdjustmentBlock() {
const currentLength = this.getLength(); // 블록체인의 길이는 높이와 같음
const adjustmentBlock: Block =
currentLength < DIFFICULTY_ADJUSTMENT_INTERVAL
? Block.getGENESIS()
: this.blockchain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL];
return adjustmentBlock; // 높이에 해당하는 블록을 반환
}
}
- 앞으로 새로 생성할 블록들을 담을
blockchain
에 GENESIS Block을 담아서 배열을 생성해줍니다.- 이렇게 생성한
blockchain
배열에 block.ts에서 생성한 검증된 블록을 추가해주어야합니다.addBlock
함수로 마지막에 생성된 블록과 10의 배수의 블록을 가져오고 인자값으로 받은data
를 가지고Block class
에 있는generateBlock
함수를 호출하여 새로운 블록을 생성합니다.- 블록을 생성하고 난후
Block class
에 있는isValidNewBlock
함수를 호출하여 새 블록과 마지막에 생성된 블록을 가지고 검증하는 단계를 거칩니다.- 이때 블록에 에러가 있을경우
error
를 리턴하고 에러가 없다면blockchain
배열 안에 새 블록을 추가하고, 추가한 새로운 블록을reture
해줍니다.