이 내용은 CoralHeath 블로그의 내용을 번역 한 것 입니다.
https://medium.com/@mycoralhealth/part-2-networking-code-your-own-blockchain-in-less-than-200-lines-of-go-17fe1dad46e1
지난 포스팅에서는 기본적인 블록체인을 코딩했습니다.
하지만 하나의 노드에서만 동작 해보았을 뿐입니다.
어떻게 다른 노드들과 연결하여 새로운 블록을 생성할 수 있을까요? 그리고 모든 노드의 블록을 업데이트 할 수 있을까요?
이번 포스팅을 통해 알아 봅시다.
Step 1
Step 2
Step 3
아래 내용은 이번 포스팅에서 해야 할 내용 들입니다.
이 튜토리얼의 목표는 노드에서 생성된 블록체인이 어디로 이동하는지 결정할 수 있는 기본적인 네트워크를 만드는 것입니다.
우리는 아직 다른 노드에서 첫번째 노드에 블록을 생성하는 방법을 아직 모릅니다. 이 포스팅에서 데이터를 클라우드에 올리는 것처럼 구현하여 쉽게 달성할 수 있게 될 것입니다. 또한 새로운 블록이 생성되어 블록체인에 업데이트 됬을때 이 새로운 블록체인을 다른 노드에 알리는 것을 시뮬레이션하게 될 것입니다.
개발을 위해서는 spew, godotenv 패키지가 필요합니다.
아래 명령어로 설치하세요(Go lang이 설치 되어있어야만 실행가능합니다).
go get github.com/davecgh/go-spew/spew
go get github.com/joho/godotenv
그리고 main.go 파일을 생성하세요.
package main
import (
"bufio"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net"
"os"
"strconv"
"time"
"github.com/davecgh/go-spew/spew"
"github.com/joho/godotenv"
)
// Block represents each 'item' in the blockchain
type Block struct {
Index int
Timestamp string
BPM int
Hash string
PrevHash string
}
// Blockchain is a series of validated Blocks
var Blockchain []Block
블록이 생성할 때 필요한 해시 계산 함수를 작성합시다.
// SHA256 hashing
func calculateHash(block Block) string {
record := string(block.Index) + block.Timestamp + string(block.BPM) + block.PrevHash
h := sha256.New()
h.Write([]byte(record))
hashed := h.Sum(nil)
return hex.EncodeToString(hashed)
블록생성 함수 입니다.
// create a new block using previous block's hash
func generateBlock(oldBlock Block, BPM int) (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 = calculateHash(newBlock)
return newBlock, nil
}
이제 블록 유효성 체크 함수를 작성합시다.
새로운 블록은 PrevHash와 이전 블록의 Hash를 체크함으로써 유효성을 체크한다는 것을 명심하세요.
// make sure block is valid by checking index, and comparing the hash of the previous block
func isBlockValid(newBlock, oldBlock Block) bool {
if oldBlock.Index+1 != newBlock.Index {
return false
}
if oldBlock.Hash != newBlock.PrevHash {
return false
}
if calculateHash(newBlock) != newBlock.Hash {
return false
}
return true
}
동시에 블록이 생성되는 경우 가장 긴 체인을 선택하여 사용하는 함수입니다.
// make sure the chain we're checking is longer than the current blockchain
func replaceChain(newBlocks []Block) {
if len(newBlocks) > len(Blockchain) {
Blockchain = newBlocks
}
}
지난 Part1 포스팅에서 사용한 함수들 중 필요한 함수를 가져왔습니다.
이제 networking 하는 방법을 알아 봅시다.
main함수를 통해 시작해 봅시다.
시작전에 bcServer(blochain server를 줄임말) 라는 전역변수를 우리가 선언했던 구조체들 아래에 선언합시다.
// bcServer handles incoming concurrent Blocks
var bcServer chan []Block
이제 main 함수를 작성 해봅시다.
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
bcServer = make(chan []Block)
// create genesis block
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
}
먼저 root 디렉토리에 있는 .env 파일에서 환경 변수들을 불러 오는 코드를 작성합니다.
이미 설정했던 .env의 ADDR을 내용을 불러와 9000번 포트를 사용할 수 있습니다.
TCP server 코드를 작성해 봅시다.
모든 데이터 전송은 터미널 콘솔을 통해 진행됩니다.
이 포트를 통해 여러 개의 connection을 다룰 것입니다.
아래 코드를 main 함수 마지막 아래에 추가 하세요.
// start TCP and serve TCP server
server, err := net.Listen("tcp", ":"+os.Getenv("ADDR"))
if err != nil {
log.Fatal(err)
}
defer server.Close()
위 코드에서 defer server.Close()함수는 connection이 더 이상 필요 없을 때 종료하기 위해서 사용됩니다.
defer를 더 알아보고 싶으면 여기를 클릭하세요.
이제 우리는 매번 connection 요청이 올때 마다 새로운 connection을 생성해야 합니다.
for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
새로운 connection들은 받을 수 있도록 무한 루프를 생성 합시다.
동시에 각 connection들을 분리해서 다룰 수 있도록 해야 하는데 이는 Go루틴 go handleConn(conn)을 통해 해결합니다.
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal(err)
}
bcServer = make(chan []Block)
// create genesis block
t := time.Now()
genesisBlock := Block{0, t.String(), 0, "", ""}
spew.Dump(genesisBlock)
Blockchain = append(Blockchain, genesisBlock)
// start TCP and serve TCP server
server, err := net.Listen("tcp", "localhost:"+os.Getenv("ADDR"))
if err != nil {
log.Fatal(err)
}
defer server.Close()
for {
conn, err := server.Accept()
if err != nil {
log.Fatal(err)
}
go handleConn(conn)
}
}
이제 Main함수 작성이 끝났습니다.
그런데 아직 handleConn 함수를 작성하지 않아 실행이 되지 않습니다.
이제 handleConn 함수를 작성해 봅시다.
func handleConn(conn net.Conn) {
defer conn.Close()
}
이 함수는 한 개의 인자(net.Conn)가 필요합니다.
defer conn.Close()를 이용하여 각 connection을 종료할 수 있습니다.
이제 우리는 사용자가 새로운 블록을 블록체인에 추가하는 것을 해야 합니다.
지난 포스팅에서 사용했던 pulse rate BPM을 사용할 예정입니다.
위 내용을 달성하기 위해서는 다음과 같은 것들이 필요합니다.
BPM을 입력한다.위 내용을 충족할 수 있는 코드가 바로 아래 있습니다. 이 코드는 defer conn.Close() 아래에 작성하면 됩니다.
io.WriteString(conn, "Enter a new BPM:")
scanner := bufio.NewScanner(conn)
// take in BPM from stdin and add it to blockchain after conducting necessary validation
go func() {
for scanner.Scan() {
bpm, err := strconv.Atoi(scanner.Text())
if err != nil {
log.Printf("%v not a number: %v", scanner.Text(), err)
continue
}
newBlock, err := generateBlock(Blockchain[len(Blockchain)-1], bpm)
if err != nil {
log.Println(err)
continue
}
if isBlockValid(newBlock, Blockchain[len(Blockchain)-1]) {
newBlockchain := append(Blockchain, newBlock)
replaceChain(newBlockchain)
}
bcServer <- Blockchain
io.WriteString(conn, "\nEnter a new BPM:")
}
}()
이제 위 코드에 대해서 하나하나 알아 봅시다.
for scanner.Scan() 반복문은 Go 루틴안에 작성합니다. 그러면 다른 connection들과 분리하여 수행합니다.bpm을 integer 형으로 변환 해줍니다.bcServer <- Blockchain 이 문장은 새로운 블록체인을 채널에 넣는 것을 의미합니다.
그리고나서 사용자의 새로운 BPM을 새로운 블록에 입력할 수 있도록 합니다.
코드는 아래 있습니다.
// simulate receiving broadcast
go func() {
for {
time.Sleep(30 * time.Second)
output, err := json.Marshal(Blockchain)
if err != nil {
log.Fatal(err)
}
io.WriteString(conn, string(output))
}
}()
for _ = range bcServer {
spew.Dump(Blockchain)
}
완성된 코드는 Github https://github.com/evasioner/blockchain에 올려 놓았습니다.
go run main.go 으로 실행합니다.
서버가 올라온 것을 확인하면 새롭게 터미널을 실행하여
netcat을 이용하여 접속해봅시다.
nc localhost 9000
으로 실행하면 BPM을 입력하라고 할것 입니다.
그리고 입력하시고 싶은 숫자를 입력하세요
입력하게 되면 터미널 1에서 블록이 추가된것을 확인 할수 있습니다.
그리고 아래 그림과 같이 매 30초마다 블록이 업데이트 되었다는 것이 클라이언트에 오게 됩니다.
다음 포스팅에서는 마이닝 알고리즘에 대해서 알아 볼것 입니다.
현재 많은 플랫폼에서 사용하고 있는 POW(proof of work) 작업증명 방식과 POS(proof of stake) 지분증명 방식 등을 직접 코딩하고 테스트 할 예정입니다.
감사합니다.