Go 언어를 공부할겸 블록체인을 만드는 공부를 진행했습니다. 이미 한글 포스팅과 영어 포스팅이 있어서 보고 공부했으며 정리를 목적으로 포스팅합니다.
1. go 언어 설치:https://go.dev/dl/
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
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 기반 블록체인 만들기 공부(원문)
Go 언어를 기반으로한 블록체인 개발공부(Proof of Stake) - Part 4 (0) | 2023.02.11 |
---|---|
Golang Algorithm - binary search 이진 탐색 (0) | 2023.02.10 |
Golang Algorithm - Linear Search - 선형 검색 (0) | 2023.02.10 |
Go 언어를 기반으로한 블록체인 개발공부(Power of Work) - Part 3 (0) | 2023.02.10 |
Go 언어를 기반으로한 블록체인 개발공부(Network) - Part 2 (0) | 2023.02.09 |