티스토리 뷰

TypeScript

[TypeScript] Block Chain 구조

ljy98 2022. 6. 16. 16:27

1. blockHeader.ts 코드 분석

2. block.ts 코드 분석

3. chain.ts 코드 분석


오늘은 TypeScript 언어로 Block Chain의 구조를 구현해보고자 한다.

 

아래 코드는 @types라는 하위 디렉토리에 생성된 Block.d.ts 파일의 내용이다.

declare interface IBlock extends IBlockHeader {
    merkleRoot: string
    hash: string
    nonce: number
    difficulty: number
    data: string[]
}

declare interface IBlockHeader {
    version: string
    height: number
    timestamp: number
    previousHash: string
}

IBlockHeader라는 interface를 선언하였고, 이를 extends하여 IBlock interface도 선언해 주었다.

 

위의 코드는 블록의 블록헤더와 바디의 구조가 어떻게 구성되는지에 대한 대략적인 틀이라고 할 수 있다.

 

그리고, @types 디렉토리 안에 있기 때문에 전역 변수처럼 (import하지 않고) 쓸 수 있다.

 

1. 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
    }

    public static getVersion() {
        return '1.0.0'
    }

    public static getTimestamp() {
        return new Date().getTime()
    }
}

블록헤더는 새로운 인스턴스가 생성될 때 constructor에 의해서 version, timestamp, height, previousHash 에 대한 값이 정해진다.

 

위의 코드에 의하면 version은 항상 1.0.0이고, timestamp는 블록이 생성되는 시점의 정보로 1970년 1월 1일부터 일정 시점까지의 시간 간격을 숫자로 나타낸 값이다. height은 이전 블록 height보다 1씩 증가하며, previousHash는 이전 블록의 해시 값을 의미한다.

 

 

2. block.ts 코드 분석

export const DIFFICULTY_ADJUSTMENT_INTERVAL: number = 10

export const BLOCK_GENERATION_INTERVAL: number = 10

export const UNIT: number = 60

export const GENESIS: IBlock = {
    version: '1.0.0',
    height: 0,
    timestamp: 1231006506,
    previousHash: '0'.repeat(64),
    hash: '0'.repeat(64),
    merkleRoot: '0'.repeat(64),
    nonce: 0,
    difficulty: 0,
    data: ['jenny block'],
}

위의 코드는 config.ts 파일이다.

 

DIFFICULTY_ADJUSTMENT_INTERVAL은 블록 생성 난이도의 변화를 10 개의 블록마다 줄 것이라는 의미이다. 즉, 첫 번째 블록부터 연속하는 10개의 블록에 대한 난이도는 모두 0이고, 그 후에도 최신 블록으로 부터 10개의 블록이 생성되는 시간에 따라 난이도가 증가하거나 감소된다. 주의할 점은 난이도의 값은 항상 0 이상의 정수라는 것이다.

 

BLOCK_GENERATION_INTERVAL은 1개의 블록이 생성되는 시간의 기준을 10분으로 하겠다는 것이다. 1개의 블록이 생성되는데 소요되는 시간이 10분보다 많고 적음에 따라 난이도가 조절된다.

 

UNIT은 1분에 60초이기 때문에 1분에 해당하는 초 단위를 정한 것이다.

 

GENESIS 블록은 IBlock 타입에 맞게 임의로 정한 값으로, 이 블록을 시작으로 새로운 블록들이 생성되고 연결된다.

 

import { UNIT, BLOCK_GENERATION_INTERVAL, DIFFICULTY_ADJUSTMENT_INTERVAL, GENESIS } from '@core/config'
import { SHA256 } from 'crypto-js'
import merkle from 'merkle'
import hexToBinary from 'hex-to-binary'
import { BlockHeader } from './blockHeader'

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[], _adjustmentBlock: Block = _previousBlock) {
        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
    }

    public static getMerkleRoot<T>(_data: T[]): string {
        const merkleTree = merkle('sha256').sync(_data)
        return merkleTree.root() || '0'.repeat(64)
    }

    public static createBlockHash({
        version,
        timestamp,
        height,
        previousHash,
        merkleRoot,
        difficulty,
        nonce,
    }: Block): string {
        const values: string = `${version}${timestamp}${merkleRoot}${previousHash}${height}${difficulty}${nonce}`
        return SHA256(values).toString()
    }

    public static generateBlock(_previousBlock: Block, _data: string[], _adjustmentBlock: Block): Block {
        const generateBlock = new Block(_previousBlock, _data, _adjustmentBlock)
        const newBlock = Block.findBlock(generateBlock)
        return newBlock
    }

    public static findBlock(_generateBlock: Block): Block {
        let nonce: number = 0
        let hash: string
        while (1) {
            nonce++
            _generateBlock.nonce = nonce
            hash = Block.createBlockHash(_generateBlock)
            const binary: string = hexToBinary(hash)
            const result: boolean = binary.startsWith('0'.repeat(_generateBlock.difficulty))
            if (result) {
                _generateBlock.hash = hash
                return _generateBlock
            }
        }
        return _generateBlock
    }

    public static getDifficulty(_newBlock: Block, _adjustmentBlock: Block, _previousBlock: Block): number {
        if (_newBlock.height < 9) return 0
        if (_newBlock.height < 19) return 1
        if (_newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVAL !== 0) return _previousBlock.difficulty

        const timeTaken: number = _newBlock.timestamp - _adjustmentBlock.timestamp
        const timeExpected: number = UNIT * BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL

        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: 'block height error' }
        if (_previousBlock.hash !== _newBlock.previousHash) return { isError: true, error: 'previous hash error' }
        if (Block.createBlockHash(_newBlock) !== _newBlock.hash) return { isError: true, error: 'block hash error' }
        return { isError: false, value: _newBlock }
    }
}

Block에는 블록헤더의 내용에 추가로 hash, merkleRoot, nonce, difficulty, data 값이 들어간다.

 

getGENESIS()는 GENESIS 블록을 return한다.

 

getMerkleRoot()는 merkleTree의 루트 값을 반환한다. 첫 번째 블록의 경우 0이 64개 연속되는 값으로 예외 처리한다.

 

createBlockHash()는 version, timestamp, merkleRoot, previousHash, height, difficulty, nonce가 인자 값으로 들어가고, 이 값들을 순서대로 string으로 join된 값을 SHA256 해시 함수의 인자 값으로 넣는다. 이 인자 값들의 순서는 같은 네트워크 상에 있는 사람들끼리 동일해야 한다. 그렇지 않으면 인자 값을 join한 값이 다르기 때문에 이로 인해 전혀 다른 해시 값이 생성되기 때문이다.

 

generateBlock()는 새로 생성된 블록 인스턴스를 findBlock()의 인자값으로 넣은 newBlock 변수를 return한다.

 

findBlock()은 nonce의 값을 결정하고, 원래의 Chain에 이어줄 블록을 찾는 과정이다. 난이도를 결정할 때에는 해시 값의 첫 번째 자리부터 0이 몇 번 연속하여 나타나는지가 기준이 된다. 따라서, 블록 인스턴스가 생성되었다고 해서 그것이 바로 체인에 붙여지는 것이 아니라 난이도의 조건을 만족하는 해시 값을 가진 블록만이 체인에 연결될 수 있다. 운이 좋지 않으면, 0이 n개 이상 연속하여 나타나는 해시 값을 찾기 위해 블록 인스턴스를 많이 생성하게 되고, 이 때까지 인스턴스를 생성한 횟수가 nonce의 값이다. Hash 함수는 역상 저항성이라는 특징을 갖고 있기 때문에 고의적으로 nonce 값을 줄여서 체인에 연결되는 블록이 많게끔 하는 것은 거의 불가능하다.

 

getDifficulty()는 난이도 값을 정하는 메소드이다. timeTaken은 난이도 설정에 기준이 되는 10개의 블록이 생성되는 데 실제로 걸린 시간을 의미하고, timeExpected는 기준 난이도로 10개의 블록이 생성되는데 걸리는 시간을 예상한 값이다. timeExpected는 60 * 10 * 10 = 6,000초이다. 만약 timeTaken의 값이 3000 보다 작으면 블록이 생성되는 시간이 너무 빠르기 때문에 난이도를 1 증가시키고, 반대로 timeTaken의 값이 12,000 보다 크면 블록이 생성되는 시간이 너무 느리기 때문에 난이도를 1 감소시킨다. 난이도가 1 증가된다는 것은 해시 값의 첫 번째 자리부터 0이 연속되는 갯수가 1개 증가한다는 것인데, 이는 확률적인 측면에서 1/2의 확률로 줄어드는 것과 같다. 따라서 블록이 생성되는 속도가 일정하게 유지될 수 있다.

 

isValidNewBlock()은 블록이 제대로 생성된 것인지 검증하기 위한 메소드이다. GENESIS 블록을 제외한 모든 블록은 이전 블록이 존재한다. 현재 블록의 height는 반드시 이전 블록 height보다 1이 커야 하며, 현재 블록의 previousHash 값은 이전 블록의 해시 값과 일치해야 한다. 또한, 현재 블록의 해시 값은 임의의 값이 아니라, createBlockHash()를 통해 만들어진 것이어야 한다.

 

 

3. chain.ts 코드 분석

import { DIFFICULTY_ADJUSTMENT_INTERVAL } from '@core/config'
import { Block } from './block'

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]
    }

    public addBlock(data: string[]): Failable<Block, string> {
        const previousBlock = this.getLatestBlock()
        const adjustmentBlock: Block = this.getAdjustmentBlock()
        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 }
    }

    public addToChain(_receivedBlock: Block): Failable<undefined, string> {
        const isValid = Block.isValidNewBlock(_receivedBlock, this.getLatestBlock())
        if (isValid.isError) return { isError: true, error: isValid.error }
        this.blockchain.push(_receivedBlock)
        return { isError: false, value: undefined }
    }

    public isValidChain(_chain: Block[]): Failable<undefined, string> {
        const genesis = _chain[0]
        for (let i = 1; i < _chain.length; i++) {
            const newBlock = _chain[i]
            const previousBlock = _chain[i - 1]
            const isValid = Block.isValidNewBlock(newBlock, previousBlock)
            if (isValid.isError) return { isError: true, error: isValid.error }
        }
        return { isError: false, value: undefined }
    }

    public replaceChain(received_chain: Block[]): Failable<undefined, string> {
        const latestReceivedBlock: Block = received_chain[received_chain.length - 1]
        const latestBlock: Block = this.getLatestBlock()
        if (latestReceivedBlock.height === 0) return { isError: true, error: 'Received Block is GENESIS BLOCK' }
        if (latestReceivedBlock.height <= latestBlock.height)
            return { isError: true, error: 'Received Block is shorter than mine' }
        if (latestReceivedBlock.previousHash === latestBlock.hash)
            return { isError: true, error: `Received.previous === Latest.now` }
        this.blockchain = received_chain
        return { isError: false, value: undefined }
    }

    public getAdjustmentBlock() {
        const adjustmentBlock: Block =
            this.getLength() < DIFFICULTY_ADJUSTMENT_INTERVAL
                ? Block.getGENESIS()
                : this.blockchain[this.getLength() - DIFFICULTY_ADJUSTMENT_INTERVAL]
        return adjustmentBlock
    }
}

Chain이라는 class의 blockchain은 Block 타입을 만족하는 블록(객체)들이 담긴 배열이다. 

 

getChain()은 chain 전체를 return하고, getLength()는 chain의 length를 return한다.

 

getLatestBlock()은 chain에서 가장 최근에 생성된 블록을 return한다.

 

addBlock()은 기존의 chain에 새로운 블록을 추가하는 메소드이다. 새로 생성된 블록이 제대로 만들어진 블록인지 isValidNewBlock()로 검증한 뒤 blockchain 배열에 push된다.

 

addToChain()은 addBlock()과 비슷하지만 addBlock()은 자기 자신의 chain에 블록을 붙이는 것이고, addToChain()은 다른 사람의 chain에서 온 블록을 자신의 블록과 비교하여 내 chain에 붙이기 위한 메소드이다.

 

isValidChain()은 해당 chain의 블록들이 제대로 연결되어 있는지를 검증한다. 모든 블록들이 각각 생성될 때 isValidNewBlock() 메소드에 따라 제대로 만들어졌는지 확인한다.

 

replaceChain()은 다른 사람의 chain과 비교하여 3가지 경우에 해당되지 않으면 내가 가진 chain을 통째로 다른 사람의 chain으로 교체하는 메소드이다. 3가지 경우는 다른 사람의 chain에 GENESIS 블록만 존재하거나, 내 chain보다 길이가 짧거나, 내 최신 블록 해시가 다른 사람의 최신 블록의 이전 블록 해시와 같은 경우이다.

 

getAdjustmentBlock()은 난이도 조절에 사용되는 블록을 return한다. 블록이 10개 미만일 때에는 GENESIS 블록이 되고, 그 외에는 최신 블록으로부터 10번째 전의 블록이 된다.

'TypeScript' 카테고리의 다른 글

[TypeScript] P2P(peer-to-peer) 구현  (0) 2022.06.17
[TypeScript] interface vs. type  (0) 2022.06.14
TypeScript 개발 환경 설정 (ESLint, Prettier)  (0) 2022.06.10
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함