[블록체인 개발 공부] 블록체인 개발하기 PART 4 : 지분증명방식 (Proof of Stake : POS)

in #kr7 years ago (edited)

CoralHealth 블로그의 https://medium.com/@mycoralhealth/code-your-own-proof-of-stake-blockchain-in-go-610cd99aa658 내용을 번역한 글입니다.

지난 포스팅에서는 Proof of Work (작업증명 방식)에 대해서 알아봤습니다.

Proof of Work의 단점은 무엇일까요?
첫번째 가장 중요한 단점은 많은 전력을 소비하는 것입니다.
예를 들어 비트코인을 얻으려면 ASIC 장비와 같은 고성능 채굴장비로 채굴하기 때문입니다.
아래 사진은 채굴을 하고 있는 작업장의 사진입니다.
fff.png

Proof of Work는 엄청난 양의 전기를 소모합니다.
비트코인 채굴은 159개국의 전기 사용량을 넘어섭니다.
이는 꽤 무책임한 방법입니다. 또한 기술적인 관점에서 다른 단점이 있습니다.
계속해서 더 많은 사람들이 채굴에 참여하게되면 알고리즘의 난이도는 올라가게 됩니다.
그렇게 되면 더 많은 전력이 필요하게 됩니다.
즉, 블록 생성과 트랜잭션처리에 더 비싼 비용이 들어간다는 의미입니다.
그래서 많은 블록체인 진영에서는 다른 방법을 찾기 시작했습니다.
그 방법은 Proof of Stake 지분증명 방식입니다. 이미 이 방식을 사용하는 진영들이 있습니다.
Nxt, 네오, 퀀텀 등이 이 방식을 이용합니다.
이더리움도 곧 이 방식으로 전환하게 됩니다.

Proof of Stake(POS : 지분 증명 방식)이란 무엇인가?

Proof of Work에서의 각 노드들 간의 경쟁 대신에 각 노드에 일정량의 토큰을 담보를 놓고 블록을 생성합니다.
이러한 노드들을 vaildator라고 부릅니다.
이번 포스팅에서는 node또는 validator라는 용어를 사용할 것입니다.
예를 들어 이더리움의 노드에 토큰을 담보로 놓습니다.
그러면 이더리움에서는 더 많은 토큰을 보유한 노드들에게 더 많은 블록을 생성할 기회를 줍니다.

Proof of Stake(POS : 지분 증명 방식)의 단점은 무엇인가?


아마도 많은 토큰을 보유한 사람들만이 많은 블록을 생성할 것이라고 추측 됩니다.
하지만 비트코인 채굴은 많은 파워 필요하기 때문에 노트북(성능이 안좋은 컴퓨터)을 가진 사람들 채굴을 할 수 없었습니다.
그러므로 많은 사람들이 고사양의 장비 없이 노트북만으로 참여할 수 있기 때문에 Proof of Stake가 더 민주적일 것이라고 추측됩니다.
기술적, 경제적 관점에서 다른 단점이 있습니다.
아쉽지만 이번 포스팅에서는 이 내용을 다룰 수 없습니다.

재밌는 소식은 이더리움의 캐스퍼 프로젝트는 Proof of Stake와 Proof of Work의 강점을 살려 이 두가지 방식을 혼용하여 사용할 예정입니다.

Proof of Statke가 어떻게 동작하는지 이해하기 위해 이제 코딩을 시작 해봅시다.

시작전에 지난 포스팅 중 part2 Networking 을 보시는 걸 추천 드립니다.

구현 절차
이제 Proof of Stake의 핵심개념을 구현 할 것입니다.
예제이기 때문에 실제 블록체인에서 사용되는 요소들은 제거하도록 하겠습니다.

  • P2P 구현, 중앙 블록체인을 TCP 서버로 올리고 네트워킹을 테스팅 할것입니다. TCP 서버에서 다른 노드들로 업데이트된 블록체인을 전송할 것입니다.

  • 지갑과 잔고 보관, 지갑은 코딩하지 않을 것입니다. 노드는 stdin(입력된) 만큼 토큰을 가집니다. 그래서 원하는 만큼 코인을 가질수 있습니다. 전체적인 구현에서 각노드의 주소와 토큰 수량을 보관할 것입니다.

구조

asdfsaf.png

  • 다른 노드로 연결할 수 있는 TCP서버를 개발할 것입니다.
  • 업데이트된 블록체인이 주기적으로 전송될 것입니다.
  • 각 노드들은 새로운 블록들은 생성한 후 서버에 제출 할 것입니다.
  • 각노드의 토큰의량 기준으르로 랜덤하게 블록을 생성할 노드를 선택합니다. 그리고 생성된 블록은 블로체인에 추가 됩니다.

환경설정 및 Imports

코드를 작성하기 전에 환경설정을 해야합니다.
TCP server를 사용하기 때문에 .env파일에 9000번 포트를 입력해 줍시다.
그리고 필요한 패키지를 import합니다.

package main
    
    import (
        "bufio"
        "crypto/sha256"
        "encoding/hex"
        "encoding/json"
        "fmt"
        "io"
        "log"
        "math/rand"
        "net"
        "os"
        "strconv"
        "sync"
        "time"
    

        "github.com/davecgh/go-spew/spew"
        "github.com/joho/godotenv"
    )

전역변수

이제 필요한 전역 변수들을 선언해봅시다.

type Block struct {
    Index     int
    Timestamp string
    BPM       int
    Hash      string
    PrevHash  string
    Validator string
}

// Blockchain is a series of validated Blocks
var Blockchain []Block
var tempBlocks []Block


// candidateBlocks handles incoming blocks for validation
var candidateBlocks = make(chan Block)


// announcements broadcasts winning validator to all nodes
var announcements = make(chan string)


var mutex = &sync.Mutex{}


// validators keeps track of open validators and balances
var validators = make(map[string]int

  • Block은 블록에 필요한 정보를 가진 구조체입니다.
  • Blockchain은 검증된 블록들을 가진 대표 블록체인 입니다. 각 블록의 PrevHash는 이전 블록의 해시와 비교하여 블록체인의 무결성을 보장합니다. tempBlocks은 블록을 생성할 수 있는 노드를 선택하기전에 블록을 보관하는 임시 저장소 입니다.
  • candidateBlock은 블록들의 채널입니다. 새로운 블록을 제출하는 각 노드들은 이 채널로 보냅니다.
  • Announcements는 TCP 서버에서 업데이트된 블록체인을 보내주는 채널입니다.
  • Mutex는 데이터가 중복되는 것을 방어해줍니다
  • Validators는 각 노드가 가지고 있는 토큰의 수를 나타냅니다.

기본적인 블록체인 함수

Proof of Stake 알고리즘을 작성하기 전에 가장 기본이 되는 함수들을 작성해 봅시다.
이전 튜토리얼 part2 networking을 확인하시면 자세하게 함수들이 하는 역할을 알 수 있습니다.

    // calculateHash is a simple SHA256 hashing function
    func calculateHash(s string) string {
        h := sha256.New()
        h.Write([]byte(s))
        hashed := h.Sum(nil)
        return hex.EncodeToString(hashed)
    }
    

    //calculateBlockHash returns the hash of all block information
    func calculateBlockHash(block Block) string {
        record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
        return calculateHash(record)
    }

calculateHash 함수는 문자열을 SHA-256 해시화 하여 리턴합니다.
calculateBlockHash는 블록안의 데이터 각 문자열들을 연결함으로써 해시화 합니다.

func generateBlock(oldBlock Block, BPM int, address string) (Block, error) {


    var newBlock Block


    t := time.Now()


    newBlock.Index = oldBlock.Index + 1
    newBlock.Timestamp = t.String()
    newBlock.BPM = BPM
    newBlock.PrevHash = oldBlock.Hash
    newBlock.Hash = calculateBlockHash(newBlock)
    newBlock.Validator = address


    return newBlock, nil
}

generateBlock함수는 새로운 블록을 생성합니다.
새롭게 생성된 블록에서 가장 중요한 필드는 이전 블록의 해시 값을 가리키는 PrevHash필드 입니다.
이 필드를 이용하여 이전 블록과 비교하여 블록체인의 무결성을 보장할 수 있습니다.
그리고 추가적으로 Validator 필드를 추가 할 것입니다. 이 필드를 통해 블록 생성에 선택된 노드를 알 수 있습니다.

func isBlockValid(newBlock, oldBlock Block) bool {
    if oldBlock.Index+1 != newBlock.Index {
        return false
    }


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


    if calculateBlockHash(newBlock) != newBlock.Hash {
        return false
    }


    return true
}

IsBlockValid함수는 블록체인의 무결성 보장을 위해 이전 블록 해시와 PrevHash를 비교하고 새로 생성한 블록의 해시를 다시 확인합니다.

노드(Validators)

노드 (Validator)가 TCP 서버에 연결될 때 몇가지 기능이 필요합니다.

  • 토큰 개수를 입력할 수 있어야 합니다.
  • 업데이트 된 블록체인을 받을수 있어야합니다.
  • 블록생성에 선택된 노드를 알 수 있어야합니다.
  • 전체 노드 리스트에 자신의 노드를 추가할 수 있어야 합니다.
  • BPM을 입력할 수 있어야 합니다.
  • 새로운 블록을 제출 할수 있어야 합니다.

handleConn함수에서 위 기능들을 코딩해 봅시다.

func handleConn(conn net.Conn) {
        defer conn.Close()
    

        go func() {
            for {
                msg := <-announcements
                io.WriteString(conn, msg)
            }
        }()
        // validator address
        var address string
    

        // allow user to allocate number of tokens to stake
        // the greater the number of tokens, the greater chance to forging a new block
        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
            }
            t := time.Now()
            address = calculateHash(t.String())
            validators[address] = balance
            fmt.Println(validators)
            break
        }
    

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

        scanBPM := bufio.NewScanner(conn)
    

        go func() {
            for {
                // take in BPM from stdin and add it to blockchain after conducting necessary validation
                for scanBPM.Scan() {
                    bpm, err := strconv.Atoi(scanBPM.Text())
                    // if malicious party tries to mutate the chain with a bad input, delete them as a validator and they lose their staked tokens
                    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()
    

                    // create newBlock for consideration to be forged
                    newBlock, err := generateBlock(oldLastIndex, bpm, address)
                    if err != nil {
                        log.Println(err)
                        continue
                    }
                    if isBlockValid(newBlock, oldLastIndex) {
                        candidateBlocks <- newBlock
                    }
                    io.WriteString(conn, "\nEnter a new BPM:")
                }
            }
        }()
    

        // simulate receiving broadcast
        for {
            time.Sleep(time.Minute)
            mutex.Lock()
            output, err := json.Marshal(Blockchain)
            mutex.Unlock()
            if err != nil {
                log.Fatal(err)
            }
            io.WriteString(conn, string(output)+"\n")
        }

먼저 Go 루틴은 TCP서버로 부터 온 선택된 노드의 주소를 받고 출력합니다. Io.WriteString(conn, "Enter token balance:") 토큰의 개수를 입력받습니다.
그리고나서 노드는 SHA256 해시 주소를 리턴합니다. 그리고 주소는 validators변수에 추가됩니다.
노드의 BPM을 입력 받을 때 블록을 처리하는 별도의 GO 루틴 생성합니다.
그리고 delete(validators, address)중요합니다.
만약에 노드가 이상한 블록을 생성한 후 제출한다면
(예를들어, BPM이 Integer형이 아닌 다른 형으로 제출 할 경우 에러를 리턴해야합니다.)
바로 노드리스트에서 제거해야합니다.
그리고 더 이상 해당 노드는 새로운 블록을 생성할수 없습니다. 그리고 나서 노드에 있는 토큰을 회수합니다.
이러한 기능 때문에 Proof of Stake는 보안적으로 안전합니다.
만약에 자신의 이익을 위하여 블록체인을 변경하려는 시도가 있으면 모든 토큰을 잃을 것입니다.
이것은 악의적인 사용자를 막을 수 있는 중요한 방법입니다.
generateBlock함수를 통해 새로운 블록을 생성하고candidateBlock채널로 보냅니다.
새로 생성된 블록을 보내려면 다음과 같이 작성해야합니다.
candidateBlock <- new Block

마지막 반복문은 주기적으로 업데이트된 블록체인을 각 노드에 알려줍니다.

블록을 생성할 수 있는 노드 선택

이제 어떻게 노드가 선택되는지 알아 봅시다. 토큰의 숫자가 많을 수록 블록을 생성하는데 선택될 가능성이 높습니다. 지금 작성하고 있는 코드에서는 새로운 블록을 제출하는 노드가 선택될 것입니다.
현재 운영되고 있는 블록체인의 지분증명 방식에서는 블록 제출 없이 선택될수 있습니다.
지금 작성하고 있는 코드가 지분 증명 방식의 표준이라고 하는 것이 아닙니다. 단지 개념일 뿐 다양한 방법들이 존재합니다 .
각 구현애는 차이가 있습니다.

여기 pickWinner 함수가 있습니다.

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


    lotteryPool := []string{}
    if len(temp) > 0 {


        // slightly modified traditional proof of stake algorithm
        // from all validators who submitted a block, weight them by the number of staked tokens
        // in traditional proof of stake, validators can participate without submitting a block to be forged
    OUTER:
        for _, block := range temp {
            // if already in lottery pool, skip
            for _, node := range lotteryPool {
                if block.Validator == node {
                    continue OUTER
                }
            }


            // lock list of validators to prevent data race
            mutex.Lock()
            setValidators := validators
            mutex.Unlock()


            k, ok := setValidators[block.Validator]
            if ok {
                for i := 0; i < k; i++ {
                    lotteryPool = append(lotteryPool, block.Validator)
                }
            }
        }


        // randomly pick winner from lottery pool
        s := rand.NewSource(time.Now().Unix())
        r := rand.New(s)
        lotteryWinner := lotteryPool[r.Intn(len(lotteryPool))]


        // add block of winner to blockchain and let all the other nodes know
        for _, block := range temp {
            if block.Validator == lotteryWinner {
                mutex.Lock()
                Blockchain = append(Blockchain, block)
                mutex.Unlock()
                for _ = range validators {
                    announcements <- "\nwinning validator: " + lotteryWinner + "\n"
                }
                break
            }
        }
    }


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

매 30초마다 블록을 생성할 노드를 선택하고 각 노드가 블록을 생성할 시간을 줍니다.
그리고나서 선택된 노드의 주소를 보관하는 loltteryPool을 만듭니다.
그리고실제 제출하는 새로운 블록이 있는지 확인하기 위해서 if len(temp) > 0를 통해 확인합니다.
외부의 반복문에서는 temp에 이미 똑 같은 블록을 가지고 있는지 확인합니다.
만약에 있다면 다음 블록으로 넘어가고 새로운 노드를 찾습니다.
k,ok := setValidators[block.Validator]으로 temp안에 있는 블록 데이터가 신뢰할수 있는 노드에서 생성된거지 확인하기 위해 validators안에 주소가 있는지 확인합니다. 만약에 존재한다면 lotteryPool에 추가합니다.

토큰의 수량에 따라 어떻게 가중치를 두는가?
lotteryPool에 선택된 노드의 주소와 토큰 갯수를 복사하여 넣습니다.
그러면 100개의 토큰을 넣은 노드는 100개를 받고 한 개의 토큰을 가지고 있는 노드는 단 한 개만 받습니다.
lotteryPool로 부터는 랜덤하게 노드를 선택하고 lotteryWinner(선택된 노드)의 주소를 돌려줍니다.
그리고나서 블록체인에 블록을 추가하고 이 syntax announcements <- “\nwinning validator: “ + lotteryWinner + “\n”와 함께 다른 노드에게 선택된 노드를 알려줍니다.
새로운 블록들이 제출 하게 되면 다시 채워질수 있기 때문에 tempBlocks의 블록을 초기화합니다.

메인함수를 작성합시다. 다음과 같이 작성합니다.

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

        // create genesis block
        t := time.Now()
        genesisBlock := Block{}
        genesisBlock = Block{0, t.String(), 0, calculateBlockHash(genesisBlock), "", ""}
        spew.Dump(genesisBlock)
        Blockchain = append(Blockchain, genesisBlock)
    

        // start TCP and serve TCP server
        server, err := net.Listen("tcp", ":"+os.Getenv("ADDR"))
        if err != nil {
            log.Fatal(err)
        }
        defer server.Close()
    

        go func() {
            for candidate := range candidateBlocks {
                mutex.Lock()
                tempBlocks = append(tempBlocks, candidate)
                mutex.Unlock()
            }
        }()
    

        go func() {
            for {
                pickWinner()
            }
        }()
    

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

실행

go run main.go를 입력하여 서버를 올려봅시다.스크린샷 2018-04-08 오후 10.54.45.png
그럼 다음과 같이 제네시스 블록이 생성된 것을 확인 할 수 있습니다.

위 예제에서는 .env파일에 9000번으로 설정 하였지만 저는 9090으로 설정하였습니다.
그리고 새로운 터미널을 실행 시켜 nc localhost 9090을 입력합니다.
스크린샷 2018-04-09 오전 12.41.11.png

그러면 메인 서버에서 노드리스트에 노드의 주소가 저장됩니다.
스크린샷 2018-04-08 오후 10.55.15.png

그리고 노드가 선택되면 노드들에게 다음과 같이 노드의 주소를 알려줍니다 그리고 업데이트된 블록체인 정보를 출력해줍니다.스크린샷 2018-04-09 오전 12.45.38.png

오늘의 포스팅은 여기 까지입니다.

다음 포스팅에서는 IPFS 프로토콜에 대해서 알아보겠습니다.

감사합니다.

Sort:  

실제 golang 으로 구현해보면서 하나하나 배워나가보려 합니다.

이전 글부터 차근차근 봐야 겠네요 ~ ^^

좋은 글 감사합니다.

!!! 힘찬 하루 보내요!