블록체인 기초 (2) 타입스크립트로 블록 만들기 2

707·2022년 6월 9일
1

블록체인

목록 보기
3/10
post-thumbnail

2. 타입스크립트로 블록만들기

(1) @types/Block.ts

Block, Header Class에서 생성되는 인스턴스의 인터페이스를 전역으로 declare해 사용할 수 있게 해주었다.


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

declare interface IBlock extends IBlockHeader {
  merkleRoot: string;
  hash: string;
  data: string[];
}
  • IBlock은 IBlockHeader를 상속받아 만들어주었다.



(2) blockHeader.ts

export class Header implements IBlockHeader {
  public version: string;
  public height: number;
  public timestamp: number;
  public previousHash: string;

  constructor(_previousBlock: IBlock) {
    this.version = Header.getVersion();
    this.height = _previousBlock.height + 1;
    this.timestamp = Header.getTimestamp();
    this.previousHash = _previousBlock.hash;
  }

  static getVersion() {
    return '1.0.0';
  }

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

class에서 인터페이스를 적용하려면 implements를 사용하면 된다.
귀찮은 점은 이렇게 굳이 인터페이스를 별도로 규정해서 가져왔더라도 클래스 내부에서 public (혹은 private 등)으로 한번 더 선언해줘야한다는 점이다. 잊기 쉬우니 유의해주자.

(3) block.ts

import { SHA256 } from 'crypto-js';
import merkle from 'merkle';
import { Header } from './blockHeader';

export class Block extends Header implements IBlock {
  public hash: string;
  public merkleRoot: string;
  public data: string[];

  constructor(_previousBlock: Block, _data: string[]) {
    const merkleroot = Block.getMerkleroot(_data);
    const _header = new Header(_previousBlock);

    super(_previousBlock); // 상속받은 클래스의 속성을 모두 가져옴
    this.hash = Block.createNewHash(_header, merkleroot);
    this.merkleRoot = merkleroot;
    this.data = _data;
  }

  static getMerkleroot(_data: string[]) {
    const merkleTree = merkle('sha256').sync(_data);
    const merkleroot = merkleTree.root();
    return merkleroot;
  }

  static createNewHash(_header: IBlockHeader, merkleroot: string) {
    const str = Object.values(_header).join('') + merkleroot;
    return SHA256(str).toString();
  }
  
  
}

  • crypto-js와 merkle 라이브러리를 import해올때 잊지말고 @types/~ 로 타입까지 인스톨해주자. 어차피 안하면 빨간줄그어짐.
  • 위에 만들어둔 Header class를 가져와 상속하여 Block Class를 만들어주자
  • 상속으로 클래스를 생성한 경우 상위 class의 속성을 모두 가져오려면 super()를 이용하면 된다.
  • 마찬가지로 implements로 인터페이스를 지정해준다
  • header를 만들기 위해서는 previousBlock이 인자로 필요하다. header를 상속받아 만드니 당연히 block에도 previousBlock을 인자로 넣어주어야한다.
  • 필요한 함수를 static으로 만들어준다. 이건 첫 날 한 부분이니 스킵



(4) this를 이용해 createNewHash함수 만들기

// 기존코드

static createNewHash(_header: IBlockHeader, merkleroot: string) {
    const str = Object.values(_header).join('') + merkleroot;
    return SHA256(str).toString();
  }

// this로 인자를 받는 createNewHashThis함수
static createNewHashThis(_block: Block) {
  	// values1 방식 ❌
    const { data, hash, ...rest } = _block;
    const values1 = Object.values(rest).join('');
  
	// values2 방식 ⭕️
    const { version, timestamp, merkleRoot, previousHash, height } = _block;
    const values2: string = `${version}${timestamp}${merkleRoot}${previousHash}${height}`;
  
    return SHA256(values2).toString();
  }


// 생성자함수도 다음과 같이 수정한다.
constructor(_previousBlock: Block, _data: string[]) {
    const merkleroot = Block.getMerkleroot<string>(_data);
    const _header = new Header(_previousBlock);

    super(_previousBlock); // 상속받은 클래스의 속성을 모두 가져옴
    this.merkleRoot = merkleroot;
    this.hash = Block.createNewHashThis(this);
    this.data = _data;
    this.nonce = 0;
    this.difficulty = 0;
  }
  • Block 클래스의 생성자함수에서 this.hash의 위치를 merkleroot의 다음에 두어 createNewHash함수의 인자로 머클루트를 포함한 헤더객체가 들어갈 수 있도록 해준다.
  • 인자의 타입을 Block으로 지정해주면 추후에 다른 곳에서 해당 함수를 이용할 때 block객체 전체 하나만 넣으면 되어 편리함.
  • Block타입의 속성을 모두 가지고 있는 객체를 인자로 받아 구조분해할당을 통해 필요한 것만 해시를 만드는데 이용하면 된다.
    ❗️ 이때 rest를 이용한 value1 방식보다는 필요한 속성을 명시적으로 가져오는 value2방식을 사용하는 것이 좋음. block에 속성이 추가될 경우 (앞으로 마이닝 등에 필요한 difficulty, nonce등의 속성을 추가할 예정) value1의 방식으로는 매번 코드를 수정해줘야한다는 단점이 있으며, 각 속성의 순서가 달라질 경우 결과값이 달라지기 때문에 명시적으로 필요한 속성명과 순서를 알 수 있는 value2 방식을 사용해야한다.


3. jest로 테스트하기

jest를 설치했으니 이걸 활용해서 블록이 제대로 생성되는지 확인해보자.
디렉토리 구조는 다음과 같다.

각각의 기능별로 디렉토리를 구분하고 해당 기능에 대한 test파일을 만드는 식으로 하면 소기능 별 확인이 쉬워진다.

block.test.ts

import { Block } from '@core/blockchain/block';

describe('Block검증', () => {
  // 블록체인 국룰 : 어차피 제네시스 블럭은 하드코딩한 값이다!
  const genesisBlock: Block = {
    version: '1.0.0',
    height: 0,
    hash: '0'.repeat(64),
    timestamp: 1231006506,
    previousHash: '0'.repeat(64),
    merkleRoot: '0'.repeat(64),
    data: ['Hello Block'],
  };
  it('블록생성', () => {
    const data = ['Block #2'];
    const newBlock = new Block(genesisBlock, data);
    console.log(newBlock);
  });
});
  • jest.config.ts에서 moduleNameMapper에 설정해주어서 경로의 별칭으로 절대경로를 잡을 수 있다. Block을 import해왔다
  • 위에서 짠 블록 인스턴스 생성용 class는 이전 블록이 필요하다. 결국 가장 최초의 블록은 직접 작성해주어야한다는 뜻이다. 제네시스 블록은 하드코딩해서 만들어야된다. 가짜 값들을 넣어서 만들어주었다.
  • describe는 여러가지의 테스트케이스를 하나로 모아주는 역할을 한다. 콜백함수 내에서 여러 테스트케이스에서 공통으로 필요한 전역변수 선언 등을 해준다. 개별적인 테스트케이스는 it의 콜백함수로 들어간다.
  • 임의의 data를 만들어준 뒤, 해당 데이터를 넣은 블록을 생성해준다!
  • 테스트 코드를 작성한 뒤 npx jest로 실행해준다. 꼭 테스트파일이 있는 디렉토리가 아니어도 상관없다. 루트디렉토리에서 실행하면 알아서 해당 디렉토리 하위의 영역에 있는 모든 .test.ts 파일을 찾아 알아서 실행해준다.

0개의 댓글