타입스크립트로 블록체인 만들기 in Go

본 게시글은 Nomad Coder의 강의 ‘타입스크립트로 블록체인 만들기’의 일부를 Golang을 사용하여 컨버팅 하는 과정을 기술하고 있습니다.

컨버팅(Converting)
같은 플랫폼 상에서 프로그램이 동일하게 돌아갈 수 있게 기존 언어 A에서 새로운 언어 B로 변경하는 과정


깃허브 저장소

https://github.com/piatoss3612/simple-blockchain

초기 설정

1. Golang 버전 확인

$ go version
go version go1.20.4 linux/amd64

최신 버전은 1.21입니다.

2. 작업 디렉터리 생성

$ mkdir simple-blockchain
$ cd simple-blockchain

3. Go 모듈 초기화

$ go mod init simple-blockchain
go: creating new go.mod: module simple-blockchain

4. main.go 파일 생성

$ touch main.go


블록 구현하기

1. 기존 코드

import crypto from 'crypto';

interface BlockShape {
    hash: string;
    prevHash: string;
    height: number;
    data: string;
}

class Block implements BlockShape {
    public hash: string;
    constructor(
        public prevHash: string,
        public height: number,
        public data: string
    ) {
        this.hash = Block.calculateHash(prevHash, height, data);
    }

    static calculateHash(prevHash: string, height: number, data: string): string {
        const toHash = `${prevHash}${height}${data}`;
        const hash = crypto.createHash('sha256'); 
        return hash.update(toHash).digest('hex');
    }
}

2. 변경한 코드

package main

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
)

type Block struct {
	hash     string
	prevHash string
	data     string
	height   int
}

func NewBlock(prevHash string, data string, height int) Block {
	block := Block{
		hash:     calculateHash(prevHash, data, height),
		prevHash: prevHash,
		data:     data,
		height:   height,
	}
	return block
}

func calculateHash(prevHash string, data string, height int) string {
	hash := sha256.New()
	_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))
	return hex.EncodeToString(hash.Sum(nil))
}

 Golang은 강타입 언어(strongly typed language)로, 변수의 타입을 명시적으로 선언해 주어야 합니다. 그런 점에서 TypeScript와 비슷하지만, Golang은 클래스를 지원하지 않습니다. 대신 구조체(Struct)가 TypeScript의 클래스 역할을 합니다. 이 부분은 Golang이 객체 지향 언어인지 아닌지에 대한 설명이 필요한데, 해당사항은 별도의 게시물로 작성해 보겠습니다.

 구조체를 사용해 정의한 블록블록의 생성자 함수입니다.

type Block struct {
	hash     string
	prevHash string
	data     string
	height   int
}

func NewBlock(prevHash string, data string, height int) Block {
	block := Block{
		hash:     calculateHash(prevHash, data, height),
		prevHash: prevHash,
		data:     data,
		height:   height,
	}
	return block
}

 블록의 해시를 생성하는 함수입니다. Golang도 마찬가지로 내장 함수를 사용하여 SHA256 해시를 생성할 수 있습니다.

func calculateHash(prevHash string, data string, height int) string {
	hash := sha256.New()
	_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))
	return hex.EncodeToString(hash.Sum(nil))
}

  Write 함수는 쓰인 바이트의 길이와 오류를 반환합니다. 여기서는 해당 값들을 적절하게 처리해주고 있지 않기 때문에 만일을 대비해 언더스코어로 사용하지 않은 값임을 표시해 주는 것이 좋습니다.

_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))

⛓ 블록체인 구현하기

1. 기존 코드

class Blockchain {
    private blocks: Block[];
    constructor() {
        this.blocks = [];
    }

    private getPrevHash() {
        if (this.blocks.length === 0) return "";
        return this.blocks[this.blocks.length - 1].hash;
    }

    public addBlock(data: string) {
        const block = new Block(this.getPrevHash(), this.blocks.length+1, data);
        this.blocks.push(block);
    }

    public getBlocks() {
        return this.blocks;
    }
}

2. 변경한 코드

type Blockchain struct {
	blocks []Block
}

func NewBlockchain() Blockchain {
	return Blockchain{}
}

func (bc *Blockchain) AddBlock(data string) {
	prevHash := bc.prevHash()
	height := len(bc.blocks) + 1
	block := NewBlock(prevHash, data, height)
	bc.blocks = append(bc.blocks, block)
}

func (bc *Blockchain) prevHash() string {
	if len(bc.blocks) == 0 {
		return ""
	}

	return bc.blocks[len(bc.blocks)-1].hash
}

func (bc Blockchain) GetBlocks() []Block {
	return bc.blocks
}

 구조체를 사용해 정의한 블록체인과 블록체인의 생성자 함수입니다. 블록을 저장하기 위해 동적으로 크기를 조절할 수 있는 슬라이스를 사용했습니다. 슬라이스는 실제 데이터가 저장된 배열을 가리키는 포인터값을 가지고 있으며, 기본 길이는 0입니다.

type Blockchain struct {
	blocks []Block
}

func NewBlockchain() Blockchain {
	return Blockchain{
		blocks: make([]Block, 0),
	}
}

 새로운 블록을 추가하는 메서드이전 블록의 해시를 가져오는 메서드입니다. 클래스에서는 private 접근자를 사용해 메서드에 대한 접근을 제한할 수 있지만 구조체에서는 메서드 이름이 대문자로 시작하면 public, 소문자로 시작하면 private 접근자가 적용됩니다. 그러나 클래스의 private 접근자는 클래스가 정의된 모듈 안에서도 유효하지만, 구조체는 외부에서 모듈을 불러와서 사용할 때만 적용됩니다.

func (bc *Blockchain) AddBlock(data string) {
	prevHash := bc.prevHash()
	height := len(bc.blocks) + 1
	block := NewBlock(prevHash, data, height)
	bc.blocks = append(bc.blocks, block)
}

func (bc *Blockchain) prevHash() string {
	if len(bc.blocks) == 0 {
		return ""
	}

	return bc.blocks[len(bc.blocks)-1].hash
}

 블록체인의 모든 블록을 가져오는 메서드입니다.

func (bc Blockchain) GetBlocks() []Block {
	return bc.blocks
}

실행하기

func main() {
	blockchain := NewBlockchain()

	blockchain.AddBlock("First block")
	blockchain.AddBlock("Second block")
	blockchain.AddBlock("Third block")

	blocks := blockchain.GetBlocks()

	for _, block := range blocks {
		fmt.Printf("Prev. hash: %s\n", block.prevHash)
		fmt.Printf("Data: %s\n", block.data)
		fmt.Printf("Hash: %s\n", block.hash)
		fmt.Println("==================================")
	}
}
$ go run main.go
Prev. hash: 
Data: First block
Hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
==================================
Prev. hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
Data: Second block
Hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
==================================
Prev. hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
Data: Third block
Hash: 3615128e0cffc4015b6d5113a1e6fd0533075d346ce12c116b8ff864cf09035e
==================================

오늘의 궁금증

1. 문제 상황

// getBlocks() is public, so we can modify private blocks array
blockchain.getBlocks().push(new Block("xxxxxxxx", 11111, "HACKED"));

 TypeScript로 작성한 코드에서 private 인스턴스 변수로 선언된 blocks를 getBlocks 메서드를 사용해 불러오면 원래 배열 자체를 수정할 수 있는 문제가 발생합니다. 이는 배열이 참조값으로 반환되기 때문인데요.

public getBlocks() {
	return [...this.blocks];
}

 이런 식으로 새로운 배열을 생성하여 반환하면 문제를 해결할 수 있습니다. 그렇다면 Golang에서도 이와 비슷한 문제가 발생할까요?

2. 문제 적용

func main() {
	blockchain := NewBlockchain()

	blockchain.AddBlock("First block")
	blockchain.AddBlock("Second block")
	blockchain.AddBlock("Third block")

	blocks := blockchain.GetBlocks() // Get blocks

	blocks[0].data = "Genesis block" // Try to change data

	blocks = blockchain.GetBlocks() // Get blocks again

	for _, block := range blocks {
		fmt.Printf("Prev. hash: %s\n", block.prevHash)
		fmt.Printf("Data: %s\n", block.data)
		fmt.Printf("Hash: %s\n", block.hash)
		fmt.Println("==================================")
	}
}

 블록체인에 슬라이스로 저장된 블록들을 불러와 첫 번째 블록의 데이터를 변경해 보았습니다. 

$ go run main.go
Prev. hash: 
Data: Genesis block
Hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
==================================
Prev. hash: 7f647b7d55a6cf3598d1c4c386326f72bb9f28ee2e107168162195cd86e5ea23
Data: Second block
Hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
==================================
Prev. hash: d552fe3755fc27d2680d65b516572ca7217bcdc5f7cee1aca115fab35035460e
Data: Third block
Hash: 3615128e0cffc4015b6d5113a1e6fd0533075d346ce12c116b8ff864cf09035e
==================================

 앗! 데이터가 변경되었네요! 왜 이런 일이 발생하는 걸까요?

3. 원인

 앞서 **‘****슬라이스는 실제 데이터가 저장된 배열을 가리키는 포인터값을 가지고 있다’**라고 했습니다. 그리고 Golang의 함수나 메서드의 매개변수로 넘어가는 값이나 결과로 반환되는 값은 모두 복사된 값으로 전달됩니다. 즉, 그렇게 전달된 값을 변경한다고 해서 원본 값을 변경할 수 없다는 것이죠. 그러나 슬라이스가 가지고 있는 포인터가 복사되어 전달되는 경우는 조금 다릅니다. 포인터가 가리키는 실제 배열은 포인터를 통해서 접근할 수 있고 변경도 가능합니다.

 다만 슬라이스가 가리키는 배열의 값에 접근할 수 있다는 것이지 원본 슬라이스의 원소를 삭제하거나 새로운 원소를 추가하는 것은 어렵습니다. 슬라이스에는 포인터값 외에도 슬라이스의 길이를 가리키는 len값과 용량을 가리키는 cap값이 포함되어 있습니다. 이 값들은 포인터값과 마찬가지로 복사되어 전달되므로 변경을 한다한들 원본 값은 영향을 받지 않습니다.

4. 해결 방법

1) 새로운 슬라이스를 생성해서 반환하기

func (bc Blockchain) GetBlocks() []Block {
	newBlocks := make([]Block, len(bc.blocks))
	copy(newBlocks, bc.blocks)
	return newBlocks
}

 새로운 슬라이스는 새로운 포인터값을 가지므로 기존 슬라이스에 접근할 수 없어집니다.

2) 패키지 분리

package block

import (
	"crypto/sha256"
	"encoding/hex"
)

type Block struct {
	hash     string
	prevHash string
	data     string
	height   int
}

func NewBlock(prevHash string, data string, height int) Block {
	block := Block{
		hash:     calculateHash(prevHash, data, height),
		prevHash: prevHash,
		data:     data,
		height:   height,
	}
	return block
}

func (b Block) Hash() string {
	return b.hash
}

func (b Block) PrevHash() string {
	return b.prevHash
}

func (b Block) Data() string {
	return b.data
}

func (b Block) Height() int {
	return b.height
}

func calculateHash(prevHash string, data string, height int) string {
	hash := sha256.New()
	_, _ = hash.Write([]byte(fmt.Sprintf("%s%d%s", prevHash, height, data)))
	return hex.EncodeToString(hash.Sum(nil))
}

 block 패키지를 분리하게 되면 Block 구조체의 필드가 소문자로 시작하므로 public 접근이 불가능하여 블록 정보를 읽어 들이기 위해 별도의 getter 메서드를 정의해줘야 합니다.

참고 자료

https://nomadcoders.co/typescript-for-beginners

https://stackoverflow.com/questions/39993688/are-slices-passed-by-value

https://go.dev/blog/slices-intro