Cute Bow Tie Hearts Blinking Pink Pointer

블록체인

[Typescript, 블록체인] P2P 네트워크를 통한 브로드캐스팅 구현하기

청포도 에이드 2022. 6. 15. 17:33
728x90

목차

-브로드캐스팅

-코드 구현

브로드캐스팅

탈 중앙화 시스템에서 거래는 '브로드 캐스팅'을 통해 이루어진다.

중앙화 시스템(우리가 사용하는 은행을 예로들면)에서는 나의 거래 데이터를 중앙 서버에 전송한다.
이에 반해 탈 중앙화 시스템에서는 나의 거래 데이터를 나와 연결된 모든 노드(피어)에게 전송한다.

이렇게 탈 중앙화 시스템에서는 시스템에 참여하는 모든 노드가 서로 데이터를 전송한다.
이론상 모든 노드가 순서는 다르지만 모두 같은 데이터를 가지고 있어야 한다.
하지만 현실에서는 네트워크의 상황이나 네트워크 오류, 피어의 관계 등에 따라(데이터 전달 속도가 다르므로) 특정 시점에 모든 노드가 가지고 있는 데이터는 서로 상의할 수 있다.

이때, 데이터를 가지고 있다고 해당 데이터가 무조건 '기록'되는 것은 아니다.

먼저, '기록'이라는 것은 블록에 데이터를 저장하는 것을 말한다.

그렇다면 누가 '기록'을 할까?
가장 좋은 방법은 '기록'하는 사람을 랜덤으로 결정하는 것이다.
그러나 어떻게 이를 구현할까?
랜덤으로 어떤 노드를 뽑아주는 프로그램을 만든다면, 그 프로그램을 가지고 있는 서버 혹은 노드가 있어야 하는데, 이렇게 되는 순간 탈 중앙화 시스템에서 벗어나게 된다. (특별한 작업을 맡아서 하는 서버(노드)가 생기므로, 이는 공격(해킹)의 대상이 된다.)

때문에 블록 체인에서 '기록'이라는 행위를 위해서는 다음 조건을 만족해야한다.

1. 모든 노드가 '기록'할 수 있어야 한다.
2. 매번 누가 '기록'을 하는지 사전에 예측할 수 없어야 한다.
3. 특정해주는 중앙 서버와 같은 것의 존재가 없어야 한다 

 

 

 

https://dingcodingco.tistory.com/

 

블록체인-브로드캐스팅

브로드캐스팅 탈 중앙화 시스템에서 거래는 '브로드 캐스팅'을 통해 이루어진다. 중앙화 시스템(우리가 사용하는 은행을 예로들면)에서는 나의 거래 데이터를 중앙 서버에 전송한다. 이에 반해

dingcodingco.tistory.com

 

 

코드구현

 

src/core/blockchain/chain.ts
import { Block } from '@core/blockchain/block'
import { DIFFICULTY_ADJUSTMENT_INTERVAL, GENESIS } from '@core/config'

//chain 은 왜 static 이 없을까
//인스턴스가 하나밖에 없음. 변경하거나 가져오는 경우가 더 많기 때문에 static이 없는게 대부분. 배열을 조작하기 위해서 만든 함수.

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 getLatestBlock(): Block {
        return this.blockchain[this.blockchain.length - 1]
    }

    public addBlock(data: string[]): Failable<Block, string> {
        //TODO: 검증을 통해서 맞으면 블록체인이라는 변수에 새로운 블럭을 푸쉬해 줄 것.
        //내가 앞으로 생성할 블록의 높이값을 가져올 수  있는가.
        //현재 높이값, - block interval
        //난이도 구해야 함.
        //생성 시간을 구하는 거.

        const previousBlock = this.getLatestBlock()
        const adjustmentBlock: Block = this.getAdjestmentBlock() // -10 block 구함.
        const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock)
        const isVaild = Block.isVaildNewBlock(newBlock, previousBlock)

        if (isVaild.isError) return { isError: true, error: isVaild.error }

        this.blockchain.push(newBlock)
        return { isError: false, value: newBlock }
    }

    public addToChain(_receivedBlock: Block): Failable<undefined, string> {
        const isVaild = Block.isVaildNewBlock(_receivedBlock, this.getLatestBlock())
        if (isVaild.isError) return { isError: true, error: isVaild.error }

        this.blockchain.push(_receivedBlock)
        return { isError: false, value: undefined }
    }

    //0615
    public isVaildChain(_chain: Block[]): Failable<undefined, string> {
        //TODO:제네시스블록을 검사하는 코드가 들어가면 될거 같습니다.
        const genesis = _chain[0]

        //TODO: 나머지 체인에 대한 코드 부분
        for (let i = 1; i < _chain.length; i++) {
            const newBlock = _chain[i]
            const previousBlock = _chain[i - 1]
            const isVaild = Block.isVaildNewBlock(newBlock, previousBlock)
            if (isVaild.isError) return { isError: true, error: isVaild.error }
        }
        return { isError: false, value: undefined }
    }

    replaceChain(recievedChain: Block[]): Failable<undefined, string> {
        // 내체인과 상대방 체인에 대해서 검사하는
        // 1. 받은 체인의 최신블록.height < 내체인최신블록.height return
        // 2. 받은 체인.previousHash === 내체인.hash
        // 3. 받은 체인의 길이가 === 1 (제네시스밖에 없음) return
        // 4. 내 체인이 더 짧다. 다 바꾸자.
        const lastestReceivedBlock: Block = recievedChain[recievedChain.length - 1]
        const lastestBlock: Block = this.getLatestBlock()

        if (lastestReceivedBlock.height === 0) {
            return { isError: true, error: '받은 최신 블록이 제네시스 블록입니다.' }
        }

        if (lastestReceivedBlock.height <= lastestBlock.height)
            return { isError: true, error: '내 블록의 길이가 더 길거나 같습니다.' } //1번 클리어
        if (lastestReceivedBlock.previousHash === lastestBlock.hash) {
            //addToChain()
            return { isError: true, error: '블록이 하나 모자랍니다.' }
        } //2번 클리어
        //체인을 바꿔주는 코드를 작성하면 됨.
        return { isError: false, value: undefined }
    }
    //체인 검증

    /**
     * 생성기준으로 블럭높이가 -10짜리 구해오기
     */
    public getAdjestmentBlock() {
        //TODO: 블록의 interval 상수
        const currentLength = this.getLength() //getlength 배열안에 몇개 있는가 높이 대체.
        const adjustmentBlock: Block =
            currentLength < DIFFICULTY_ADJUSTMENT_INTERVAL
                ? Block.getGENESIS()
                : this.blockchain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL]
        return adjustmentBlock //높이에 해당되는 블록 자체를 반환.
    }
}

addToChain, isVaildChain, replaceChain 이 세 개의 함수 → p2p 브로드캐스팅을 위한 함수이다.

나머지는 체인과 블록 구현 코드이다.

 

 

 

src/serve/p2p.ts
import { WebSocket } from 'ws'
import { Chain } from '@core/blockchain/chain'

enum MessageType {
    latest_block = 0,
    all_block = 1,
    recievedChain = 2,
}

interface Message {
    type: MessageType
    payload: any
}

export class P2PServer extends Chain {
    public sockets: WebSocket[]

    constructor() {
        super() //chian 그대로 가지고 옴.
        this.sockets = []
    }

    getSockets() {
        return this.sockets
    }
    //서버 시작하는 실행 코드 //내가 누군가한테 받을때만 실행.
    listen() {
        const server = new WebSocket.Server({ port: 7545 })
        server.on('connection', (socket: any) => {
            console.log(`gyuri websocket connetion`)

            this.connectSocket(socket)
        })
    }

    //client 연결 코드 //보내고 받을 때 실행
    connectionToPeer(newPeer: string) {
        const socket = new WebSocket(newPeer)
        socket.on('open', () => {
            //handshake가 왼벽히 일어난 시점. 이걸 안해도 되긴하지만 에러 남.
            this.connectSocket(socket)
        })
    }

    connectSocket(socket: WebSocket) {
        this.sockets.push(socket)
        this.messageHandler(socket)
        //메세지 받기

        //{typ:'',data:''}
        //응답 string > object 요청 string

        const data: Message = {
            type: MessageType.latest_block,
            payload: {},
        }
        this.errorHandler(socket)

        //메시지 보내기 상대방한테 보냄.
        const send = this.send(socket)
        send(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.getLatestBlock()], //상대방이 나한테 준거.
                    }
                    send(message)
                    //내용
                    //send
                    break
                }
                case MessageType.all_block: {
                    const message: Message = {
                        type: MessageType.recievedChain,
                        payload: this.getChain(),
                    }
                    //블록검증코드이후 블록을 넣을지 말지
                    //TODO: 내 체인에 넣을지 말지 검사.
                    //내 블록의 hash와 상대방 블록의 Previoushash값과 같은가.
                    //내 체인에 상대방 블록을 넣으면 된다.
                    //내가 가지고 있는 최신 블록이랑, 받은 블록이랑 비교를 해야됨.

                    //아무리 이전에 검증한 체인이더라도 내 체인에 넣을떄는 검증을 한번 더 거치고 넣어야만 한다.
                    const [recievedBlock] = result.payload
                    const isVaild = this.addToChain(recievedBlock)
                    if (!isVaild.isError) break
                    console.log(isVaild)
                    send(message)
                    break
                }
                case MessageType.recievedChain: {
                    const recievedChain: IBlock[] = result.payload
                    this.handledChainRespone(recievedChain)
                    recievedChain.forEach((e) => console.log(e))
                    // console.log(recievedChain)
                    //체인바꿔주는 코드
                    break
                }
            }
        }
        socket.on('message', callback)
    }

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

    //socket에 대한 내용을 받아오면 되서 static 있어도 되고 없어도 됨.
    //고차함수
    send(_socket: WebSocket) {
        return (_data: Message) => {
            _socket.send(JSON.stringify(_data))
        }
    }
    broadcast(message: Message): void {
        this.sockets.forEach((socket) => this.send(socket)(message))
    }

    public handledChainRespone(recievedChain: IBlock[]): Failable<Message | undefined, string> {
        //전달받은 체인이 일단 올바른가?
        const isVaildChain = this.isVaildChain(recievedChain)
        if (isVaildChain.isError) return { isError: true, error: isVaildChain.error }

        const isVaild = this.replaceChain(recievedChain)
        if (isVaild.isError) return { isError: true, error: isVaild.error }

        this.blockchain = recievedChain

        const message: Message = {
            type: MessageType.recievedChain,
            payload: recievedChain,
        }
        this.broadcast(message)

        return { isError: false, value: undefined }
    }

    static dataParse<T>(_data: string): T {
        return JSON.parse(Buffer.from(_data).toString())
    }
}

p2p 연결과 브로드캐스팅에 대한 코드이다.

 

 

 

index.ts // 루트디렉토리
import { BlockChain } from '@core/index'
import express from 'express'
import { P2PServer } from './src/serve/p2p'
import peers from './peer.json'
const app = express()
const bc = new BlockChain()
const ws = new P2PServer()

enum MessageType {
    latest_block = 0,
    all_block = 1,
    recievedChain = 2,
}

interface Message {
    type: MessageType
    payload: any
}

app.use(express.json())

app.get('/', (req, res) => {
    res.send('Chain')
})

app.get('/chains', (req, res) => {
    res.json(ws.getChain())
})

app.post('/mineBlock', (req, res) => {
    const { data } = req.body
    const newBlock = ws.addBlock(data)
    if (newBlock.isError) return res.status(500).json(newBlock.error)
    const msg: Message = {
        type: MessageType.latest_block,
        payload: {},
    }
    ws.broadcast(msg)
    res.json(newBlock.value)
})

app.post('/addToPeer', (req, res) => {
    const { peer } = req.body
    const url = `ws://${peer}`
    console.log()
    ws.connectionToPeer(url)
})

app.post('/addPeers', (req, res) => {
    peers.forEach((peer) => {
        ws.connectionToPeer(peer)
    })
})

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('서버시작')
    ws.listen()
})

 

 

 

peer.json //루트 디렉토리
["ws://xxx.xxx.x.xxx:port", "ws://xxx.xxx.x.xxx:port", "ws://xxx.xxx.x.xxx:port"]

// 등등 연결할 ip 주소들을 담는다!

 

 

import하는 코드 중에 위에서 없는 파일은 이 전 게시글에 있다(너무 중복이어서 넣지 않았다...).

이 전글로 바로가기

 

[Typescript] 타입스크립트로 블록체인 P2P 구현해보기(찍먹)

타입스크립트로 블록체인에 이용되는 p2p를 찍먹해보겠다. index.ts import { BlockChain } from '@core/index' //블록정보가 담겨있는 객체임. 전 글에서 확인가능합니다 import express from 'express' import {..

green-grapes.tistory.com

/addPeers 로 post를 전송하면(data값 넣지 x), peer.json 에 있는 ip들과 전부 연결됨!

/addToPeer로 특정 ip를 지정해서 전송하면, 그 ip와만 연결된다.

 

근데 그 ip가 이미 다른 많은 ip들과 브로드캐스트 된 상태라면? 나도 거기에 참여할 수 있게 된다! 

728x90