iT邦幫忙

2023 iThome 鐵人賽

DAY 13
0
Web 3

從 區塊鏈 到 去中心化應用程式(DApp)系列 第 13

區塊鏈建立: 連線協定 與 初始化設定

  • 分享至 

  • xImage
  •  

連線協定 與 初始化設定

專案 GitHub 位置: https://github.com/weiawesome/block-chain_go

首先 區塊鏈網路 是 點對點(P2P)

所以這次協議設計
打算採用 基於 TCP 之上

至於初始化的設定
包括 區塊 與 UTXO 的資料初始化

因此這篇主要以幾點敘述

  1. 節點的連接
  2. 節點的建立
  3. 節點的傳輸與接收
  4. 初始化節點資訊

節點的連接

首先在建立節點監聽前
可以先決定連線的節點(已存在區塊鏈網上的節點)

檔案位置: "./protocal/connection/node_build.go"

func BuildNode(NodeAddr string, NodeAddresses []string, TransactionChannel chan transaction.Transaction, BlockChannel chan blockchain.Block, BroadcastTransactionChannel chan transaction.Transaction, BroadcastBlockChannel chan blockchain.Block) {
        ConnectionChannel := make(chan net.Conn)
	go CommunicateClient(ConnectionChannel, BroadcastTransactionChannel, BroadcastBlockChannel)
    
	for _, addr := range NodeAddresses {
		conn, err := net.Dial("tcp", addr)
		if err != nil {
			continue
		}
		go InitBlocks(conn)
		ConnectionChannel <- conn
	}
}
  1. 先建立與連接點溝通的通道(Channel)
  2. 在建立與客戶溝通的 Goroutine
  3. 按照順序與每個節點進行連接
  4. 進行連線 與 初始化區塊
  5. 將連線加入連線者的通道
  6. 繼續連接其他節點
備註:
Goroutine是Go語言中的輕量級執行緒,用於並行編程。

與傳統執行緒(Threads)相比,Goroutine更加高效,
可輕鬆創建數千個,而不會消耗大量記憶體。

每個Goroutine都由Go運行時管理,
具有獨立的執行流程,但共享相同的記憶體空間。

透過通道(Channel)進行Goroutine之間的通訊,實現數據同步。

節點的建立

接下來要建立節點 可以接收其他節點的連線

每當新的連線出現 便開啟一個 Goroutine 處理相關連線

檔案位置: "./protocal/connection/node_build.go"

func BuildNode(NodeAddr string, NodeAddresses []string, TransactionChannel chan transaction.Transaction, BlockChannel chan blockchain.Block, BroadcastTransactionChannel chan transaction.Transaction, BroadcastBlockChannel chan blockchain.Block) {
	listener, err := net.Listen("tcp", NodeAddr)
	if err != nil {
		panic(err)
		return
	}
	defer func(listener net.Listener) {
		err := listener.Close()
		if err != nil {
			panic(err)
		}
	}(listener)

	for {
		conn, err := listener.Accept()
		if err != nil {
			utils.LogError(err.Error())
			continue
		}
		go ReceiveReplyClient(conn, TransactionChannel, BlockChannel)
		ConnectionChannel <- conn
	}
}
  1. 首先便是先建立tcp監聽
  2. 在程序結束時關閉監聽
  3. 如果有新的連線者 便開啟一個ReceiveReplyClient()

節點的傳輸與接收

對於節點間通訊內容包括

  1. 廣播交易
  2. 廣播區塊
  3. 初始化區塊的詢問與接收

最後則是 相互溝通的函式

  • 主動傳輸
  • 接收與回覆

廣播交易 與 廣播區塊

檔案位置: "./protocal/connection/content.go"

type BroadcastTransaction struct {
	Transaction transaction.Transaction
}

type BroadcastBlock struct {
	Block blockchain.Block `json:"block"`
}

基本上 就是傳輸出去的內容必須包括 區塊 或是 交易

初始化區塊的詢問與接收

檔案位置: "./protocal/connection/content.go"

const InitQuery = "InitQuery"

type InitBlock struct {
	BlockHash string `json:"blockHash"`
}
type ReturnBlock struct {
	Block blockchain.Block `json:"block"`
}

想法是逐一詢問
舊有節點透過給新節點 最後一區塊 再不斷往前訊問

InitBlock 過去詢問的區塊
ReturnBlock 回傳的區塊資訊

傳送資訊 與 錯誤處理

檔案位置: "./protocal/connection/content.go"

func SendContent(conn net.Conn, val string) {
	responseBytes := []byte(string(val) + SuffixString)

	_, err := conn.Write(responseBytes)
	if err != nil {
		utils.LogError(err.Error())
		return
	}
}

type ErrorMessage struct {
	Error string `json:"error"`
}

func SentErrorMessage(conn net.Conn, message string) {
	response, err := json.Marshal(ErrorMessage{Error: message})
	if err != nil {
		utils.LogError(err.Error())
	}
	SendContent(conn, string(response))
}

檔案位置: "./protocal/connection/parameters.go"

const (
	SuffixString = "\r\nEOF\r\n"
	BufferSize   = 1024 * 10
)

基本上 就是透過 制式化的方式 傳輸資訊

主動傳輸 與 接收回覆

檔案位置: "./protocal/connection/node_build.go"

func ReceiveReplyClient(conn net.Conn, transactionChannel chan transaction.Transaction, blockChannel chan blockchain.Block) {
	defer func(conn net.Conn) {
		err := conn.Close()
		if err != nil {
			panic(err)
		}
	}(conn)

	buffer := make([]byte, BufferSize)

	for {
		totalRequest := ""
		for {
			n, err := conn.Read(buffer)
			if err != nil {
				utils.LogError(err.Error())
				break
			}
			request := string(buffer[n:])
			totalRequest += request
			if strings.HasSuffix(totalRequest, SuffixString) {
				totalRequest = totalRequest[:len(totalRequest)-len(SuffixString)]
				break
			}
		}

		var data interface{}
		err := json.Unmarshal([]byte(totalRequest), &data)
		if err != nil {
			utils.LogError(err.Error())
			return
		}
		switch v := data.(type) {
		case BroadcastTransaction:
			transactionChannel <- v.Transaction
		case BroadcastBlock:
			blockChannel <- v.Block
		case InitBlock:
			if v.BlockHash == InitQuery {
				lastBlock, err := block_control.GetLastBlock()
				if err != nil {
					SentErrorMessage(conn, "Error to get last block")
					utils.LogError(err.Error())
					continue
				}
				Block, err := block.GetBlock(lastBlock)
				if err != nil {
					SentErrorMessage(conn, "Error to get block")
					utils.LogError(err.Error())
					continue
				}
				response, err := json.Marshal(ReturnBlock{Block: Block})
				if err != nil {
					SentErrorMessage(conn, "Error to send block")
					utils.LogError(err.Error())
					continue
				}
				SendContent(conn, string(response))
			} else {
				Block, err := block.GetBlock(v.BlockHash)
				if err != nil {
					SentErrorMessage(conn, "Error to get block")
					utils.LogError(err.Error())
					continue
				}
				response, err := json.Marshal(ReturnBlock{Block: Block})
				if err != nil {
					SentErrorMessage(conn, "Error to send block")
					utils.LogError(err.Error())
					continue
				}
				SendContent(conn, string(response))
			}
		case ErrorMessage:
			utils.LogError(v.Error)
		default:
			SentErrorMessage(conn, "Unknown request")
		}
	}
}

func CommunicateClient(ConnectionChannel chan net.Conn, BroadcastTransactionChannel chan transaction.Transaction, BroadcastBlockChannel chan blockchain.Block) {
	var connections []net.Conn
	for {
		select {
		case val := <-ConnectionChannel:
			connections = append(connections, val)
		case b := <-BroadcastBlockChannel:
			response, err := json.Marshal(BroadcastBlock{Block: b})
			fmt.Println(b.BlockHash)
			for _, connection := range connections {
				if err != nil {
					SentErrorMessage(connection, "Error to send block")
					utils.LogError(err.Error())
					continue
				}
				SendContent(connection, string(response))
			}
		case t := <-BroadcastTransactionChannel:
			response, err := json.Marshal(BroadcastTransaction{Transaction: t})
			for _, connection := range connections {
				if err != nil {
					SentErrorMessage(connection, "Error to send transaction")
					utils.LogError(err.Error())
					continue
				}
				SendContent(connection, string(response))
			}
		default:
			continue
		}

	}
}

ReceiveReplyClient
此處為接收相鄰節點的資訊 並做處理或回覆

CommunicateClient
此處主要是主動傳送訊息給連接的節點

初始化節點資訊

連接節點後就要開始去更新區塊資料庫
收到區塊時同時要建立 UTXO 資料庫內容

接收與更新 區塊 資料庫

檔案位置: "./protocal/connection/node_build.go"

func InitBlocks(conn net.Conn) {
	flag := false
	request, err := json.Marshal(InitBlock{BlockHash: InitQuery})
	if err != nil {
		utils.LogError(err.Error())
		return
	}
	SendContent(conn, string(request))
	for {
		totalResponse := ""
		buffer := make([]byte, BufferSize)
		for {
			n, err := conn.Read(buffer)
			if err != nil {
				utils.LogError(err.Error())
				break
			}
			response := string(buffer[n:])
			totalResponse += response
			if strings.HasSuffix(totalResponse, SuffixString) {
				totalResponse = totalResponse[:len(totalResponse)-len(SuffixString)]
				break
			}
		}

		var data interface{}
		err := json.Unmarshal([]byte(totalResponse), &data)
		if err != nil {
			utils.LogError(err.Error())
			return
		}
		switch v := data.(type) {
		case ReturnBlock:
			if err := block.SetBlock(v.Block); err == nil {
				err := initalize.BuildUTXO(v.Block)
				if err != nil {
					return
				}
				if v.Block.BlockTop.PreviousHash == conseous.GenesisBlockPreviousHash {
					return
				}
				request, err := json.Marshal(InitBlock{BlockHash: v.Block.BlockTop.PreviousHash})
				if err != nil {
					utils.LogError(err.Error())
					return
				}
				SendContent(conn, string(request))
				if !flag {
					err := block_control.SetLastBlock(v.Block.BlockHash)
					if err != nil {
						return
					}
				}
				flag = true
			} else {
				return
			}
		default:
			SentErrorMessage(conn, "Unknown Request")
			return
		}
	}
}

建立 UTXO 資料庫

檔案位置: "./protocal/initalize/build_utxo.go"

func BuildUTXO(Block blockchain.Block) error {
	for i, transaction := range Block.BlockTransactions {
		if i == 0 {
			continue
		}
		err := utxo.SetUTXO(transaction)
		if err != nil {
			return err
		}
	}
	return nil
}

主要就是透過 傳遞過來的區塊內容新增 UTXO

結言

整體來說 建立連線並不困難

困難點 在於溝通格式的決定

希望透過這篇能夠理解

  1. 節點如何連線
  2. 節點如何接收連線
  3. 節點溝通內容都有哪些

下回預告

經過節點的建立
現在已經能與其他節點溝通了

不過對於實際區塊運算現在才剛開始
礦工對於交易從節點接收後

如何處理 驗證 運算 選擇

以交易來說 當然先挑手續費高的來做啊
對礦工來說

那究竟要如何實現呢?

下回 "區塊鏈建立: 區塊運算 之 交易的流向"


上一篇
區塊鏈建立: 資料庫的建置與規劃
下一篇
區塊鏈建立: 區塊運算 之 交易的流向
系列文
從 區塊鏈 到 去中心化應用程式(DApp)30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言