상세 컨텐츠

본문 제목

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

Programming Language/Go

by Yongari 2023. 2. 6. 22:49

본문

 

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

 

환경설정

 

1. go 언어 설치:https://go.dev/dl/

 

Downloads - The Go Programming Language

Downloads After downloading a binary release suitable for your system, please follow the installation instructions. If you are building from source, follow the source installation instructions. See the release history for more information about Go releases

go.dev

2. Spew 설치 (go의 data 구조와 디버깅을 할 때 유용하게 쓸 수 있습니다.)

go get github.com/davecgh/go-spew/spew


3. Gorilla/mux 설치 (웹 핸들러를 이용하기 위한 패키지입니다.)

go get github.com/gorilla/mux


4. Gotdotenv (go에서 .env 파일을 읽을 때 사용하는 패키지입니다.)

go get github.com/joho/godotenv

 

5. vi .env 파일
(vi로 .env 파일을 만든 후 다음 내용을 넣고 저장해주세요.)
8080을 이미 쓰고 있으면 다른 포트를 써주세요. 저는 이미 쓰고 있어서 8888로 했습니다. 
포트 확인 커맨드 "netstat -tnlp | grep "Port"

vi .env 

ADDR=8888

 

GO언어를 기반으로 하는 블록체인 코드 실습

 

A. 환경설정 코드

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"time"

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

 

B. 블록체인을 구성하기 위한 구조체를 정의

//블록체인을 구성하기 위한 구조체를 정의
type Block struct {
	Index int  // Index는 블록체인에서 데이터 레코드의 위치를 나타냅니다.
	Timestamp string  // Timestamp 는 데이터가 작성될때의 시간이 작성되며 자동으로 결정됩니다.
	BPM int //pulse rate // BPM 또는 beats per minute, 이것은 pulse rate을 의미합니다.
	Hash string // Hash 는 SHA256를 이용하며 이 데이타 레코드의 식별을 하는데 사용됩니다.
	PrevHash string // PrevHash 이전 데이터 레코드의 Hash를 의미합니다.
}

 

 

C. 블록체인 변수 설정과 SHA256 해시를 생성하는 함수 만들기

var Blockchain []Block // Blockchain 변수 설정

//블록에 대한 SHA256 해시를 생성하는 함수 만들기
func calculateHash(block Block) string {
	record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash //record 변수에 블록 인덱스+ 블록.timestamp + 블록.BPM + 블록.prevhash(앞에있는 해쉬) 연결해서 만듬 
	h := sha256.New() //해시함수를 새로 생성
	h.Write([]byte(record)) //레코드를 SHA256으로 해시화한 후 문자열 형태의 해시 값으로 반환함
	hashed := h.Sum(nil) //그리고 h값을 sum으로 전부 더한다? 
	return hex.EncodeToString(hashed) // hashed 최종값을 encode해서 hex값으로 리턴한다?
}

 

 

D. 블록을 생성하는 함수 만들기

//블록 생성하는 함수
func generateBlock(oldBlock Block, BPM int)(Block, error){
	var newBlock Block 

	t := time.Now()

	newBlock.Index = oldBlock.Index + 1 //올드블록 인덱스에서 +1
	newBlock.Timestamp = t.String() //시간을 문자열로 
	newBlock.BPM = BPM 	//bpm 속성 설정 
	newBlock.PrevHash = oldBlock.Hash //올드블록의 해쉬 
	newBlock.Hash = calculateHash(newBlock) //hash계산하는 함수에 newBlock 인자를 넣고 호출 

	return newBlock, nil //return newBlock, newBlock 값이 없을 때 할당되는 초기값으로 생각됨

// 	- zero value는 명시적인 초기값을 할당하지 않고 변수를 만들었을 때 해당 변수가 갖게 되는 값이다.
// - nil은 포인터, 인터페이스, 맵, 슬라이스, 채널, 함수의 zero value이다. 

}

 

 

E. 블록의 유효성을 검사하는 함수 만들기

func isBlockValid(newBlock, oldBlock Block) bool {
	//올드블록 인덱스+1한 값과 뉴블록 인덱스가 같지 않으면 false를 반환
	if oldBlock.Index+1 != newBlock.Index{
		return false 
	}
	
	//올드블록 해쉬값과 뉴블록의 이전 해쉬값이 같지 않으면 false를 반환 
	if oldBlock.Hash != newBlock.PrevHash {
		return false 
	}
	
	//뉴블록을 calculateHash값에 넣어서 호출한 값과 뉴블록 해시값이 일치하지 않으면 false를 반환 
	if calculateHash(newBlock) != newBlock.Hash{
		return false 
	}
	
	//위 3가지 경우의 수를 제외한 경우에는 true를 반환하기 
	return true 
}

 

F. 블록체인의 분기에 대한 함수

//블록체인 분기에 대한 함수
//새로운 블록의 길이가 기존 블록체인의 길이보다 길다면 
//블록체인 변수에 새로운 길이의 블록을 변수로 넣어주기 
func replaceChain(newBlocks []Block){
	if len(newBlocks) > len(Blockchain){
		Blockchain = newBlocks
	}
}

 

 

G. run 함수를 통한 웹 서버 올리기 - ㅡ 블록체인 확인용 

//gorilla/mux 패키지를 이용해서 run함수를 통해 웹서버 올리기
//.env 파일에서  8888포트를 가져옴 
func run() error {
	mux := makeMuxRouter()
	//mux := mux.NewRouter()
	httpAddr := os.Getenv("ADDR")
	log.Println("Listening on ", os.Getenv("ADDR"))
	
	s:= &http.Server{
		Addr: ":" + httpAddr,
		Handler: mux,
		ReadTimeout: 10 * time.Second,
		WriteTimeout: 10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	
	if err := s.ListenAndServe(); err != nil{
		return err 
	}
	
	return nil 
}

 

H. 라우터를 정의하기 위한 함수

 

//모든 핸들러를 정의하기 위한 함수 
func makeMuxRouter() http.Handler{
	muxRouter := mux.NewRouter()
	muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
	muxRouter.HandleFunc("/",handleWriteBlock).Methods("POST")
	return muxRouter 
}

 

I. get 핸들러를 통해 서버에 요청을 보내면 블록체인을 볼 수 있다.

//get 핸들러를 통해 서버에 요청을 보내면 브라우저를 통해 블록체인을 볼 수 있음?
func handleGetBlockchain(w http.ResponseWriter, r *http.Request){
	bytes, err := json.MarshalIndent(Blockchain, "", " ")
	//에러가 nil값이 아니면 즉 에러가 있으면 다음 값들을 리턴 
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return 
	}
	//string을 쓰는 함수? 
	io.WriteString(w, string(bytes))
}

 

J. BPM과 블록생성하는 block write 핸들러 함수 생성

//POST request는 BPM을 담은 구조체가 필요함
//그래서 다음과 같이 구조체로 BPM을 int로 가지는 Message를 선언함
type Message struct {
	BPM int 
}



func handleWriteBlock(w http.ResponseWriter, r *http.Request){
	var m Message 
	//request body의 decord 값
	decoder := json.NewDecoder(r.Body)

	//decorder값을 디코딩했을 때 에러로 설정 이후 JSON으로 리스폰스 반환하는 값 설정 
	if err := decoder.Decode(&m); err != nil{
		respondWithJSON(w,r, http.StatusBadRequest, r.Body)
		return 
	}

	defer r.Body.Close()

	//뉴블록에 제너레이트 블록값 설정 
	newBlock, err := generateBlock(Blockchain[ len(Blockchain) -1  ], m.BPM)
	
	//에러 처리 
	if err != nil {
		respondWithJSON(w, r, http.StatusInternalServerError,m)
		return 
	}

	//블록유효성 검사, true일경우 
	if isBlockValid(newBlock, Blockchain[ len(Blockchain) -1 ]){

		//뉴블록체인은 블록체인에 뉴블록을 삽입하는 것
		newBlockchain := append(Blockchain, newBlock)
		replaceChain(newBlockchain)
		//spew.Dump는 콘솔에 찍어주기위한 함수임, 디버그를 위한 함수
		spew.Dump(Blockchain)
	}

	respondWithJSON(w,r,http.StatusCreated, newBlock)

}

 

K. response를 json으로 하기 위한 함수

func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{} ){
	//response 값에 json.
	response, err := json.MarshalIndent(payload, "", " ")
	
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("HTTP 500: Internal Server Error"))
		return 
	}
	//헤더에 코드를 삽입하고
	w.WriteHeader(code)
	//리스폰스를 삽입함
	w.Write(response)
}

 

L. Main func 함수 만들기

 

func main(){
	//env에 있는 설정을 로드한다. 포트를 가져오기 
	err := godotenv.Load()
    
	//에러가 있으면 로그출력
	if err != nil{
		log.Fatal(err)
	}

	go func(){
		//현재시간 생성
		t := time.Now()
        
		//제네시스블록 선언 
		genesisBlock := Block{0, t.String(), 0, "", ""}
        
		//디버깅을 위한 spew.Dump 함수실행 
		spew.Dump(genesisBlock)
        
		//블록체인 변수에 제네시스 변수 추가 
		Blockchain = append(Blockchain, genesisBlock)
	}()

	log.Fatal(run())
}

 

Go 전체 소스

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"time"

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


//블록체인을 구성하기 위한 구조체를 정의
type Block struct {
	Index int  // Index는 블록체인에서 데이터 레코드의 위치를 나타냅니다.
	Timestamp string  // Timestamp 는 데이터가 작성될때의 시간이 작성되며 자동으로 결정됩니다.
	BPM int //pulse rate // BPM 또는 beats per minute, 이것은 pulse rate을 의미합니다.
	Hash string // Hash 는 SHA256를 이용하며 이 데이타 레코드의 식별을 하는데 사용됩니다.
	PrevHash string // PrevHash 이전 데이터 레코드의 Hash를 의미합니다.
} 

var Blockchain []Block // Blockchain 변수 설정

//블록에 대한 SHA256 해시를 생성하는 함수 만들기
func calculateHash(block Block) string {
	record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash //record 변수에 블록 인덱스+ 블록.timestamp + 블록.BPM + 블록.prevhash(앞에있는 해쉬) 연결해서 만듬 
	h := sha256.New() //해시함수를 새로 생성
	h.Write([]byte(record)) //레코드를 SHA256으로 해시화한 후 문자열 형태의 해시 값으로 반환함
	hashed := h.Sum(nil) //그리고 h값을 sum으로 전부 더한다? 
	return hex.EncodeToString(hashed) // hashed 최종값을 encode해서 hex값으로 리턴한다?
}

//블록 생성하는 함수
func generateBlock(oldBlock Block, BPM int)(Block, error){
	var newBlock Block 

	t := time.Now()

	newBlock.Index = oldBlock.Index + 1 //올드블록 인덱스에서 +1
	newBlock.Timestamp = t.String() //시간을 문자열로 
	newBlock.BPM = BPM 	//bpm 속성 설정 
	newBlock.PrevHash = oldBlock.Hash //올드블록의 해쉬 
	newBlock.Hash = calculateHash(newBlock) //hash계산하는 함수에 newBlock 인자를 넣고 호출 

	return newBlock, nil //return newBlock, newBlock 값이 없을 때 할당되는 초기값으로 생각됨

// 	- zero value는 명시적인 초기값을 할당하지 않고 변수를 만들었을 때 해당 변수가 갖게 되는 값이다.
// - nil은 포인터, 인터페이스, 맵, 슬라이스, 채널, 함수의 zero value이다. 

}
func isBlockValid(newBlock, oldBlock Block) bool {
	//올드블록 인덱스+1한 값과 뉴블록 인덱스가 같지 않으면 false를 반환
	if oldBlock.Index+1 != newBlock.Index{
		return false 
	}
	
	//올드블록 해쉬값과 뉴블록의 이전 해쉬값이 같지 않으면 false를 반환 
	if oldBlock.Hash != newBlock.PrevHash {
		return false 
	}
	
	//뉴블록을 calculateHash값에 넣어서 호출한 값과 뉴블록 해시값이 일치하지 않으면 false를 반환 
	if calculateHash(newBlock) != newBlock.Hash{
		return false 
	}
	
	//위 3가지 경우의 수를 제외한 경우에는 true를 반환하기 
	return true 
}

//블록체인 분기에 대한 함수
//새로운 블록의 길이가 기존 블록체인의 길이보다 길다면 
//블록체인 변수에 새로운 길이의 블록을 변수로 넣어주기 
func replaceChain(newBlocks []Block){
	if len(newBlocks) > len(Blockchain){
		Blockchain = newBlocks
	}
}

//gorilla/mux 패키지를 이용해서 run함수를 통해 웹서버 올리기
//.env 파일에서  8888포트를 가져옴 
func run() error {
	mux := makeMuxRouter()
	//mux := mux.NewRouter()
	httpAddr := os.Getenv("ADDR")
	log.Println("Listening on ", os.Getenv("ADDR"))
	
	s:= &http.Server{
		Addr: ":" + httpAddr,
		Handler: mux,
		ReadTimeout: 10 * time.Second,
		WriteTimeout: 10 * time.Second,
		MaxHeaderBytes: 1 << 20,
	}
	
	if err := s.ListenAndServe(); err != nil{
		return err 
	}
	
	return nil 
}

//모든 핸들러를 정의하기 위한 함수 
func makeMuxRouter() http.Handler{
	muxRouter := mux.NewRouter()
	muxRouter.HandleFunc("/", handleGetBlockchain).Methods("GET")
	muxRouter.HandleFunc("/",handleWriteBlock).Methods("POST")
	return muxRouter 
}

//get 핸들러를 통해 서버에 요청을 보내면 브라우저를 통해 블록체인을 볼 수 있음?
func handleGetBlockchain(w http.ResponseWriter, r *http.Request){
	bytes, err := json.MarshalIndent(Blockchain, "", " ")
	//에러가 nil값이 아니면 즉 에러가 있으면 다음 값들을 리턴 
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return 
	}
	//string을 쓰는 함수? 
	io.WriteString(w, string(bytes))
}

//POST request는 BPM을 담은 구조체가 필요함
//그래서 다음과 같이 구조체로 BPM을 int로 가지는 Message를 선언함
type Message struct {
	BPM int 
}

func handleWriteBlock(w http.ResponseWriter, r *http.Request){
	var m Message 
	//request body의 decord 값
	decoder := json.NewDecoder(r.Body)

	//decorder값을 디코딩했을 때 에러로 설정 이후 JSON으로 리스폰스 반환하는 값 설정 
	if err := decoder.Decode(&m); err != nil{
		respondWithJSON(w,r, http.StatusBadRequest, r.Body)
		return 
	}

	defer r.Body.Close()

	//뉴블록에 제너레이트 블록값 설정 
	newBlock, err := generateBlock(Blockchain[ len(Blockchain) -1  ], m.BPM)
	
	//에러 처리 
	if err != nil {
		respondWithJSON(w, r, http.StatusInternalServerError,m)
		return 
	}

	//블록유효성 검사, true일경우 
	if isBlockValid(newBlock, Blockchain[ len(Blockchain) -1 ]){

		//뉴블록체인은 블록체인에 뉴블록을 삽입하는 것
		newBlockchain := append(Blockchain, newBlock)
		replaceChain(newBlockchain)
		//spew.Dump는 콘솔에 찍어주기위한 함수임, 디버그를 위한 함수
		spew.Dump(Blockchain)
	}

	respondWithJSON(w,r,http.StatusCreated, newBlock)

}
func respondWithJSON(w http.ResponseWriter, r *http.Request, code int, payload interface{} ){
	//response 값에 json.
	response, err := json.MarshalIndent(payload, "", " ")
	
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		w.Write([]byte("HTTP 500: Internal Server Error"))
		return 
	}
	//헤더에 코드를 삽입하고
	w.WriteHeader(code)
	//리스폰스를 삽입함
	w.Write(response)
}

func main(){
	//env에 있는 설정을 로드한다. 포트를 가져오기 
	err := godotenv.Load()
	//에러가 있으면 로그출력?
	if err != nil{
		log.Fatal(err)
	}

	go func(){
		//현재시간 생성
		t := time.Now()
		//제네시스블록 선언 
		genesisBlock := Block{0, t.String(), 0, "", ""}
		//디버깅을 위한 spew.Dump 함수실행 
		spew.Dump(genesisBlock)
		//블록체인 변수에 제네시스 변수 추가 
		Blockchain = append(Blockchain, genesisBlock)
	}()

	log.Fatal(run())
}

 

코드를 작성한 뒤 실행하면 다음과 같습니다.

go run main.go 

2023/02/06 22:42:37 Listening on  8888
(main.Block) {
 Index: (int) 0,
 Timestamp: (string) (len=54) "2023-02-06 22:42:37.009985972 +0900 KST m=+0.010562082",
 BPM: (int) 0,
 Hash: (string) "",
 PrevHash: (string) ""
}
([]main.Block) (len=2 cap=2) {
 (main.Block) {
  Index: (int) 0,
  Timestamp: (string) (len=54) "2023-02-06 22:42:37.009985972 +0900 KST m=+0.010562082",
  BPM: (int) 0,
  Hash: (string) "",
  PrevHash: (string) ""
 },
 (main.Block) {
  Index: (int) 1,
  Timestamp: (string) (len=54) "2023-02-06 22:42:41.900319406 +0900 KST m=+4.900895499",
  BPM: (int) 50,
  Hash: (string) (len=64) "8c99758ad22bba145dea773571ded40e74a2851a61d6297b0089a8c07085fcab",
  PrevHash: (string) ""
 }
}
([]main.Block) (len=3 cap=4) {
 (main.Block) {
  Index: (int) 0,
  Timestamp: (string) (len=54) "2023-02-06 22:42:37.009985972 +0900 KST m=+0.010562082",
  BPM: (int) 0,
  Hash: (string) "",
  PrevHash: (string) ""
 },
 (main.Block) {
  Index: (int) 1,
  Timestamp: (string) (len=54) "2023-02-06 22:42:41.900319406 +0900 KST m=+4.900895499",
  BPM: (int) 50,
  Hash: (string) (len=64) "8c99758ad22bba145dea773571ded40e74a2851a61d6297b0089a8c07085fcab",
  PrevHash: (string) ""
 },
 (main.Block) {
  Index: (int) 2,
  Timestamp: (string) (len=55) "2023-02-06 22:42:50.222287086 +0900 KST m=+13.222863178",
  BPM: (int) 50,
  Hash: (string) (len=64) "2839ce411cc63a3284eb26799824c2f2c651b2cc1dae8002e530740bc8cb852a",
  PrevHash: (string) (len=64) "8c99758ad22bba145dea773571ded40e74a2851a61d6297b0089a8c07085fcab"
 }
}

 

블록을 생성하게 만들려면 다음 curl 스크립트 또는 POSTMan을 쓰시면 됩니다.

 

#쉘 스크립트 내용 확인
~/work/go_study/blockchain$ cat post_blockchain.sh 
#!/bin/bash


curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"BPM":50 }' \
  http://localhost:8888
  
 
 
#쉘 스크립트 실행
~/work/go_study/blockchain$ ./post_blockchain.sh 
{
 "Index": 3,
 "Timestamp": "2023-02-06 22:43:44.205101573 +0900 KST m=+67.205677675",
 "BPM": 50,
 "Hash": "2387e8825394f92f81c9f4b1f6902d33b8991bdc6b9f39491b31b1100c8570bf",
 "PrevHash": "2839ce411cc63a3284eb26799824c2f2c651b2cc1dae8002e530740bc8cb852a"

 

 

 

 

 

 

참고 링크

한글 go 기반 블록체인 만들기 공부
영어 go 기반 블록체인 만들기 공부(원문)

 

 

[블록체인 개발 공부] 블록체인 개발하기 PART 1 — Steemit

이 글은 아래 블로그(Coral Health)의 내용을 번역한 것입니다. https://medium.com/@mycoralhealth/code-your-own-blockchain-in-less-than-200-lines-of-go-e296282bcffc 먼저 블록체인을 공부하기 위해서는 다음과 같은 과정이

steemit.com

 

관련글 더보기