天天看點

一個基于PoS共識算法的區塊鍊執行個體解析(更新版)

作者:ReganYue

來源:恒生LIGHT雲社群

一、前言

前面我們簡單的介紹了一個基于PoS共識算法的例子,今天我們來解析一個更新版的例子。如果喜歡部落客的話,記得點贊,關注,收藏哦~

二、本例中的一些資料結構

type Block struct {
    Index     int
    TimeStamp string
    BPM       int
    HashCode  string
    PrevHash  string
    Validator string
}

var Blockchain []Block
var tempBlocks []Block

var candidateBlocks = make(chan Block)

var announcements = make(chan string)

var validators = make(map[string]int)
           

首先是定義了一個區塊結構體Block,然後定義一條區塊鍊Blockchain,其實就是區塊數組。這個tempBlocks是區塊緩沖區。candidateBlocks是候選區塊,任何一個節點提議一個新塊時,都會将它發送到這個管道。announcements是來廣播的通道。validators是驗證者清單,存節點位址和他擁有的tokens。

三、生成區塊和計算哈希

func generateBlock(oldBlock Block, BPM int, address string) Block {
    var newBlock Block
    newBlock.Index = oldBlock.Index + 1
    newBlock.TimeStamp = time.Now().String()
    newBlock.BPM = BPM
    newBlock.PrevHash = oldBlock.HashCode
    newBlock.Validator = address
    newBlock.HashCode = GenerateHashValue(newBlock)
    return newBlock
}

func GenerateHashValue(block Block) string {
    var hashcode = block.PrevHash +
        block.TimeStamp + block.Validator +
        strconv.Itoa(block.BPM) + strconv.Itoa(block.Index)
    return calculateHash(hashcode)
}

func calculateHash(s string) string {
    var sha = sha256.New()
    sha.Write([]byte(s))
    hashed := sha.Sum(nil)
    return hex.EncodeToString(hashed)
}
           

這個真的前面每個例子都在講,這裡真的不想再講了,不了解的小夥伴可以看一看本專欄前面的例子。

四、主邏輯

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal(err)
    }

    genesisBlock := Block{}
    genesisBlock = Block{0, time.Now().String(), 0,
        GenerateHashValue(genesisBlock), "", ""}
    spew.Dump(genesisBlock)

    Blockchain = append(Blockchain, genesisBlock)

    port := os.Getenv("PORT")

    server, err := net.Listen("tcp", ":"+port)
    if err != nil {
        log.Fatal(err)
    }

    log.Println("HTTP Server Listening on port :", port)

    defer server.Close()

    go func() {
        for cadidate := range candidateBlocks {

            mutex.Lock()

            tempBlocks = append(tempBlocks, cadidate)
            mutex.Unlock()
        }
    }()

    go func() {
        for {

            pickWinner()
        }
    }()

    for {
        conn, err := server.Accept()
        if err != nil {
            log.Fatal(err)
        }
        go handleConn(conn)
    }
}
           

我們先來看一看主邏輯,先是加載本地的.env檔案,這個檔案可以存儲很多參數,這裡我們存儲一個端口号9000.

一個基于PoS共識算法的區塊鍊執行個體解析(更新版)

然後是建立創世區塊,創世區塊注意它的高度為0.

spew.Dump(genesisBlock)
           

就是把創世區塊通過指令行格式化輸出。

Blockchain = append(Blockchain, genesisBlock)
           

這行代碼是将創世區塊添加到區塊鍊。

port := os.Getenv("PORT")
           

前面說.env檔案中存儲了端口号,這裡就擷取這個檔案中的端口号到port變量中。

然後啟動服務程序監聽上面擷取的端口。

defer server.Close()
           

要養成啟動服務就書寫延遲關閉的習慣,不然後面任意忘記釋放資源。

**然後是并發操作,循環讀取candidateBlocks,一旦這個管道有一個區塊進入,馬上把它讀取到緩沖區。

然後不斷接收驗證者節點的連接配接,連上就處理終端發送過來的資訊。

五、擷取記賬權的節點

func pickWinner() {
    time.Sleep(30 * time.Second)
    mutex.Lock()
    temp := tempBlocks
    mutex.Unlock()

    lotteryPool := []string{}
    if len(temp) > 0 {
    OUTER:
        for _, block := range temp {
            for _, node := range lotteryPool {
                if block.Validator == node {
                    continue OUTER
                }
            }


            mutex.Lock()

            setValidators := validators
            mutex.Unlock()

            k, ok := setValidators[block.Validator]
            if ok {

                for i := 0; i < k; i++ {
                    lotteryPool = append(lotteryPool, block.Validator)
                }
            }
        }

        s := rand.NewSource(time.Now().Unix())
        r := rand.New(s)

        lotteryWinner := lotteryPool[r.Intn(len(lotteryPool))]

        for _, block := range temp {
            if block.Validator == lotteryWinner {
                mutex.Lock()
                Blockchain = append(Blockchain, block)
                mutex.Unlock()
                for _ = range validators {
                    announcements <- "\nvalidator:" + lotteryWinner + "\n"
                }
                break
            }
        }

    }
    mutex.Lock()
    tempBlocks = []Block{}
    mutex.Unlock()
}
           

這裡就是PoS的精髓,根據代币tokens數量來确定擁有記賬權的節點。

先是每次選出擁有記賬權的節點就得休息30秒,不能一直不停的選吧。

每次選擁有記賬權的節點之前,将緩沖區的區塊拷貝一份部分,然後操作副本。

我們先聲明一個彩票池來放置驗證者位址。

然後判斷緩沖區是否為空,如果緩沖區副本不為空,就周遊緩沖區副本,然後如果區塊的驗證者在彩票池就繼續周遊,如果不在就執行後面的内容。

然後是擷取一個驗證者清單副本,擷取上面不在彩票池中的驗證者節點的token代币數量,然後向彩票池中添加和代币數量一樣多的驗證者位址字元串放入彩票池。

彩票池填充完畢後,就開始選幸運兒了。通過随機數來選取,然後将獲勝者的區塊加到區塊鍊上面,再廣播這個獲勝者的區塊消息。

如果臨時緩沖區為空,我們就将讓他等于一個空區塊。

六、處理指令行的請求

func handleConn(conn net.Conn) {

	defer conn.Close()

	go func() {
		for {
			msg := <-announcements
			io.WriteString(conn, msg)
		}
	}()

	var address string

	io.WriteString(conn, "Enter token balance:")

	scanBalance := bufio.NewScanner(conn)
	for scanBalance.Scan() {

		balance, err := strconv.Atoi(scanBalance.Text())
		if err != nil {
			log.Printf("%v not a number: %v", scanBalance.Text(), err)
			return
		}

		address = calculateHash(time.Now().String())

		validators[address] = balance
		fmt.Println(validators)
		break
	}

	io.WriteString(conn, "\nEnter a new BPM:")

	scanBPM := bufio.NewScanner(conn)
	go func() {

		for {
			for scanBPM.Scan() {
				bmp, err := strconv.Atoi(scanBPM.Text())
				if err != nil {
					log.Printf("%v not a number: %v", scanBPM.Text(), err)
		
					delete(validators, address)
					conn.Close()
				}
		
				mutex.Lock()
				oldLastIndex := Blockchain[len(Blockchain)-1]
				mutex.Unlock()

	
				newBlock := generateBlock(oldLastIndex, bmp, address)
				if err != nil {
					log.Println(err)
					continue
				}

				if isBlockValid(newBlock, oldLastIndex) {
			
					candidateBlocks <- newBlock
				}
			}
		}
	}()


	for {
		time.Sleep(time.Second * 20)
		mutex.Lock()

		output, err := json.Marshal(Blockchain)
		mutex.Unlock()
		if err != nil {
			log.Fatal(err)
		}

		io.WriteString(conn, string(output)+"\n")
	}

}

func isBlockValid(newBlock, oldBlock Block) bool {

	if oldBlock.Index+1 != newBlock.Index {
		return false
	}

	if oldBlock.HashCode != newBlock.PrevHash {
		return false
	}

	if GenerateHashValue(newBlock) != newBlock.HashCode {
		return false
	}
	return true
}
           

先是延時釋放連接配接資源。

defer conn.Close()
           

然後從管道中讀取選出幸運兒的消息,并将其輸出到連接配接conn。

然後在指令行視窗接收該節點的tokens數量。

然後根據目前時間生成驗證者的位址。

address = calculateHash(time.Now().String())
           

再将驗證者位址和他擁有的tokens存到validators中。

然後再根據提示輸入交易資訊。如果輸入的交易資訊非法,就将該節點删除。

delete(validators, address)
conn.Close()
           

之後的邏輯是取上一個區塊,然後生成新的區塊資訊,然後簡單的驗證區塊是否合法,合法的話就将區塊放入candidateBlocks管道,等待抽取幸運兒。

此處驗證區塊是否合法的方法很簡單,就是驗證目前區塊的高度是不是上一個子產品加一,然後判斷新區塊的PrevHash是不是等于上一個區塊的哈希值。然後再一次檢驗哈希值是否正确。

七、運作結果