P2P - ch.2

SKKRYPTO·2021년 5월 7일
1

SKKRYPTO 7기 김용입니다. 저번시간에는 P2P가 무엇인지에 대해 알아보았습니다. 이번 시간에는 P2P기반의 블록체인 네트워크를 GO라는 프로그래밍언어를 통해 구현해 보도록 하겠습니다.


P2P 구현

노드의 역할

채굴 노드

채굴 노드는 새로운 블록을 최대한 빠르게 채굴하는 것이다. 채굴 노드는 작업 증명 시스템을 이용하는 블록체인에서만 가능하다.(블록체인은 작업증명 시스템이다.)

풀 노드

이 노드는 채굴 노드에 의해 채굴된 블록의 유효성을 확인하고 트랜잭션을 검증한다. 또한 풀 노드는 다른 노드들이 서로를 찾을 수 있도록 돕는 일과 같은 라우팅 연산을 수행하기도 한다.

SPV(Simplified Payment Verification)

이 노드는 트랜잭션 검증을 할 수 있다. SPV 노드는 풀 노드로부터 데이터를 얻어오며 하나의 풀 노드에 여러개의 SPV 노드가 연결될 수 있다. SPV는 지갑 애플리케이션을 가능케 한다.

다음 시나리오를 구현한다.

  1. 중앙 노드는 블록체인을 생성한다.
  2. 다른 지갑 노드들은 중앙 노드에 연결되어 블록체인을 다운로드 받는다.
  3. 채굴자 노드들은 중앙 노드에 연결되어 블록체인 다운로드 받는다.
  4. 지갑 노드는 트랜잭션을 생성한다.
  5. 채굴자 노드는 트랜잭션을 받아 메모리 풀에 저장해 둔다.
  6. 메모리 풀에 충분한 양의 트랜잭션이 쌓이면 채굴자 노드는 새로운 블록의 채굴을 시작한다.
  7. 새로운 블록이 채굴되면, 중앙 노드에 전송된다.
  8. 지갑 노드는 중앙 노드와 동기화된다.
  9. 지갑 노드의 사용자는 지불이 정상적으로 이루어졌는지 확인한다.

노드는 메시지를 통해 통신한다. 새로운 노드가 실행되면 DNS 시드에서 여러 노드를 가져와 version 메시지를 전송한다. 메시지는 다음과 같이 구현할 수 있다.

type version struct {
 	Version int
	BestHeight int
	AddrFrom string
}

BestHeight는 노드가 가진 블록체인의 길이를 저장한다.
AddrFrom은 전송자의 주소를 저장한다.

version 메시지를 어떤 노드가 받았다면 이 노드는 자신의 version 메시지로 응답해야 한다.
version은 더 긴 블록체인을 찾는데 사용된다. 노드가 version 메시지를 받으면 이는 자신이 가진 블록체인이 BestHeight보다 더 긴지 확인한다. 더 짧은 경우, 노드는 누락된 블록을 요청하여 다운로드 받는다.

메시지를 받기 위해서는 서버가 필요하며 아ㄹ는 서버를 구현한 코드이다.

var nodeAddress string 
var knownNodes = []string(“localhost:3000”)

func StartServer(nodeID, minerAddress string) {
	nodeAddress = fmt.Sprintf(“localhost:%s”, nodeID)
	miningAddress = minerAddress
	In, err := net.Listen(protocol, nodeAddress)
	defer In.Close()

	bc := NewBlockchain(nodeID)

	if nodeAddress != knownNodes[0] {
		sendVersion (knownNodes[0], bc)
	)

	for {
		conn, err := ln.Accept()
		go handleConnection(conn, bc)
	}
}

먼저 중앙 노드의 주소를 코딩한다. 모든 노드는 처음에 연결할 노드를 알아야 한다.
minerAddress 인자는 채굴 보상을 받을 주소를 지정한다.

if nodeAddress != knownNodes[0] {
	sendVersion(knownNodes[0], bc)
}

현재 노드가 중앙노드가 아니면 자신의 블록체인이 최신 데이터인지 확인하기 위해 중앙 노드에 version 메시지를 전송해야 한다.

func sendVersion(addr string, bc *Blockchain) {
	bestHeight := bc.GetBestHeight()
	payload := gobEncode(version(nodeVersion, bestHeight, nodeAddress))
	
	request := append(commandToBytes(“version”), payload...)
	sendData(addr, request)
}

메시지는 바이트의 연속이다. 첫 12 바이트는 커맨드명을 나타내며 이어지는 바이트는 gob으로 인코딩된 메시지 구조체이다. commandToBytes는 다음과 같이 구현한다.

func commandToBytes(command string) []bytes {
	var bytes [commandLength]byte

	for I, c := range command {
		bytes[i] = byte(c)
	}
	return bytes[:]
}

이 함수는 12 바이트의 버퍼를 만들어 커맨드명을 채워넣은 뒤 나머지 바이트는 빈 상태 그대로 둔다. 반대로 바이트 시퀀스를 커맨드로 변환하는 함수도 있다.

func bytesToCommand(bytes []byte) string {
	var command []byte
	
	for _, b := range bytes {
		if b != 0x0 {			command = append(command, b)
		}
	}
	return fmt.Sprintf(“%s”, command)
}

노드가 커맨드를 수신하면, bytesToCommand를 통해 커맨드 명을 가져와 적절한 핸들러로 커맨드 내용을 처리한다.

func handleConnection(conn net.Conn, bc *Blockchain) {
	request, err := ioutil.ReadAll(conn)
	command := bytesToCommand(request[:commandLengthj])
	fmt.Printf(“Received %s command\n”, command)
	
	switch command {
	...
	case “version”:
		handleVersion(request, bc)
	default:
		릇.Println(“Unknown command!”)
	}
	conn.Close()
}

version 커맨드 핸들러는 다음과 같다.

func handleVersion(request []byte, bc *Blockchain) {
        var buff bytes.Buffer
        var payload version

        buff.Write(request[commandLength:])
        dec := gob.NewDecoder(&buff)
        err := dec.Decode(&payload)

        myBestHeight := bc.GetBestHeight()
        foreignerBestHeight := payload.BestHeight

        if myBestHeight < foreignerBestHeight {
                sendGetBlocks(payload.AddrFrom)
        } else if myBestHeight > foreignerBestHeight {
                sendVersion(payload.AddrFrom, bc)
        }

        if !nodeIsKnown(payload.AddrFrom) {
                knownNodes = append(knownNodes, payload.AddrFrom)
        }
}

getblocks : 가지고 있는 블록을 보여준다는 의미

type getblocks struct {
	AddFrom string 
}
getblock을 handle하는 함수 이다.
func handleGetBlocks(request []byte, bc *Blockchain) {
        ...
        blocks := bc.GetBlockHashes()
        sendInv(payload. AddrFrom, "block", blocks)
}

inv : 현재 노드가 가진 블록 및 트랜잭션을 다른 노드에 표시한다. 말하지만, 이는 전체 블록과 전체블록과 트랜잭션이 아니라 그저 블록과 트랜잭션의 해시 값만 포함하고 있다.

type inv struct {
	AddrFrom string 
	Type string
	Items [][]byte
}
inv 처리 코드
func handleInv(request []byte, bc *Blockchain) {
        ...
        fmt.Printf("Received inventory with %d %s\n", len(payload.Items), payload.Type)

        if payload.Type == "block" {
                blocksInTransit = payload.Items

                blockHash := payload.Items[0]
                sendGetData(payload.AddrFrom, "block", blockHash)

                newInTransit := [][]byte{}
                for _, b := range blocksInTransit {
                        if bytes.Compare(b, blockHash) != 0 {
                                newInTransit = append(newInTransit, b)
                        }
                }
                blocksInTransit = newInTransit
        }

        if payload.Type == "tx" {
                txID := payload.Items[0]
                if mempool[hex.EncodeToString(txID)].ID == nil {
                        sendGetData(payload.AddrFrom, "tx", txID)
                }
        }
}

블록 해시값들을 전달받으면 다운로드한 블록을 추적하기 위해 blockInTransit 변수에 저장한다. 이렇게 하면 다른 노드에서 블록을 다운로드 할 수 있다. 블록을 전송상태로 전환한 직후에 inv 메시지를 보낸 노드에게 getdata커맨드를 전송하고 blockInTransit을 갱신한다. 실제 P2P네트워크에서는 메시지를 보낸 노드만이 아니라 서로 다른 노드에서 블록을 전송하려고 한다.

getdata

type getdata struct {
	AddrFrom string
	Type string
	ID []byte
}
getdata는 특정 블록 및 트랜잭션에 대한 요청이며, 단 하나의 블록 및 트랜잭션의 ID만을 포함할 수 있다.
func handleGetData(request []byte, bc *Blockchain) {
        ...
        if payload.Type == "block" {
                block, err := bc.GetBlock([]byte(payload.ID))
                sendBlock(payload.AddrFrom, &block)
        }

        if payload.Type == "tx" {
                txID := hex.EncodeToString(payload.ID)
                tx := mempool[txID]
                sendTx(payload.AddrFrom, &tx)
        }
}

block과 tx

type block struct {
	AddrFrom string
	Block []byte
}

type tx struct {
	AddrFrom string 
	Transaction []byte
}
block메시지의 처리
func handleBlock(request []byte, bc *Blockchain) {
        ...

        blockData := payload.Block
        block := DeserializeBlock(blockData)

        fmt.Println("Received a new block!")
        bc.AddBlock(block)

        fmt.Printf("Added block %x\n", block.Hash)

        if len(blocksInTransit) > 0 {
                blockHash := blocksInTransit[0]
                sendGetData(payload.AddrFrom, "block", blockHash)
                blocksInTransit = blocksInTransit[1:]
        } else {
                UTXOSet := UTXOSet{bc}
                UTXSOSet.Reindex()
        }
}
tx 메시지의 처리
func handleTx(request []byte, bc *Blockchain) {
    ...
    txData := payload.Transaction
    tx := DeserializeTransaction(txData)
    mempool[hex.EncodeToString(tx.ID)] = tx

    if nodeAddress == knownNodes[0] {
        for _, node := range knownNodes {
            if node != nodeAddress && node != payload.AddrFrom {
                sendInv(node, "tx", [][]byte{tx.ID})
            }
        }
    } else {
        if len(mempool) >= 2 && len(miningAddress) > 0 {
        MineTransactions:
            var txs []*Transaction

            for id := range mempool {
                tx := mempool[id]
                if bc.VerifyTransaction(&tx) {
                    txs = append(txs, &tx)
                }
            }

            if len(txs) == 0 {
                fmt.Println("All transactions are invalid! Waiting for new ones...")
                return
            }

            cbTx := NewCoinbaseTX(miningAddress, "")
            txs = append(txs, cbTx)

            newBlock := bc.MineBlock(txs)
            UTXOSet := UTXOSet{bc}
            UTXOSet.Reindex()

            fmt.Println("New block is mined!")

            for _, tx := range txs {
                txID := hex.EncodeToString(tx.ID)
                delete(mempool, txID)
            }

            for _, node := range knownNodes {
                if node != nodeAddress {
                    sendInv(node, "block", [][]byte{newBlock.Hash})
                }
            }

            if len(mempool) > 0 {
                goto MineTransactions
            }
        }
    }
}

처음에 할 일은 새로운트랜잭션을 mempool에 추가하는 것이다. 다음 코드를 살펴보자.

if nodeAddress == knownNodes[0] {
    for _, node := range knownNodes {
        if node != nodeAddress && node != payload.AddFrom {
            sendInv(node, "tx", [][]byte{tx.ID})
        }
    }
}

현재 노드가 중앙 노드인지를 확인한다. 중앙노드는 블록을 채굴하지 않는다. 대신 새로운 트랜잭션을 네트워크 상의 다른 노드들에게 전달해 준다.

if len(mempool) >= 2 && len(miningAddress) > 0 {}

miningAddress은 채굴자 노드에만 있다. 현재 노드의 mempool에 두 개 이상의 트랜잭션이 있을 때 채굴을 시작한다.

for id := range mempool {
    tx := mempool[id]
    if bc.VerifyTransaction(&tx) {
        txs = append(txs, &tx)
    }
}

if len(txs) == 0 {
    fmt.Println("All transactions are invalid! Waiting for new ones...")
    return
}

mempool의 모든 트랜잭션이 검증된다. 유효하지 않은 트랜잭션을 무시되며, 유효한 트랜잭션이 없는 경우 채굴은 중단된다.

cbTx := NewCoinbaseTX(miningAddress, "")
txs = append(txs, cbTx)

newBlock := bc.MineBlock(txs)
UTXOSet := UTXOSet{bc}
UTXOSet.Reindex()

fmt.Println("New block is mined!")

검증된 트랜잭션은 보상을 가진 코인베이스 트랜잭션과 함께 블록에 추가된다. 블록 채굴이 끝나면 UTXO집합은 재색인 된다.

for _, tx := range txs {
    txID := hex.EncodeToString(tx.ID)
    delete(mempool, txID)
}

for _, node := range knownNodes {
    if node != nodeAddress {
        sendInv(node, "block", [][]byte{newBlock.Hash})
    }
}

if len(mempool) > 0 {
    goto MineTransactions
}

트랜잭션은 채굴된 후 mempool에서 제거된다. 현재 노드가 알고 있는 모든 노드들은 새로운 블록해시가 담긴 inv메시지를 받는다. 이 노드들은 메시지를 처리한 후에 블록을 요청할 수 있다.


profile
성균관대학교 블록체인 학회 SKKRYPTO 입니다.

0개의 댓글