저번 포스팅까지의 내용은 TypeScript를 이용해 블록체인을 만들어보는 것이었다. 이번 포스팅부터는 블록체인 P2P 네트워크를 만드는 과정에 대해 알아보고자 한다. 기존에 만들었던 Block 클래스와 Chain 클래스를 사용하면서 P2P 네트워크를 만들 것이기 때문에 block.ts 파일과 chain.ts 파일 안에 작성된 코드를 다시 한번 상기시켜 보도록 하자.
참고)
2022.06.11 - [BlockChain] - BlockChain - TypeScript로 블록체인 만들기 (1)
2022.06.11 - [BlockChain] - BlockChain - TypeScript로 블록체인 만들기 (2) (feat. Jest)
2022.06.12 - [BlockChain] - BlockChain - TypeScript로 블록체인 만들기 (3)
2022.06.14 - [BlockChain] - BlockChain - TypeScript로 블록체인 만들기 (4) PoW
< 목차 >
- Block 클래스 , Chain 클래스 리뷰
- http / ws 세팅하기
1. Block 클래스 , Chain 클래스 리뷰
// block.ts 파일
import { SHA256 } from "crypto-js";
import merkle from "merkle";
import hexToBinary from "hex-to-binary";
import { BlockHeader } from "./blockHeader";
import {
DIFFICULTY_ADJUSTMENT_INTERVAL,
BLOCK_GENERATION_INTERVAL,
BLOCK_GENERATION_TIME_UNIT,
GENESIS,
} from "@core/config";
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) {
super(_previousBlock);
const merkleRoot = Block.getMerkleRoot(_data);
this.merkleRoot = merkleRoot;
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();
}
public static createBlockHash(_block: Block): string {
const {
version,
timestamp,
height,
merkleRoot,
previousHash,
difficulty,
nonce,
} = _block;
const values: string = `${version}${timestamp}${height}${merkleRoot}${previousHash}${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) {
let hash: string;
let nonce: number = 0;
while (true) {
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;
}
}
}
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 =
BLOCK_GENERATION_TIME_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: "height error" };
if (_previousBlock.hash !== _newBlock.previousHash)
return { isError: true, error: "previousHash error" };
if (Block.createBlockHash(_newBlock) !== _newBlock.hash)
return { isError: true, error: "block hash error" };
return { isError: false, value: _newBlock };
}
}
// chain.ts 파일
import { Block } from "@core/blockchain/block";
import { DIFFICULTY_ADJUSTMENT_INTERVAL } from "@core/config";
export class Chain {
private 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 getAdjustmentBlock() {
const currentLength = this.getLength();
const adjustmentBlock: Block =
this.getLength() < DIFFICULTY_ADJUSTMENT_INTERVAL
? Block.getGENESIS()
: this.blockchain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL];
return adjustmentBlock;
}
}
현 프로젝트의 디렉토리 구조는 위와 같다. 우선 src/core/ 디렉토리 안에 index.ts 파일을 생성한 후, 해당 파일 안에서 기존에 만들어 놓은 Chain 클래스를 사용해 블록체인을 만들 수 있도록 하는 BlockChain 클래스를 새롭게 정의해주었다.
// index.ts 파일
import { Chain } from "@core/blockchain/chain";
export class BlockChain {
public chain: Chain;
constructor() {
this.chain = new Chain();
}
}
이제 우리는 index.ts 파일 안의 BlockChain 클래스를 사용해 블록체인 인스턴스를 생성할 수 있다.
2. http / ws 세팅하기
블록체인 P2P 네트워크를 만들기 위해 우리는 http 와 ws (웹 소켓) 을 사용할 예정이다. http 서버는 블록 데이터를 가져오는 api 용도로 사용될 것이기 때문에 express 모듈을 사용해 서버를 만들어줄 것이고 실질적인 P2P 네트워크는 ws를 사용해 만들어 보고자 한다.
우선 터미널에서 express 모듈과 ws 모듈을 설치해주도록 하자.
npm install express
npm i --save-dev @types/express
npm install ws
npm i --save-dev @types/ws
루트 디렉토리 안에 index.ts 파일을 생성하여 해당 파일 안에서는 http 서버와 관련된 코드를 작성하였고 src/ 디렉토리 안의 server/ 디렉토리 안에서는 p2p.ts 파일을 생성하여 ws 서버와 관련된 코드를 작성해주었다. ws 서버 쪽 코드를 먼저 살펴보도록 하자.
// p2p.ts 파일
import { WebSocket } from "ws";
import { Chain } from "@core/blockchain/chain";
export class P2PServer extends Chain {
private sockets: WebSocket[];
constructor() {
super();
this.sockets = [];
}
/**
* listen()
* 서버 입장
* 클라이언트가 연결을 시도했을 때 실행되는 코드
*/
listen() {
const server = new WebSocket.Server({ port: 7545 });
// 서버 기준 "connection"
server.on("connection", (socket) => {
console.log("webSocket connected");
this.connectSocket(socket);
});
}
/**
* connectToPeer()
* 클라이언트 입장
* 서버쪽으로 연결 요청시 실행되는 코드
*/
connectToPeer(newPeer: string) {
const socket = new WebSocket(newPeer);
// 클라이언트 기준 "open"
socket.on("open", () => {
this.connectSocket(socket);
});
}
connectSocket(socket: WebSocket) {
this.sockets.push(socket);
socket.on("message", (data: string) => {
console.log(data);
});
socket.send("msg from server");
}
}
p2p.ts 파일 안에서 P2PServer 라는 클래스를 정의하였는데 해당 클래스는 Chain 클래스를 상속 받고 있다. 부모 클래스 (Chain 클래스)의 속성들을 가져오기 위해 constructor( ) 함수 안에서 super( ) 메소드를 사용하였으며 P2PServer 클래스 안에서 listen( ) , connectToPeer( ) , connectSocket( ) 메소드를 만들어 주었다.
블록체인 P2P 네트워크 상에서는 네트워크에 참여하는 모든 컴퓨터가 클라이언트이면서 동시에 서버로서의 역할을 담당한다. 다시말해, 클라이언트나 서버란 개념이 없이 오로지 동등한 계층의 노드들(peer nodes)이 서로 클라이언트와 서버 역할을 동시에 네트워크 위에서 하게 되는 셈이다. 따라서 P2P 네트워크를 구축할 때는 서버 쪽 코드와 클라이언트 쪽 코드를 동시에 작성해줘야만 한다.
P2PServer 클래스 안에서 정의된 listen( ) 메소드는 서버 입장에서 실행되는 코드이다.
listen() {
const server = new WebSocket.Server({ port: 7545 });
// 서버 기준 "connection"
server.on("connection", (socket) => {
console.log("webSocket connected");
this.connectSocket(socket);
});
}
listen( ) 메소드 안의 new WebSocket.Server({ port: 7545 }) 에 의해 7545번 포트에서 웹 소켓 서버가 요청을 받을 수 있는 상태가 된다. 그리고 핸드쉐이크가 일어나서 클라이언트와 웹 소켓이 연결(connection 이벤트)되었을 때 connectSocket( ) 메소드가 실행된다.
반면, connectToPeer( ) 메소드는 클라이언트 입장에서 실행되는 코드라고 볼 수 있다.
connectToPeer(newPeer: string) {
const socket = new WebSocket(newPeer);
// 클라이언트 기준 "open"
socket.on("open", () => {
this.connectSocket(socket);
});
}
connectToPeer( ) 메소드의 경우 newPeer 라는 string 타입의 매개변수를 가지게 된다. 향후 connectToPeer( ) 함수가 호출될 때 인자값으로 요청을 보낼 url을 전달하면 new WebSocket( newPeer ) 에 의해 해당 url 주소를 갖고있는 노드와 웹 소켓이 연결된다. (이 때 요청을 받은 노드에서는 listen( ) 메소드가 실행되는 것이다.) 웹 소켓이 연결되었을 때 클라이언트 입장에서의 이벤트명은 "open"이 되며 open 이벤트 발생 이후 connectSocket( ) 메소드가 실행된다.
다음으로 살펴볼 것은 connectSocket( ) 메소드이며 해당 함수 안에는 서버 쪽 코드와 클라이언트 쪽 코드가 동시에 존재한다.
connectSocket(socket: WebSocket) {
this.sockets.push(socket);
socket.on("message", (data: string) => {
console.log(data);
});
socket.send("msg from server");
}
예를 들어 A 노드 컴퓨터에서 connectToPeer( ) 함수를 호출시켜 B 노드의 컴퓨터와 웹 소켓이 연결될 경우 B 노드 컴퓨터에서는 "connection" 이벤트가 발생하여 connectSocket( ) 함수가 호출되고 socket.send( ) 메소드에 의해 "msg from server" 데이터가 A 노드에게 전달된다. A 노드에서는 B노드와 웹 소켓 연결이 완료된 시점에서 "open" 이벤트가 발생되었을 것이고 connectSocket( ) 함수가 호출되어 socket.on( "message", (data)=>{ console.log(data) } ) 에 의해 B 노드에서 전달한 "msg from server" 데이터를 받게 된다.
위의 예시에서는 마치 A 노드 컴퓨터가 클라이언트 , B 노드 컴퓨터는 서버로서 동작하는 느낌을 받을 수 있다. 하지만 C 노드 컴퓨터가 해당 네트워크에 참여하여 A 노드 컴퓨터에 요청을 보내는 순간 A 노드는 B 노드처럼 , C 노드는 A 노드처럼 동작하게 된다. 방금 전까지 클라이언트로서 기능하던 A 노드가 C에게는 서버로서 작동하는 것이다. 이처럼 P2P 네트워크에서는 모든 노드들이 서버이면서 동시에 클라이언트가 될 수 있다.
이제 루트 디렉토리에서 index.ts 파일을 생성하여 http 서버를 만들어 주도록 하자. 여기서 http 서버는 블록체인 P2P 네트워크 상에서 블록의 데이터를 조회하거나 블록의 정보를 가져오는 api 용도로서 기능하게 된다.
// 루트 디렉토리 index.ts 파일
// BlockChain 클래스 가져오기
import { BlockChain } from "@core/index";
import { P2PServer } from "./src/server/p2p";
import express from "express";
const app = express();
const ws = new P2PServer();
app.use(express.json());
app.get("/", (req, res) => {
res.send("bit-chain");
});
// 블록 내용 조회 api
app.get("/chains", (req, res) => {
res.json(ws.getChain());
});
// 블록 채굴 api
app.post("/mineBlock", (req, res) => {
const { data } = req.body;
const newBlock = ws.addBlock(data);
if (newBlock.isError) return res.status(500).send(newBlock.error);
res.json(newBlock.value);
});
// ws 연결 요청 api
app.post("/addToPeer", (req, res) => {
const { peer } = req.body;
ws.connectToPeer(peer);
});
// 연결된 sockets 조회
app.get('/peers', (req, res) => {
const sockets = ws.getSockets().map((s: any) => s._socket.remoteAddress + ':' + s._socket.remotePort);
res.json(sockets);
});
app.listen(3000, () => {
console.log("server onload # port: 3000");
ws.listen();
});
express를 사용해 기능별로 라우터를 만들어줬으며 향후 해당 기능들을 하나씩 구현해볼 예정이다. 현재 눈여겨 봐야할 부분은 app.listen( 3000, () => { ws.listen() } ) 과 app.post( "/addToPeer", (req, res) => { ws.connectToPeer(peer) } ) 부분이다. app.listen( 3000, () => { } ) 으로 3000번 포트에서 http 서버를 열어 놓음과 동시에 new P2PServer( ) 로 생성된 ws 인스턴스의 listen( ) 메소드에 의해 웹 소켓 서버 역시 7545번 포트에서 열어 놓았다.
url 데이터와 함께 http 서버의 "/addToPeer" 경로로 post 요청이 들어오면 ws.connectToPeer( ) 메소드가 실행되면서 해당 url 주소를 갖고 있는 노드 컴퓨터와 웹 소켓을 연결하게 된다. 이후 url 주소에 해당하는 컴퓨터는 서버 역할을 하게 되고 ws.connectToPeer( ) 메소드가 실행된 노드 컴퓨터는 클라이언트 입장이 되는 것이다.
이번 포스팅에서는 블록체인 P2P 네트워크를 구축하기 위한 기본적인 골격을 잡아보는 데에 중점을 두고 코드를 작성하였다. p2p.ts 파일 안에 작성된 코드들을 살펴보면 실제 블록체인과 관련된 내용의 코드들이 아닌 전체적인 흐름에 중점을 둔 코드 형태로 작성된 것을 알 수 있다. 이제 P2P 네트워크가 어떤식으로 동작하는지에 대한 이해를 바탕으로 다음번 포스팅에서 실제 블록체인 네트워크를 만들기 위한 기초 작업을 진행해 보고자 한다.
다음 글)
2022.06.15 - [분류 전체보기] - BlockChain - 블록체인 P2P 네트워크 만들기 (2)
'BlockChain' 카테고리의 다른 글
BlockChain - 블록체인 P2P 네트워크 만들기 (3) (2) | 2022.06.16 |
---|---|
BlockChain - 블록체인 P2P 네트워크 만들기 (2) (0) | 2022.06.15 |
BlockChain - TypeScript로 블록체인 만들기 (4) PoW (0) | 2022.06.14 |
BlockChain - TypeScript로 블록체인 만들기 (3) (0) | 2022.06.12 |
BlockChain - TypeScript로 블록체인 만들기 (2) (feat. Jest) (1) | 2022.06.11 |