/* Block.ts */
import { BLOCK_GENERATION_INTERVAL, DIFFICULTY_ADJUSTMENT_INTERVAL, UNIT } from '@src/core/config';
import { SHA256 } from 'crypto-js';
import merkle from 'merkle';
import { BlockHeader } from './blockHeader';
import hexToBinary from 'hex-to-binary';
export class Block extends BlockHeader implements IBlock {
public hash: string;
public merkleRoot: string;
public data: string[];
public nonce: number;
public difficulty: number;
constructor(_previousBlock: Block, _data: string[], _adjustmentBlock: Block = _previousBlock) {
super(_previousBlock);
const merkleRoot = Block.getMerkleRoot(_data);
this.merkleRoot = merkleRoot;
this.hash = Block.createBlockHash(this);
this.nonce = 0;
this.difficulty = Block.getDiffficulty(this, _adjustmentBlock, _previousBlock);
this.data = _data;
}
public static getGenesis() {
return Block.createGENESIS();
}
// 블록 머클루트를 구할때 실행할 함수
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, difficulty, nonce } = _block;
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;
}
// 블록의 nonce값을 찾는 함수
public static findBlock(_generateBlock: Block): Block {
let hash: string;
let nonce: number = 0;
while (true) {
nonce++;
console.log(_generateBlock);
_generateBlock.nonce = nonce;
hash = Block.createBlockHash(_generateBlock);
const binary: string = hexToBinary(hash); // 01010101000101
const result: boolean = binary.startsWith('0'.repeat(_generateBlock.difficulty));
if (result) {
_generateBlock.hash = hash;
return _generateBlock;
}
}
}
// 블록 난이도를 조절할때 사용할 함수
public static getDiffficulty(_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; // 6000
const timeExpected: number = UNIT * BLOCK_GENERATION_INTERVAL * DIFFICULTY_ADJUSTMENT_INTERVAL;
if (0 < 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 };
}
// 제네시스 블록
static createGENESIS(): Block {
const GENESIS = {
version: '1.0.0',
height: 0,
hash: '6df9b12826161ba149f0e2a1666cce76df41b1bd5ffc333350352235236ac2a6',
previousHash: '0000000000000000000000000000000000000000000000000000000000000000',
merkleRoot: '1123E2B1E583165C7A8264434C9129E29B546AE77DCAEF752A9FA408588F641C',
timestamp: 1655218800000,
difficulty: 0,
nonce: 0,
data: ['FUCKING AWESOME GENESIS 5'],
};
return GENESIS;
}
}
block class
부분은 지난시간에 블록을 만들었을때의 코드와 크게 바뀐것은 없고 제네시스 블록이 다른 파일에서 미리 하드코딩으로 작성해 두고getGenesis()
메서드로 가져와서 사용했었는데block class
에서 제네시스 블록을 만드는 함수를 만들어서 사용하였습니다.
/* Chain.ts */
import { Block } from '@src/core/blockchain/block';
import { DIFFICULTY_ADJUSTMENT_INTERVAL } from '@src/core/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 getLastestBlock(): Block {
return this.blockchain[this.blockchain.length - 1];
}
// 블록을 추가할 때 실행할 함수
public addBlcok(data: string[]): Failable<Block, string> {
const previousBlock = this.getLastestBlock();
const adjustmentBlock: Block = this.getAdjustmentBlock();
const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock);
const isVaild = Block.isValidNewBlock(newBlock, previousBlock);
if (isVaild.isError) return { isError: true, error: isVaild.error };
this.blockchain.push(newBlock);
return { isError: false, value: newBlock };
}
// 블록을 검증하여 에러가 없을시 내 블록체인에 받은 블록을 추가하는 코드
public addToChain(_receviedBlock: Block): Failable<undefined, string> {
const isValid = Block.isValidNewBlock(_receviedBlock, this.getLastestBlock());
if (isValid.isError) return { isError: true, error: isValid.error };
this.blockchain.push(_receviedBlock);
return { isError: false, value: undefined };
}
// 블록체인에 담겨있는 모든 블럭들을 검증하는 코드
public isValidChain(_chain: Block[]): Failable<undefined, string> {
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 === true) return { isError: true, error: isValid.error };
}
return { isError: false, value: undefined };
}
// 10,20,30...n 번째 블록을 찾는 함수 ( 난이도를 조절할때 사용함 )
public getAdjustmentBlock() {
const currentLength = this.getLength();
const adjustmentBlock: Block =
currentLength < DIFFICULTY_ADJUSTMENT_INTERVAL
? Block.getGenesis()
: this.blockchain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL];
return adjustmentBlock;
}
// 서로의 블록체인을 비교하여 블록체인을 바꿔줄 함수
replaceChain(receivedChain: Block[]): Failable<undefined, string> {
const latestReceivedBlock: Block = receivedChain[receivedChain.length - 1];
const latestBlock: Block = this.getLastestBlock();
// 1. 받은 체인의 최신블록.heigth <= 내 체인 최신블록.height = return
// 2. 받은 체인의 최신블록.previousHash === 내 체인 최신블록.hash = reuturn
// 3. 받은 체인의 길이가 === 1 ( 제네시스 블록밖에 없음 ) reuturn
if (latestReceivedBlock.height === 0) {
return { isError: true, error: '받은 최신블록이 제네시스 블록입니다. ' };
}
if (latestReceivedBlock.height <= latestBlock.height) {
return { isError: true, error: '자신의 체인이 더 길거나 같습니다. ' };
}
if (latestReceivedBlock.previousHash === latestBlock.hash) {
return { isError: true, error: '블록이 하나 모자랍니다. ' };
}
// 4. 내 체인이 더 짧으면 받은 블록체인으로 변경
this.blockchain = receivedChain;
return { isError: false, value: undefined };
}
}
chain class
에서는addToChain
함수를 생성하여 내가 마이닝하여 블록을 생성할때 블록을 검증하고 난 후에 블록체인에 추가하는 함수를 만들어주었고,isValidChain
함수를 만들어서 추후에broadcast
를 통해 서로 블록을 비교할텐데 블록의 높이가 2 이상 차이나면 블록체인을 통째로 갈아끼울것인데 그 전에 미리 모든 블록들을 검증하여 받은 블록체인을 검증해줄것입니다.- 이렇게 블록체인의 검증이 끝나면
replaceChain
함수를 통해 내 블록체인을 통째로 변경해줄것 입니다.
블록체인은
P2P
네트워크를 통해 컴퓨터와 컴퓨터간에 서로 데이터를 주고 받는데, 이때P2P
네트워크란 간단하게 중앙 서버없이 누구나 서버가 되거나 클라이언트가 되어 서로 데이터를 주고 받는것입니다.
이렇게 되면 중앙 서버가 없기 때문에 탈중앙화를 구현 할 수 있으며 보다 빠른 확장성을 가진다는 장점이 있습니다.
/* peer.json */
[
"ws://192.168.0.123:7545",
"ws://192.168.0.143:7545",
"ws://192.168.0.234:7545",
"ws://192.168.0.256:7545",
"ws://192.168.0.187:7545",
"ws://192.168.0.165:7545",
"ws://192.168.0.162:7545"
]
Websocket
연결 요청을 보낼IP
값들입니다.
이IP
들이 서버 역할을 하고 제가 요청을 보내는 클라이언트의 역할이 될 것입니다.
/* index.ts */
import { P2PServer } from '@src/core/serve/p2p';
import peers from './peer.json';
import express from 'express';
const app = express();
const ws = new P2PServer();
enum MessageType {
latest_block = 0,
all_block = 1,
receivedChain = 2,
}
interface Message {
type: MessageType;
payload: any;
}
app.use(express.json());
app.get('/', (req, res) => {
res.send('block_chain');
});
// 내 블록체인 조회
app.get('/chains', (req, res) => {
res.json(ws.getChain());
});
// 블록채굴 API
app.post('/mineBlock', (req, res) => {
const { data } = req.body;
const newBlock = ws.addBlcok(data);
if (newBlock.isError) return res.status(500).send(newBlock.error);
const message: Message = {
type: MessageType.all_block,
payload: [newBlock.value],
};
ws.broadcast(message);
res.json(newBlock.value);
});
// 연결된 socket 조회
app.get('/peers', (req, res) => {
const sockets = ws.getSockets().map((s: any) => {
return s._socket.remoteAddress + ':' + s._socket.remotePort;
});
res.json(sockets);
});
// ws 연결 요청 API
app.post('/addPeers', (req, res) => {
peers.forEach((peer) => {
ws.connectToPeer(peer);
});
});
app.listen(3000, () => {
console.log('server on');
ws.listen();
});
- 서버를 실행하면
ws.listen()
으로 웹소켓 서버도 같이 열리게 되고/addPeers
로 요청을 보낼시peer.json
에서 받아온IP
값들에게 웹소켓 요청을 보내게 되어 양방향으로 데이터를 주고 받을 수 있게 됩니다.- 지금 내가 웹소켓 연결이 되어있는 노드들을 확인하려면
/peers
에 요청하면 현재 웹소켓 연결이 되있는IP
값들을 확인할 수 있습니다./mineBlock
을 보면data
를 받아와서 새로운 블록을 생성하고 만든 새로운 블록을broadcast
로 연결되어 있는 모든 노드들에게 새로 만든 블록을 보내줍니다./chains
에 요청을 보내면 지금 현재 내 블록체인에 있는 블록들의 정보를 확인할 수 있습니다.
import { WebSocket } from 'ws';
import { Chain } from '@src/core/blockchain/chain';
enum MessageType {
latest_block = 0,
all_block = 1,
receivedChain = 2,
}
interface Message {
type: MessageType;
payload: any;
}
export class P2PServer extends Chain {
private sockets: WebSocket[];
constructor() {
super();
this.sockets = [];
}
getSockets() {
return this.sockets;
}
// 서버 실행코드
listen() {
const server = new WebSocket.Server({ port: 7545 });
server.on('connection', (_socket) => {
this.connectSocket(_socket);
});
}
// 클라이언트 실행코드
connectToPeer(newPeer: string) {
const socket = new WebSocket(newPeer);
socket.on('open', () => {
this.connectSocket(socket);
});
}
// 서버와 클라이언트 모두에서 실행
connectSocket(_socket: WebSocket) {
this.sockets.push(_socket); // 추후에 Websocket 연결된 노드들에게 broadcast 하기 위함
this.messageHandler(_socket); // 요청이 들어오면 실질적으로 실행되어 클라이언트와 서버의 블록을 같게 해줄 코드
const data: Message = {
type: MessageType.latest_block,
payload: {},
};
this.errorHandler(_socket); // 후에 Websocket 연결이 끊긴 노드들을 sockets 배열에서 삭제하여 broadcast를 실행하지 않도록해줍니다.
this.send(_socket)(data); // 클라이언트는 서버에게 서버는 클라이언트에게 데이터를 보내주는 역할
}
messageHandler(_socket: WebSocket) {
const callback = (data: string) => {
const result: Message = P2PServer.dataParse<Message>(data);
const send = this.send(_socket);
switch (result.type) {
// 블록 체인에 가장 마지막에 있는 블록을 보내줍니다.
case MessageType.latest_block: {
const message: Message = {
type: MessageType.all_block,
payload: [this.getLastestBlock()],
};
send(message);
break;
}
// result.payload로 마지막 받은 블록을 내 블록체인에 추가하고 error가 발생하지 않았을때만 연결된 다른 sockets들에게 broadcast
case MessageType.all_block: {
const message: Message = {
type: MessageType.receivedChain,
payload: this.getChain(),
};
const [receivedBlock] = result.payload;
const isVaild = this.addToChain(receivedBlock);
if (!isVaild.isError) {
const message: Message = {
type: MessageType.all_block,
payload: [this.getLastestBlock()],
};
this.broadcast(message);
break;
}
send(message);
break;
}
// 블록의 높이가 2 이상 차이 날경우 체인을 통째로 바꿔줍니다.
case MessageType.receivedChain: {
const receivedChain: IBlock[] = result.payload;
console.log('체인 통째로 바꿔끼기', receivedChain);
this.handleChainResponse(receivedChain);
break;
}
}
};
_socket.on('message', callback);
}
// Websocket 연결이 끊길때 실행될 함수
errorHandler(_socket: WebSocket) {
const close = () => {
this.sockets.splice(this.sockets.indexOf(_socket), 1);
};
_socket.on('close', close);
_socket.on('error', close);
}
// 클라이언트 혹은 서버에게 data를 보낼때 실행될 함수
send(_socket: WebSocket) {
return (_data: Message) => {
_socket.send(JSON.stringify(_data));
};
}
// 연결되 있는 모든 sockets들에게 내가 가진 블록체인과 상대방의 블록체인을 비교하여
// 블록체인 값이 다르면 서로의 블록체인을 같게 만드는 함수
broadcast(message: Message): void {
this.sockets.forEach((socket) => {
this.send(socket)(message);
});
}
// 블록체인을 통째로 갈아끼우는 함수
handleChainResponse(receivedChain: IBlock[]): Failable<Message | undefined, string> {
const isValidChain = this.isValidChain(receivedChain);
if (isValidChain.isError) return { isError: true, error: isValidChain.error };
const isValid = this.replaceChain(receivedChain);
if (isValid.isError) return { isError: true, error: isValid.error };
const message: Message = {
type: MessageType.receivedChain,
payload: receivedChain,
};
this.broadcast(message);
return { isError: false, value: undefined };
}
// 데이터 parse
public static dataParse<T>(_data: string): T {
return JSON.parse(Buffer.from(_data).toString());
}
}
- 처음
Websocket
이 연결되면listen()
은 서버가 실행할 코드connectToPeer()
는 클라이언트가 실행할 코드로 나뉘게 됩니다.- 서버 측에서는 7545
port
로 웹소켓 서버가 열리고connection
이 되면connectSocket()
함수가 실행되고 인자값으로 클라이언트의socket
정보가 담기게 됩니다.- 클라이언트 측에서는
peer.json
에 있는IP
가newPeer
로 인자값으로 넘어가고 해당IP
에 웹소켓 연결 요청을 하게 됩니다. 마찬가지로 핸드쉐이킹이 완료되면connectSocket()
함수가 실행되게 되고 인자값으로 서버의socket
정보가 담기게 됩니다.- 서버와 클라이언트 모두 실행되는
connectSocket
함수를 보면 위에서 미리 정의해둔socket
배열에 현재 연결된sokcet
정보가 담기고 추후에 이 배열안에 있는, 즉 웹소켓 연결이 되어있는 노드들에게broadcast
를 해주게 됩니다.messageHandler
함수는 후에socket
에서message
를 받으면callback
함수가 실행되어case
마다 다른 코드를 실행하도록 해주었습니다.errorHandler
함수는 웹소켓 연결이 끊긴 노드들을sockets
배열에서 삭제하여 더이상 해당 노드에게broadcast
를 하지 않도록 해줄것입니다.send
함수는 클라이언트측에서는 서버에게 서버측에서는 클라이언트에게 데이터를 보내주는 역할을 할 것 입니다.
혹시 참고한 문헌이 뭔지 여쭤볼 수 있을까요?