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

dev_swan·2022년 6월 16일
1

블록체인

목록 보기
5/36
post-thumbnail

P2P 네트워크 연결 코드 리뷰

웹소켓 서버 열기

/* index.ts */
app.listen(3000, () => {
    console.log('server on');
    ws.listen();
});
/* p2p.ts */
listen() {
        const server = new WebSocket.Server({ port: 7545 });
        server.on('connection', (_socket) => {
            this.connectSocket(_socket);
        });
    }
  • 먼저 서버를 실행하면 ws.listen()에 의해 p2p.ts에 있는 listen()함수가 실행되며 7545 port로 웹소켓 서버가 열립니다.
  • 이렇게 열어둔 웹소켓 서버로 누군가가 접속하게 된다면 저는 서버가 되는것이고 요청보낸 컴퓨터는 클라이언트가 되는것입니다.
  • 이러한 네트워크 구조를 P2P 네트워크라고 합니다.

웹소켓 서버에 연결하기 ( 클라이언트 )

/* index.ts */
app.post('/addPeers', (req, res) => {
    peers.forEach((peer) => {
        ws.connectToPeer(peer);
    });
});
/* 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"
]
  • /addPeerspost 요청을 보내면 peer.json에 배열안에 있는 모든 IP들에게 웹소켓 요청을 보냅니다.
  • 이런식으로 서버에 웹소켓 연결을 요청하게 되면 저는 클라이언트의 역할이 되는것이고 제 요청을 받은 컴퓨터는 서버의 역할이 되는것 입니다.

웹소켓 연결후 데이터 주고 받기 ( 1 )

/* p2p.ts */
private sockets: WebSocket[];

    constructor() {
        super();
        this.sockets = [];
    }

/* --- 중략 --- */

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); // 클라이언트는 서버에게 서버는 클라이언트에게 데이터를 보내주는 역할
    }

/* --- 중략 --- */

errorHandler(_socket: WebSocket) {
        const close = () => {
            this.sockets.splice(this.sockets.indexOf(_socket), 1);
        };
        _socket.on('close', close);
        _socket.on('error', close);
    }

send(_socket: WebSocket) {
        return (_data: Message) => {
            _socket.send(JSON.stringify(_data));
        };
    }
  • 이제 서버, 클라이언트가 정상적으로 핸드쉐이킹이 되면 connectSocket 함수가 실행됩니다.
    함수를 살펴보면 sockets.push로 현재 연결되어 있는 소켓의 정보를 배열에 담아줍니다, 이때 서버측에서는 클라이언트의 정보를 배열에 담게되고 클라이언트측은 서버의 정보를 배열에 담게됩니다.
  • errorHandler 함수는 웹소켓 연결이 끊겼을 경우나 error가 발생했을 경우에만 해당 노드를 배열에서 삭제하여 후에 연결되어 있는 노드들에게 broadcast를 할 때 연결이 끊긴 노드에게는 broadcast를 하지 않도록 해주었습니다.
  • send함수가 실행되며 현재 연결되어 있는 노드에게 type: MessageType.latest_block의 데이터를 보냅니다. 이때도 마찬가지로 서버일경우 클라이언트에게, 클라이언트일경우 서버에게 데이터를 보내줍니다.

웹소켓 연결후 데이터 주고 받기 ( 2 )

/* p2p.ts */

/* --- 중략 --- */

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

/* --- 중략 --- */
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 };
    }
/* --- chain.ts --- */
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 };
    }
    
 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 };
    }
/* --- block.ts --- */
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 };
    }
  • send 함수에서 보낸 datamessageHandler_socket.on('message', callback) 메서드에서 받고 callback 함수를 실행합니다.
  • 함수의 내용을 보면 맨 처음 웹소켓 통신이 완료되고 switch 문으로 case MessageType.latest_block으로 빠져 내가 가지고 있는 블록체인의 가장 마지막 블록을 payload에 담아 case MessageType.all_block으로 보내줍니다.
  • 이번에는 switch문의 case MessageType.all_block로 빠져 받은 payload로 받은 마지막 블록을 addToChain()함수 받아온 마지막 블록을 인자값으로 넣어 실행합니다.
  • addToChain 함수를 보면 받은 블록을 본인의 가장 마지막 블록과 같이 isValidNewBlock() 함수의 인자값으로 넣어 블록을 검증하여 블록 높이, 이전 블록해시, 블록해시를 비교하여 검증하고 에러가 없을경우 받은 블록을 리턴합니다.
  • 다시 addToChain 함수로 돌아와서 isValid의 리턴값이 에러이면 에러를 리턴하고 아니면 본인의 블록체인에 추가하고난 후 return 해줍니다.
  • addToChain 함수에서 에러가 발생하지 않았다면 broadcastsockets에 있는 모든 노드들에게 새로운 블록을 추가하도록 message를 보냅니다.
  • addToChain 함수에서 만약 에러가 발생했다면 type: MessageType.receivedChain으로 다시 message를 보내서 case MessageType.receivedChain으로 빠지게 됩니다. 이때는 블록의 높이가 2 이상 차이났을 경우이니 받은 블록체인을 통째로 갈아끼워주면 됩니다.
  • receivedChain에 받은 블록체인을 담아주고, handleChainResponse 함수를 실행시키고 isValidChain함수로 받은 블록체인을 검증합니다.`isValidChain 함수를 살펴보면 반복문으로 받은 블록체인에 있는 모든 블록들을 검증합니다.
  • isValidChain 함수에서 에러가 나지 않았으면 replaceChain 함수에 받은 블록체인을 넣어 서로의 블록체인을 비교하여 내가 가지고 있는 체인이 더 짧으면 내 블록체인을 받아온 블록체인으로 통째로 변경해주고 return해줍니다.
  • 마지막으로 받아온 블록체인을 payload에 담아 broadcast로 연결되어있는 모든 노드들에게 message를 보내줍니다.

블록 생성시 broadcast 추가

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);
});
  • 마지막으로 블록을 생성하고 나서도 broadcast로 message를 보내주어 다른 노드들도 똑같이 블록을 추가하도록 해주었습니다.

0개의 댓글