타입스크립트로 블록체인 만들기 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