상세 컨텐츠

본문 제목

Go 언어를 기반으로한 블록체인 개발공부(Proof of Stake) - Part 4

Programming Language/Go

by Yongari 2023. 2. 11. 20:22

본문

 

요구되는 사전 지식과 보면 좋을 포스팅

 

지분증명에 대한 개념입니다. 

https://next-block.tistory.com/entry/%EC%A7%80%EB%B6%84%EC%A6%9D%EB%AA%85-PoS

 

지분증명 (PoS)

PoS(지분 증명 합의 알고리즘) PoS(지분 증명)은 Proof of Stake의 약자로 암호화폐를 보유하고 있는 지분율에 비례하여 채굴을 할 수 있게 해주는 합의 알고리즘입니다. 지분 증명(PoS)는 네트워크 참

next-block.tistory.com

 

https://next-block.tistory.com/entry/Go%EC%96%B8%EC%96%B4%EB%A5%BC-%EA%B8%B0%EB%B0%98%EC%9C%BC%EB%A1%9C%ED%95%9C-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EA%B3%B5%EB%B6%80

 

Go언어를 기반으로한 블록체인 개발공부 - Part1

Go 언어를 공부할겸 블록체인을 만드는 공부를 진행했습니다. 이미 한글 포스팅과 영어 포스팅이 있어서 보고 공부했으며 정리를 목적으로 포스팅합니다. 환경설정 1. go 언어 설치:https://go.dev/dl/

next-block.tistory.com

 

https://next-block.tistory.com/entry/Go%EC%96%B8%EC%96%B4%EB%A5%BC-%EA%B8%B0%EB%B0%98%EC%9C%BC%EB%A1%9C%ED%95%9C-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EA%B3%B5%EB%B6%80Network-Part2

 

Go언어를 기반으로한 블록체인 개발공부(Network) - Part2

요구되는 사전 지식 TCP : 위키 링크 HTTP : 위키 링크 Part1편에서는 기본적인 블록체인 개발을 Go로 구현했는데요 이번에는 로컬호스트를 이용해서 터미널을 다른 노드라고 생각하고 로컬 네트워

next-block.tistory.com

 

 

 

https://next-block.tistory.com/entry/Go%EC%96%B8%EC%96%B4%EB%A5%BC-%EA%B8%B0%EB%B0%98%EC%9C%BC%EB%A1%9C%ED%95%9C-%EB%B8%94%EB%A1%9D%EC%B2%B4%EC%9D%B8-%EA%B0%9C%EB%B0%9C%EA%B3%B5%EB%B6%80Power-of-Work-Part3

 

Go언어를 기반으로한 블록체인 개발공부(Power of Work) - Part3

요구되는 사전 지식과 보면 좋을 포스팅 https://next-block.tistory.com/entry/%EC%9E%91%EC%97%85%EC%A6%9D%EB%AA%85PoW 작업증명(PoW) 작업증명(PoW)의 정의 작업 증명(PoW)은 Proof of Work의 약자이며 목표값 이하의 해시

next-block.tistory.com

 

 

 


 

 

왜 POS를 쓸까요? 

 

이미 비트코인은 PoW로 개발됐는데도 불구하고 왜 이더리움은 PoW에서  PoS로 전환했을까요?

먼저 PoW는 목표값을 찾기위한 경쟁이 치열하고 하드웨어와 전력낭비가 심합니다. 

그래서 일부 국가에서는 채굴을 금지하는 국가도 있었죠. 이런 에너지  이슈가 하나 있고 또 PoW 같이 채굴을 할 때 막대한 컴퓨팅파워와 하드웨어가 필요하다면 자본가들만 참여할 수 있습니다.  블록체인의 탈중앙적이고 민주적인 철학과 맞지 않죠 

반면에 PoS는 지분으로 합의를 하기 때문에 PoW보다는 진입장벽이 높지 않습니다. 그러나 이것도 지분이 많은 노드일수록 결정권이 많아지기 때문에 탈중앙화와 맞지 않다고 비판하시는 분들도 있죠. 그러나 PoW와 비교했을 때는 좀 더 탈중앙화에 가깝다고? 할 수도 있을 것 같습니다.  이 문제도 계속 풀어야할 이슈입니다.  따라서 위의 이슈들이 문제가 되서  현재는 많은 블록체인들이 PoS를 합의 알고리즘으로 사용하고 있습니다.

 

 

실습 목표 : 

 

  • 이더리움의 채굴방식인 지분증명에 대해 학습한다. 
  • 벨리데이터의 기본 개념에 대해 공부한다. (물론 현재 블록체인은 더 고도화된 알고리즘과 코드로 동작합니다.)
  • 벨리데이터 중에서 승자가 된 벨리데이터가 어떻게 동작하는지 학습한다. 
  • 네트워크를 통해 벨리데이터 정보와 블록체인 정보를 서로 공유한다. 

 

 

pos_4.go 코드

(이해를 위해 저는 중간중간 console.log도 같이 넣었습니다.)

(잘 모르는 문법 설명까지 넣어서 코드가 많이 지저분해 보일수도 있습니다. 감안해주세요.)

 

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)

// TCP 서버에서 업데이트 된 블록체인을 보내주는 채널이다. 
// announcements broadcasts winning validator to all nodes
// make를 사용해서 초기화해줘야하는데 슬라이스와 맵과 채널의 경우 make를 이용한다. 
var announcements = make(chan string)

//데아터가 중복되는 것을 방지함 
// 상호 배제(mutual exclusion)라고도 하며 여러 스레드(고루틴)에서 공유되는 데이터를 보호할 때 주로 사용합니다.
var mutex = &sync.Mutex{}

//노드가 가지고 있는 토큰 수를 나타냄
// validators keeps track of open validators and balances
var validators = make(map[string]int)


	//문자열을 sha256 해시화하여 리턴 
	// calculateHash is a simple SHA256 hashing function
	func calculateHash(s string) string {
		h := sha256.New()
		fmt.Println("h",h)
		h.Write([]byte(s))
		hashed := h.Sum(nil)
		fmt.Println("hashed",hashed)
		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)
    }

	//새롭게 블록을 생성하는 함수 
	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
	}

	//블록의 유효성을 검증하는 함수 
	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
	}

	//connection 핸들러
	func handleConn(conn net.Conn) {
        defer conn.Close()
    
		//goroutine을 통해 announcements를 msg로 넣는다
		// 채널 값을 msg에 넣는다. 
        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)
		// NewScanner() 메서드는 os.Stdin으로부터 문자를 읽어오는 Scanner를 생성, 반환합니다. 
		// Scanner는 Scan() 메서드와 Text() 메서드를 이용해 문자를 읽고, 변수에 저장할 수 있습니다. 다음의 예제를 살펴보겠습니다.
        for scanBalance.Scan() {
			//func Atoi(s string) (i int, err error): 숫자로 이루어진 문자열을 숫자로 변환
            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", 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) {
						//블록 유효성이 검증되면 candidateBlock 채널로 새로운 블록을 보내야함 
                        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")
        }

	}

		func pickWinner() {
			//30초마다 동작
			time.Sleep(30 * time.Second)
			mutex.Lock()
			//temp에 임시 블록들 값 넣기 
			temp := tempBlocks
			mutex.Unlock()
		
			//lotteryPool 배열 생성 
			//선택된 노드의 주소를 보관하는 lotteryPool을 만들기 
			lotteryPool := []string{}

			//temp의 길이가 양수일 때 
			//실제 제출하는 새로운 블록이 있는지 확인 
			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
					// 만약 lottery 풀에 노드가 있는지 체크 있으면 반복문 나가기 
					for _, node := range lotteryPool {
						if block.Validator == node {
							continue OUTER
						}
					}
		
		
					// lock list of validators to prevent data race
					mutex.Lock()
					//벨리데이터 변수설정해서 벨리데이터 값을 넣기 
					setValidators := validators
					mutex.Unlock()
		
					//temp안에 있는 블록데이터가 신뢰할 수 있는 노드에서 생성된 것인지 확인하고 validator 안에 주소가 있는지 확인 
					k, ok := setValidators[block.Validator]
					//만약 validators 안에 주소가 있으면 lotteryPool에 추가함  
					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()
		}




		func main() {
			//env 파일 로드하기 
			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 사용 
			spew.Dump(genesisBlock)
			//블록체인에 genesis블록 삽입 
			Blockchain = append(Blockchain, genesisBlock)
		
	
			// start TCP and serve TCP server
			// tcp서버 시작 
			server, err := net.Listen("tcp", ":"+os.Getenv("ADDR"))
			if err != nil {
				log.Fatal(err)
			}
			//tcp 연결을 닫음 
			defer server.Close()
		
			// 고루틴을 시작한다. 
			// candidate 변수를 시작으로 tempBlocks에 candidate를 넣고 candidateBlocks 크기만큼  반복순회
			go func() {
				for candidate := range candidateBlocks {
					//go루틴 보호를 위해 동시시작하는 것을 보호하기 위해 설정 
					mutex.Lock()
					tempBlocks = append(tempBlocks, candidate)
					mutex.Unlock()
				}
			}()
		
	
			go func() {
				for {
					//승자 고르기 시작 
					pickWinner()
				}
			}()
		
	
			for {
				//func (l *TCPListener) Accept() (Conn, error): 클라이언트가 연결되면 TCP 연결(커넥션)을 리턴
				//값이 있으면 conn이고 없으면 err
				conn, err := server.Accept()
				//에러가 있을 경우 로그 출력 
				if err != nil {
					log.Fatal(err)
				}
				//커넥션 핸들러 함수 실행 
				go handleConn(conn)
			}
		}

 

 

 

메인서버 로그 

 

서버 실행 커맨드 

go run main.go 커맨드 실행 (메인서버)

 

노드 로그 (로컬에서 실행했으므로 터미널로 대체했습니다.) 

참고 링크:

 

한글 : 링크

영문 : 링크

 

 

관련글 더보기