블록체인 Block-Chain - 블록체인 P2P 네트워크 구현 (1)

dev_swan·2022년 6월 16일
1

블록체인

목록 보기
4/36
post-thumbnail

Block class

/* 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;
    }
}

  1. block class 부분은 지난시간에 블록을 만들었을때의 코드와 크게 바뀐것은 없고 제네시스 블록이 다른 파일에서 미리 하드코딩으로 작성해 두고 getGenesis() 메서드로 가져와서 사용했었는데 block class에서 제네시스 블록을 만드는 함수를 만들어서 사용하였습니다.

Chain 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 };
    }
}


  1. chain class에서는 addToChain 함수를 생성하여 내가 마이닝하여 블록을 생성할때 블록을 검증하고 난 후에 블록체인에 추가하는 함수를 만들어주었고, isValidChain 함수를 만들어서 추후에 broadcast를 통해 서로 블록을 비교할텐데 블록의 높이가 2 이상 차이나면 블록체인을 통째로 갈아끼울것인데 그 전에 미리 모든 블록들을 검증하여 받은 블록체인을 검증해줄것입니다.
  2. 이렇게 블록체인의 검증이 끝나면 replaceChain 함수를 통해 내 블록체인을 통째로 변경해줄것 입니다.

P2P 네트워크 ( peer to peer )

블록체인은 P2P 네트워크를 통해 컴퓨터와 컴퓨터간에 서로 데이터를 주고 받는데, 이때 P2P 네트워크란 간단하게 중앙 서버없이 누구나 서버가 되거나 클라이언트가 되어 서로 데이터를 주고 받는것입니다.
이렇게 되면 중앙 서버가 없기 때문에 탈중앙화를 구현 할 수 있으며 보다 빠른 확장성을 가진다는 장점이 있습니다.


P2P 네트워크 구현

peer.json

/* 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

/* 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();
});

  1. 서버를 실행하면 ws.listen()으로 웹소켓 서버도 같이 열리게 되고 /addPeers로 요청을 보낼시 peer.json에서 받아온 IP값들에게 웹소켓 요청을 보내게 되어 양방향으로 데이터를 주고 받을 수 있게 됩니다.
  2. 지금 내가 웹소켓 연결이 되어있는 노드들을 확인하려면 /peers에 요청하면 현재 웹소켓 연결이 되있는 IP값들을 확인할 수 있습니다.
  3. /mineBlock을 보면 data를 받아와서 새로운 블록을 생성하고 만든 새로운 블록을 broadcast로 연결되어 있는 모든 노드들에게 새로 만든 블록을 보내줍니다.
  4. /chains에 요청을 보내면 지금 현재 내 블록체인에 있는 블록들의 정보를 확인할 수 있습니다.

p2p.ts

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());
    }
}

  1. 처음 Websocket이 연결되면 listen()은 서버가 실행할 코드 connectToPeer()는 클라이언트가 실행할 코드로 나뉘게 됩니다.
  2. 서버 측에서는 7545 port로 웹소켓 서버가 열리고 connection이 되면 connectSocket()함수가 실행되고 인자값으로 클라이언트의 socket 정보가 담기게 됩니다.
  3. 클라이언트 측에서는 peer.json에 있는 IPnewPeer로 인자값으로 넘어가고 해당 IP에 웹소켓 연결 요청을 하게 됩니다. 마찬가지로 핸드쉐이킹이 완료되면 connectSocket()함수가 실행되게 되고 인자값으로 서버의 socket정보가 담기게 됩니다.
  4. 서버와 클라이언트 모두 실행되는 connectSocket 함수를 보면 위에서 미리 정의해둔 socket 배열에 현재 연결된 sokcet정보가 담기고 추후에 이 배열안에 있는, 즉 웹소켓 연결이 되어있는 노드들에게 broadcast를 해주게 됩니다.
  5. messageHandler 함수는 후에 socket에서 message를 받으면 callback 함수가 실행되어 case마다 다른 코드를 실행하도록 해주었습니다.
  6. errorHandler 함수는 웹소켓 연결이 끊긴 노드들을 sockets 배열에서 삭제하여 더이상 해당 노드에게 broadcast를 하지 않도록 해줄것입니다.
  7. send 함수는 클라이언트측에서는 서버에게 서버측에서는 클라이언트에게 데이터를 보내주는 역할을 할 것 입니다.

1개의 댓글

comment-user-thumbnail
2024년 7월 7일

혹시 참고한 문헌이 뭔지 여쭤볼 수 있을까요?

답글 달기