Blockchain

#8 mining ch.2 - 활용

Sila 2022. 6. 17. 19:08

저번 글에서 공부한 nonce와 difficulty가 어떻게 활용되는지 알아보자.

 

우선 새로운 속성이 추가되는만큼 IBlock class 객체의 구성을 수정해주어야 한다.

 

/*  @types/Block.d.ts  */

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

declare interface IBlock extends IBlockHeader {
    merkleRoot : string
    hash : string
    data : string[]
    nonce : number
    difficulty : number
    // 아래 두 줄을 추가
}

 

제네시스 블럭을 포함한 Block class에도 난이도와 논스의 초기값을 추가해주자.

 

/*  src/core/config.ts  */

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

 

/*  src/core/blockchain/block.ts  */

export class Block extends BlockHeader implerments IBlock {
    public hash : string
    public data : string[]
    public merkleRoot : string
    public difficulty : number
    public nonce : number
    
    constructor( _previousBlock ; Block, _data : string[] ) {
        super(_previousBlock)
        
        this.data = _data
        
        const merkleRoot = Block.getMerkleRoot(_data)
        this.merkleRoot = merkleRoot
        
        this.nonce = 0
        this.difficulty = 0 // 추후에 일반화한다
        this.hash = Block.createBlockHash(this)
    }
    
    // ...생략
}

 

1. 난이도 설정

블럭의 추가 속도는 어느 정도 범위내에서 변하도록 설계되어 있다.

 

블럭의 추가 속도는 블럭 안에 있는 정보 중 timestamp를 이용해 계산해낼 수 있다.

 

일정 간격의 블럭들간의 timestamp 차이를 계산하면 블럭 생성에 걸리는 평균적인 시간을 알 수 있고,

 

이 시간이 너무 길다면 ( = 블럭 생성이 너무 빠르다면 ) 난이도를 낮춰주고,

 

너무 짧다면 ( = 블럭 생성이 너무 느리다면 ) 난이도를 높혀주면 된다.

 

그를 위해서 우리가 먼저 정해줘야 할 상수들이 몇 가지 있는데, 이것들은 config.ts에 추가해주자.

 

/*  src/core/config.ts  */

export const GENESIS : IBlock = {
    //...중략
}

export const DIFFICULTY_ADJUSTMENT_INTERVAL : number = 10
// 난이도가 적절한지 체크할 간격

export const BLOCK_GENERATION_INTERVAL_GOAL : number = 60
// 블럭 한 개가 생성되는데 걸리는 시간의 목표치

 

우리가 블럭이 1개/min 의 속도로 생성되도록 생성 속도를 조절하고 싶다면 

 

BLOCK_GENERATION_GINTERVAL_GOAL 를 60 (초)로 써주면 된다.

 

난이도 변경 여부를 10개 단위로 결정하고 싶다면 DIFFICULTY_ADJUSTMENT_INTERVAL의 값을 10으로 정해준다.

 

그렇게 되면 10개 블럭이 생성되는 평균 시간은 600초 (10분)이 되는데,

 

n번째 블럭과 n-10번째 블럭의 timestamp 차이가 이보다 너무 크다면 (블럭 생성이 느림) 난이도를 낮추고

 

너무 작다면 난이도를 높히면 된다.

 

 

이제 본격적으로 난이도 조정의 메커니즘을 구현해보자.

 

n번째 블럭이 생성된 직후, 난이도를 체크하고 조정 여부를 결정하고 싶다고 하자.

 

우선 현재 블럭의 길이 즉, n의 값을 가져온 후, 그걸 기반으로

 

n번째 블럭과 거기서 DIFFICULTY_ADJUSTMENT_INTERVAL (상수, 이하 DAI) 개 전의

 

블럭 (이하 adjustment Block)을  가져온다.

 

 

i) 아직 블럭 갯수가 충분하지 않아 DAI보다 작을 경우, 즉 n < DAI 여서 n-DAI가 음수일 경우

 

그냥 제네시스 블럭의 난이도와 동일하게 하면 된다.

 

(DAI = 10) 일 경우, 1 ~ 9 번째 블럭까지는 10개 전의 블럭이 존재하지 않으므로 그냥 제네시스 블럭의 난이도와 동일하다.

 

ii) 체인의 길이가 충분히 길 경우, 현재 블럭과 adjustment Block을 비교하는데,

 

여기서는  블럭 생성 속도를 1개/min을 기준치로 잡았고, 이보다 2배 이상 빠를 경우 난이도를 1 높히고,

 

너무 느릴 경우 1 낮추도록 하겠다. ( 이에 대한 코드는 후술 )

 

우선 chain.ts에서 adjustmentBlock을 가져오는 함수를 작성하자.

 

/*  src/core/blockchain/chain.ts  */

import { DIFFICULTY_ADJUSTMENT_BLOCK } from '@core/config'

export class Chain {
    //...중략
    
    public getAdjustmentBlock() {
        const currentLength = this.getLength()
        const adjustmentBlock : Block =
        currentLength < DIFFICULTY_ADJUSTMENT_INTERVAL
        ? Block.getGenesis()
        // 블럭 수가 충분히 많지 않다면 제네시스 블럭을 가져온다
        : this.blockchain [ currentLength - DIFFICULTY_ADJUSTMENT_INTERTAL + 1 ]
        return adjustmentBlock
        // 블럭 수가 충분하다면 DAI개 전의 블럭을 리턴
    }
}

 

여기까지 하고 잠시 멈춰서 실제로 새로 계산한 난이도가 적용되기 시작하는 시점을 생각해봐야 한다.

 

우리가 n번째 블럭을 생성할 때 난이도를 맞출 기준 블럭으로 n-10번째 블럭을 가져왔으며,

 

n번째 블럭의 hash값을 찾기 전에 그 난이도가 먼저 정해진다.

 

만약 난이도가 (n-1)번째 블럭까지는 10이었는데, n번째 블럭에서 1이 증가해 11이 되었다면

 

우리가 n번째 블럭의 만족하는 hash값을 반복 연산을 통해 구할 때, 그 문제의 난이도는 10이 아니라 11이 된다.

 

이 난이도 값이 곧바로 hash의 생성을 위해 대입되어야 하므로

 

난이도 조정에 대한 함수는 Block class안에서 실행되어야 그 난이도 값을 정확한 시점에서 반영할 수 있다.

 

다음과 같이 난이도를 구하는 함수를 Block class에 추가하자.

 

/*  src/core/blockchain/block.ts  */

public static getDifficulty(_newBlock : Block, _adjustmentBlock : Block, _previousBlock : Block) {
    if ( _adjustmentBlock.height === 0 ) return 0
    // adjustment Block이 제네시스 블럭인 경우 난이도는 0
    
    if( _newBlock.height % DIFFICULTY_ADJUSTMENT_INTERVAL !== 0 ) {
        return _previousBlock.difficulty
    }
    // 난이도를 조정할 주기가 아직 돌아오지 않았을 경우 이전 블럭의 난이도를 그대로 가져간다.
    
    const timeTaken : number = _newBlock.timestamp - _adjustmentBlock.timestamp
    // 실제 DAI개의 블럭이 생성되는 데 걸린 시간
    const timeExpected : number = BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL
    // 내가 기대한 DAI개의 블럭이 생성되는데 걸리는 시간
    
    if( timeTaken < timeExpected / 2 ) {
        return _adjustmentBlock.difficulty + 1
    }
    // 너무 시간이 적게 걸린 경우 난이도 증가
    
    if( timeTaken >= timeTaken * 2 ) {
        return _adjustmentBlock.difficulty - 1
    }
    // 너무 시간이 오래 걸린 경우 난이도 감소
    
    return _adjustmentBlock.difficulty
    // 걸린 시간이 목표치와 큰 차이가 없을 경우 난이도 유지
}

 

이 함수의 리턴값을 블럭의 난이도에 대입할 것이므로 Block class의 constructor 함수를 수정한다.

 

/*  src/core/blockchain/block.ts  */

constructor( _ previousBLock : Block, _data : string[], _adjustmentBlock : Block ) {
// constructor 함수에 매개변수 _adjustmentBlock을 추가

    // ...중략
    
    this.difficulty = Block.getDifficulty(this, _adjustmentBlock, _previousBlock)
}

 

이제 난이도라는 파라미터를 hash를 구하는데에 추가하기 위해 createBlockHash 함수,

 

새로운 블럭을 생성하는 generateBlock 함수를 수정한다.

 

/*  src/core/blockchain/block.ts  */

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

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

 

이제 generateBlock 함수를 실행하면 난이도까지 반영된 새 블럭이 생성된다.

 

이를 블록체인에 추가하기 위해서 다시 chain class에서 addBlock 함수를 수정한다.

 

/*  src/core/blockchain/chain.ts  */

public addBlock ( data : string[]) : Failable<Block, string> {
    const previousBlock : this.getLatestBlock()
    const adjustmentBlock : Block = this.getAdjustmentBlock
    // 난이도 조정을 위한 adjustment Block을 구하는 함수
    const newBlock : Block = Block.generateBlock(previousBlock, data, adjustmentBlock)
    // 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개마다 조건에 맞게 조정되는지 확인 후, 다음으로 넘어간다.

 

2. nonce

이제 다음으로 '조건에 맞는' hash값을 생성할 수 있도록 nonce값을 추가할 것이다.

 

createBlockHash 함수에 다음과 같이 nonce를 매개변수로 추가하자.

 

/*  src/core/blockchain.ts  */

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

 

이렇게 하고 block을 생성하면 nonce 값에 0을 넣고 hash값을 생성한다.

 

difficulty를 만족하는 hash 값을 찾기 위해 다음과 같이 nonce값을 증가시켜가며 반복 연산을 수행할 

 

findBlock 함수를 선언한다.

 

/*  src/core/blockchain/block.ts  */

public static findBlock ( _generateBlock : Block ) : Block {
    let hash : string
    let nonce : number = 0
    
    while (true) {
        nonce++
        _generateBlock.nonce = nonce
        // nonce 값을 1 증가시켜 생성중인 블럭의 nonce 값에 대입한다.
        
        hash = Block.createBlockhash(_generateBlock)
        // 변한 nonce값을 이용해 다시 hash를 만들어 대입한다.
        const binary : string = hexToBinary(hash)
        const result : boolean = binary.startsWith('0'.repeat(_generateBlock.difficulty))
        // hash를 2진수로 바꿧을 때 difficulty값 만큼의 0이 처음에 오는지 확인해
        // 맞다면 true, 아니면 false를 리턴
        if( result ) {
            generateBlock.hash = hash
            return _generateBlock
            // result가 true라면 만든 hash값을 새로운 블럭의 hash에 대입하고 리턴
        }
    }
}

 

이렇게 새롭게 만들어진 hash를 반영하기 위해 generateBlock 함수를 수정해주어야 한다.

 

/*  src/core/blockchain/block.ts  */

publoc static generateBlock ( _previousBlock : Block, _data : string[], _adjustmentBlock : Block) :Block {
    const generateBlock = new Block (_previousBlock, _data, _adjustmentBlock)
    // 이 시점에서 generateBlock은 nonce = 0, hash는 이를 기준으로 만들어진다.
    // 따라서 아직 difficulty(조건)을 만족하지 않음
    
    const newBlock : Block = Block.findBlock(generateBlock)
    // generateBlock을 이용해 findBlock 함수로 다시 nonce, hash를 찾아 대입
    // 조건을 만족하는 블럭 생성
    
    return newBlock
    // hash, nonce가 수정된 블럭 리턴
}

 

findBlock 함수가 generateBlock 함수 안에서 실행되므로 chain.ts의 addBlock 함수는 바꿔주지 않아도 알아서

 

수정된 블럭이 들어갈 것이다.

 

 

 

 

'Blockchain' 카테고리의 다른 글

#10 지갑, 개인키  (0) 2022.06.20
#9 체인 최신화  (0) 2022.06.17
#7 mining ch.1 - 개념  (0) 2022.06.17
#6 chain  (0) 2022.06.12
#5 block 검증  (0) 2022.06.12