Blockchain

Go로 만드는 블록체인 part 8 - Network

hou27 2022. 2. 8. 21:23

현재 구현하고 있는 암호화폐는 기본적으로 분산 네트워크에서 각각의 참여자들이 원장을 공유하며,

거래의 데이터를 합의를 통해 관리하는 중앙화 기술이다.

하나의 데이터베이스에 데이터를 저장하던 기존의 구조와 다르게 분산한 것이 가장 큰 특징이다.

 

이는 P2P 네트워크와 블록체인에 기초하며 분산 원장 기술을 사용한다.

 


분산 원장이란,

여러 사람이 액세스 할 수 있는 사이트 및 지역 등에서 합의하여 공유 및 동기화되는 데이터베이스를 뜻한다.

중앙화 되어있는 데이터베이스와 달리 탈중앙화 된 원장은 본질적으로 보안적 측면에서 더 안전하며, 합의 알고리즘을 통하여 데이터의 위변조를 방지한다.

 


 

P2P 네트워크

P2P 네트워크란 무엇일까?

P2P는 Peer to Peer를 뜻하는데, 여기서 Peer란 동등/대등한 지위로 동작하는 단위를 의미한다.

 

통신 간에 노드들은 서로 주종 혹은 Client - Server의 관계가 아니며, 중앙에 서버가 따로 존재하지 않는다.

Network Topology(통신망의 구조)는 링형, 버스형, 망형 등으로 구성할 수 있다.

 

 

사실 지금까지 작성한 코드로는 단 하나의 노드가 서버를 실행하고 코인을 거래하는,

진짜 암호화폐라고 할 수 없는 구조였다.

그렇기 때문에 우리는 마지막으로 P2P 네트워크를 간소하게 구현하여 분산된 환경을 만들어줄 것이다.

 

실제 블록체인의 네트워크처럼 서로 다른 노드들이 각자 맡은 역할을 수행하며,

기능이 정상적으로 동작할 수 있도록 해볼 것이다.

 

비트코인에서는 P2P 네트워크에서 노드를 처음 설정하게 되면 다른 노드와 연계하여 트랜잭션(Transaction)을 전달한다.

  1. DNSSEED 옵션을 사용하여 노드를 검색한다.
  2. 검색된 노드와 연결된 후 블록체인 데이터를 동기화한다.
  3. 동기화 후 거래가 생성되면 네트워크를 통해 이웃 노드에 전달한다.

Implement Server

 

Network

 

비트코인의 네트워크를 구성해보기 전에 우선 조금 더 자세히 알아보도록 하자.

노드는 서로 메시지를 통해 통신한다.

Bitcoin Network에서 사용하는 메시지의 종류를 살펴보겠다.

 

  • version - Information about program version and block count. Exchanged when first connecting.
  • verack - Sent in response to a version message to acknowledge that we are willing to connect.
  • addr - List of one or more IP addresses and ports.
  • inv - "I have these blocks/transactions: ..." Normally sent only when a new block or transaction is being relayed. This is only a list, not the actual data.
  • getdata - Request a single block or transaction by hash.
  • getblocks - Request an inv of all blocks in a range.
  • getheaders - Request a headers message containing all block headers in a range.
  • tx - Send a transaction. This is sent only in response to a getdata request.
  • block - Send a block. This is sent only in response to a getdata request.
  • headers - Send up to 2,000 block headers. Non-generators can download the headers of blocks instead of entire blocks.
  • getaddr - Request an addr message containing a bunch of known-active peers (for bootstrapping).
  • submitorder, checkorder, and reply - Used when performing an IP transaction.
  • alert - Send a network alert.
  • ping - Does nothing. Used to check that the connection is still online. A TCP error will occur if the connection has died.

다른 peer에 연결하기 위해선 version 메시지를 전송하고, 해당 peer에서는 수락하는 경우에 verack 메시지와 자신의 version 메시지를 보내준다.

getaddr 메시지와 addr 메시지를 통해 주소들을 저장하여 인식한 노드들은 보존해둔다.

tx 메시지는 트랜잭션을 보내는 데에 사용된다.

 

Message Structures

// Information about program version and block count.
// Exchanged when first connecting.
type Version struct {
	Version 	int
	BlockHeight 	int
	From		string
}

// "I have these blocks/transactions: ..."
// Normally sent only when a new block or transaction is being relayed.
// This is only a list, not the actual data.
type Inv struct {
	Type	string
	Items	[][]byte
	From	string
}

type block struct {
	From	string
	Block	[]byte
}

// Send a transaction. This is sent only in response to a getdata request.
type tx struct {
	From	string
	Tx	[]byte
}

// Request an inv of all blocks in a range.
// It isn't bringing all the blocks, but requesting a hash list of blocks.
type getblocks struct {
	From	string
	Height	int
}

// Request a single block or transaction by hash.
type getdata struct {
	From	string
	Type	string
	ID	[]byte
}

많은 메시지들이 있지만 간소하게 구현할 것이므로 위의 것들만 준비하였다.

 

그렇다면 이러한 메시지를 주고받을 노드들의 종류를 한번 살펴보자.

Full Node

풀 노드는 블록체인에서 이뤄진 모든 거래 정보를 저장하는 노드이다.

이름 그대로 모든 기능을 전부 갖춘 노드이며, 블록체인의 제네시스 블록부터 최신의 블록까지 전부 가지고 있다.

풀 노드는 기본적으로 모든 거래를 검증하고 데이터를 업데이트하는 역할을 한다.

Mining Node

채굴 노드는 새로운 블록을 채굴하는 역할을 담당한다.

이 노드는 POW(작업 증명) 방식을 사용하고 있는 블록체인에만 존재하는 노드이다.

 

많은 노드들이 있지만 우선 위 노드들을 바탕으로 우리의 암호화폐 네트워크를 구성해볼 생각이다.

 

 

구현할 시나리오를 한번 살펴보자.

Scenario

NODE
ID
3000(Full Node) 3001 3002(Mine Node)
1 createblockchain createblockchain createblockchain
2 send 3 coins - -
3 starnode db copy db copy
4 - - starnode
5 - send 3 coins sendVersion
6 - sendTx to 3000 -
7 sendInv to 3002 - -
8 - - sendGetData to 3000
9 sendTx to 3002 - -
10 - - sendInv to 3000
11 sendGetData to 3002 - -
12 - - sendBlock to 3000
13 - startnode -
14 - sendVersion -
15 sendVersion to 3001 - -
16 - sendGetBlocks to 3000 -
17 sendInv to 3001 - -
18 - sendGetData to 3000 -
19 sendBlock to 3001 - -

우선 P2P를 테스트할 수 있도록 각 노드의 구분을 port 번호로 해줄 것이다.

 

3000 노드는 중앙 노드로, 풀 노드의 역할을 할 것이다.

다른 노드들과 연결되어 데이터를 동기화해줄 것이다.

 

3002 노드는 채굴 노드로, 정말 채굴하는 역할만을 담당해줄 것이다.

 

3001 노드는 거래를 생성해줄 노드이다.

 

이 노드들을 구성하여 실제 암호화폐가 동작하듯이 시나리오를 진행할 것이다.

Function StartServer

// Starts a node
func StartServer(nodeID, minenode string) {
	nodeAddr = fmt.Sprintf(":%s", nodeID)

	if len(minenode) > 0 {
		mineNode = minenode
		fmt.Println("Now mining is on. Miner ::: ", mineNode)
	}

	// Creates servers
	ln, err := net.Listen(networkProtocol, nodeAddr)
	if err != nil {
		log.Panic(err)
	}

	// Close Listener
	defer ln.Close()

	bc := GetBlockchain(nodeID)

	if nodeAddr != nodesOnline[0] {
		sendVersion(nodesOnline[0], bc)
	}

	for {
		// Wait for connection
		conn, err := ln.Accept()
		if err != nil {
			log.Panic(err)
		}
		go handleConnection(conn, bc)
	}
}

서버를 시작하는 함수이다.

// Creates servers
ln, err := net.Listen(networkProtocol, nodeAddr)
if err != nil {
    log.Panic(err)
}

listen 메서드로 서버를 만들어주고,

if nodeAddr != nodesOnline[0] {
    sendVersion(nodesOnline[0], bc)
}

서버를 시작한 노드가 중앙 노드가 아니라면 중앙 노드에게 version 메시지를 보낸다.

var nodesOnline = []string{":3000"}

(중앙 노드는 하드코딩해두었다.)

for {
    // Wait for connection
    conn, err := ln.Accept()
    if err != nil {
        log.Panic(err)
    }
    go handleConnection(conn, bc)
}

accept는 다음 호출을 기다렸다가 connection을 반환한다.

 

반환받은 연결은 go 루틴으로 처리해준다.

Function handleConnection

func handleConnection(conn net.Conn, bc *Blockchain) {
	request, err := ioutil.ReadAll(conn)

	command := bytesToCommand(request[:commandLength])
	fmt.Printf("Received ::: %s\n", command)

	switch command {
	case "version":
		handleVersion(request, bc)
	case "inv":
		handleInv(request)
	case "getblocks":
		handleGetBlocks(request, bc)
	case "getdata":
		handleGetData(request, bc)
	case "block":
		handleBlock(request, bc)
	case "tx":
		handleTx(request, bc)
	default:
		fmt.Println("Command unknown.")
	}

	conn.Close()
}

StartServer 함수에서 넘겨준 Connection들을 컨트롤해주는 함수이다.

request, err := ioutil.ReadAll(conn)

ReadAll 메서드는 오류 또는 EOF가 발생할 때까지 conn에서 데이터를 읽어내어 반환한다.

switch command {
case "version":
    handleVersion(request, bc)
case "inv":
    handleInv(request)
case "getblocks":
    handleGetBlocks(request, bc)
case "getdata":
    handleGetData(request, bc)
case "block":
    handleBlock(request, bc)
case "tx":
    handleTx(request, bc)
default:
    fmt.Println("Command unknown.")
}

switch문에서는 변환받은 command를 통해 적절한 함수로 request를 넘겨준다.

 

다음으로 version 관련 함수들을 살펴보기 전에

block 구조체가 이전 포스트와 달라진 점을 보고 넘어가겠다.

type Block struct {
	TimeStamp		int32 `validate:"required"`
	Hash			[]byte `validate:"required"`
	PrevHash		[]byte `validate:"required"`
	Transactions		[]*Transaction `validate:"required"`
	Nonce			int `validate:"min=0"`
	Height			int `validate:"min=0"`
}

Height, 블록의 높이가 추가되었다.

 

블록의 높이란,

특정 블록에서 해당 블록보다 앞에 있는 블록의 수로 정의된다.

 

블록의 높이 정보를 통해 블록체인의 길이를 비교할 수 있기 때문에 매우 중요한 정보이다.

 

Messages - version

Function handleVersion

func handleVersion(request []byte, bc *Blockchain) {
	payload := Version{}

	dec := returnDecoder(request)
	err := dec.Decode(&payload)

	if nodeAddr == nodesOnline[0] {
		chkFlag := false
		for _, node := range nodesOnline {
			if node == payload.From {
				chkFlag = !chkFlag
			}
		}
		if !chkFlag {
			nodesOnline = append(nodesOnline, payload.From)
		}		
	}

	myBestHeight := bc.getBestHeight()
	foreignerBestHeight := payload.BlockHeight

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

 

자신의 블록체인 길이와 버전을 보낸 노드의 길이를 비교하여 다른 노드의 블록체인이 더 길다면

getblocks 메시지를 보내고, 반대라면 version 메시지를 보낸다.

if nodeAddr == nodesOnline[0] {
    chkFlag := false
    for _, node := range nodesOnline {
        if node == payload.From {
            chkFlag = !chkFlag
        }
    }
    if !chkFlag {
        nodesOnline = append(nodesOnline, payload.From)
    }		
}

위 부분은 연결됐던 노드들을 기억하기 위해 메모리에 저장하는 코드이다.

Function sendVersion

func sendVersion(dest string, bc *Blockchain) {
	bestHeight := bc.getBestHeight()
	payload := GobEncode(Version{nodeVersion, bestHeight, nodeAddr})

	request := append(commandToBytes("version"), payload...)
	sendData(dest, request)
}

sendVersion에서는 자신의 블록체인의 버전과 높이 그리고 주소를 인코딩하여 데이터를 전송하도록 하였다.

 

Messages - getblocks

Function handleGetBlocks

func handleGetBlocks(request []byte, bc *Blockchain) {
	var payload getblocks

	dec := returnDecoder(request)
	err := dec.Decode(&payload)
	if err != nil {
		log.Panic(err)
	}

	blocks := bc.GetBlockHashes(payload.Height)
	sendInv(payload.From, "blocks", blocks)
}

 

Function sendGetBlocks

func sendGetBlocks(dest string, myBestHeight int) {
	payload := GobEncode(getblocks{nodeAddr, myBestHeight})
	request := append(commandToBytes("getblocks"), payload...)

	sendData(dest, request)
}

 

 

 

 

 

Messages - inv

Function handleInv

func handleInv(request []byte) {
	var payload Inv

	dec := returnDecoder(request)
	err := dec.Decode(&payload)

	fmt.Printf("Recevied %d %s\n", len(payload.Items), payload.Type)

	if payload.Type == "blocks" {
		for _, blockHash := range payload.Items {
			sendGetData(payload.From, "block", blockHash)
		}
	} else if payload.Type == "tx" {
		txID := payload.Items[0]

		if mempool[hex.EncodeToString(txID)].ID == nil {
			sendGetData(payload.From, "tx", txID)
		}
	}
}

 

Function sendInv

func sendInv(dest, kind string, items [][]byte) {
	inven := Inv{kind, items, nodeAddr}
	payload := GobEncode(inven)
	request := append(commandToBytes("inv"), payload...)

	sendData(dest, request)
}

 

 

 

 

Messages - getdata

Function handleGetData

func handleGetData(request []byte, bc *Blockchain) {
	var payload getdata

	dec := returnDecoder(request)
	err := dec.Decode(&payload)

	if payload.Type == "block" {
		block, err := bc.GetBlock([]byte(payload.ID))

		sendBlock(payload.From, &block)
	} else if payload.Type == "tx" {
		tx := mempool[hex.EncodeToString(payload.ID)]

		sendTx(payload.From, &tx)
	}
}

 

Function sendGetData

func sendGetData(dest, kind string, id []byte) {
	payload := GobEncode(getdata{nodeAddr, kind, id})
	request := append(commandToBytes("getdata"), payload...)

	sendData(dest, request)
}

 

 

Messages - tx

Function handleTx

func handleTx(request []byte, bc *Blockchain) {
	var payload tx

	dec := returnDecoder(request)
	err := dec.Decode(&payload)

	txData := payload.Tx
	tx := DeserializeTx(txData)
	mempool[hex.EncodeToString(tx.ID)] = *tx

	if nodeAddr == nodesOnline[0] {
		for _, node := range nodesOnline {
			if node != nodeAddr && node != payload.From {
				sendInv(node, "tx", [][]byte{tx.ID})
			}
		}
	} else {
		if len(mempool) >= 2 && len(mineNode) > 0 {
		MineTxs:
			var txs []*Transaction

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

			if len(txs) == 0 {
				fmt.Println("There's no valid Transaction...")
				return
			}

			rewardTx := NewCoinbaseTX(mineNode, "Mining reward")
			txs = append(txs, rewardTx)
			newBlock := bc.MineBlock(txs)
			UTXOSet := UTXOSet{bc}
			UTXOSet.Update(newBlock)

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

			for _, node := range nodesOnline {
				if node != nodeAddr {
					sendInv(node, "blocks", [][]byte{newBlock.Hash})
				}
			}

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

 

Function sendTx

func sendTx(dest string, transaction *Transaction) {
	data := tx{nodeAddr, transaction.Serialize()}
	payload := GobEncode(data)
	request := append(commandToBytes("tx"), payload...)

	sendData(dest, request)
}

 

 

 

Messages - block

Function handleBlock

func handleBlock(request []byte, bc *Blockchain) {
	var payload block

	dec := returnDecoder(request)
	err := dec.Decode(&payload)

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

	fmt.Println("Recevied a new block")
	bc.AddBlock(block)
	UTXOSet := UTXOSet{bc}
	UTXOSet.Update(block)
}

 

Function sendBlock

func sendBlock(dest string, b *Block) {
	data := block{nodeAddr, b.Serialize()}
	payload := GobEncode(data)
	request := append(commandToBytes("block"), payload...)

	fmt.Println("Send Block ::: ", b.Hash)
	sendData(dest, request)
}

 

 

 

 

 

https://github.com/hou27/blockchain_go/tree/part8

 

GitHub - hou27/blockchain_go: Making a Cryptocurrency with GO.

Making a Cryptocurrency with GO. . Contribute to hou27/blockchain_go development by creating an account on GitHub.

github.com


참고자료

 

Bitcoin Network

bitcoin-p2p-network

what-is-blockchain

distributed ledger

비트코인 블록체인 동작원리

Bitcoin Node