블록체인/메인넷

Go 언어로 블록체인 메인넷 만들기 - CLI(Command Line Interface)

메인넷 그 자체와는 관련이 없긴 하다만, 잠깐 쉬어가는 느낌으로 우리가 만든 블록체인을 조금 더 쉽게 조작할 수 있도록 커맨드 라인 프로그램을 만들어보자. 지난 포스트에서 작성한 내용을 테스트하려면 두 번의 실행이 필요하다고 했다. 이를 main() 함수에 작성하려면 코드를 주석처리해야 하는 등 번거롭고 추후를 도모하기 위해서라도 만들어두면 좋을 듯하여 만들어본다. 점점 blockchain_go 에 나온 코드들이 왜 그렇게 쓰였는지 조금씩 알게되는 것 같다.

type CLI

이 타입은 CLI 와 관련있는 메서드를 정의하고 동작을 정의하기 위해 존재한다.

type CLI struct {}

func .createBlockchain() 

블록체인을 생성하는 메서드다. 뭔가 이상함이 느껴질지도 모르겠다. NewBlockchain() 함수가 있는데 이것은 무엇인가 하고 말이다. 아래에서 이야기하겠지만, .createBlockchain() 메서드는 블록체인을 "새로" 만드는 행동을 취한다. 즉, 기존의 NewBlockchain() 함수에서는 블록체인이 있는지 없는지에 따라 다른 행동을 취했는데, 그 행동을 분리했다고 생각하면 좋다.

func (c *CLI) createBlockchain() {
	bc := CreateBlockchain()
	bc.db.Close()
}

CreateBlockchain() 함수를 통해 블록체인을 새로 생성한다. 내용은 NewBlockchain() 에 있던 것을 거의 가져온 것이나 다름없다.

func CreateBlockchain() *Blockchain

func CreateBlockchain() *Blockchain {
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Panic(err)
	}

	var l []byte

	err = db.Update(func(tx *bolt.Tx) error {
		b, err := tx.CreateBucket([]byte(BlocksBucket))
		if err != nil {
			log.Panic(err)
		}

		genesis := NewBlock("Genesis Block", []byte{})

		err = b.Put(genesis.Hash, genesis.Serialize())
		if err != nil {
			log.Panic(err)
		}

		// "l" 키는 마지막 블록해시를 저장합니다.
		err = b.Put([]byte("l"), genesis.Hash)
		if err != nil {
			log.Panic(err)
		}
        
 		l = genesis.Hash

		return nil
	})

	return &Blockchain{db, l}
}

블록체인을 새로 만드는 역할만 하기때문에 조건문으로 인한 분기처리가 하나도 없다. 그럼 기존에 있는 블록체인을 가져와서 처리하는 것은 어디서 할까? 그건 그저 기존에 있던 NewBlockchain() 메서드에서 한다.

func NewBlockchain() *Blockchain

이 함수는 블록체인이 이미 생성된 상태에서 관련 객체만 생성하기 때문에 블록체인을 완전히 새로 생성하지 않고 기존에 있던 기존에 있던 블록체인을 얻어올 필요가 있는 경우에 사용한다. 예를 들면 블록을 생성할 때와 출력할 때 사용할 것이다.

func NewBlockchain() *Blockchain {
	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Panic(err)
	}

	var l []byte

	err = db.Update(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(BlocksBucket))

		l = b.Get([]byte("l"))

		return nil
	})
	if err != nil {
		log.Panic(err)
	}

	return &Blockchain{db, l}
}

func .addBlock(string)

새로운 블록을 추가한다. 단순히 Blockchain.AddBlock() 을 호출할 것이다. 여기서 가져오는 블록체인은 기존에 있던 체인에 추가하는 것이므로 NewBlockchain() 을 쓴다.

func (c *CLI) addBlock(data string) {
	bc := NewBlockchain()
	defer bc.db.Close()

	bc.AddBlock(data)
}

func .list()

블록체인에 있는 데이터를 출력한다. 마찬가지로 이미 있는 블록체인을 출력하는 것이니 NewBlockchain() 을 쓰자.

func (c *CLI) list() {
	bc := NewBlockchain()
	defer bc.db.Close()

	bc.List()
}

func blockchain.List()

BlockchainIterator 를 사용하여 블록체인을 순회하면서 돌자. 가장 마지막 블럭부터 역순으로 진행하게 된다는 것을 잊지말자.

func (bc *Blockchain) List() {
	bci := NewBlockchainIterator(bc)

	for bci.HasNext() {
		block := bci.Next()

		fmt.Printf("PrevBlockHash: %x\n", block.PrevBlockHash)
		fmt.Printf("Hash: %x\n", block.Hash)
		fmt.Printf("Data: %s\n", block.Data)

		pow := NewProofOfWork(block)
		fmt.Println("pow:", pow.Validate(block))

		fmt.Println()
	}
}

func .Run()

어플리케이션에 들어온 Arguments 를 파싱하고 그에 해당하는 커맨드를 실행하는 메서드다. 명령어에 따라 FlagSet 들을 정의하고 명령이 들어오면 파싱, 그에 해당하는 것들을 실행한다.

func (c *CLI) Run() {
	newCmd := flag.NewFlagSet("new", flag.ExitOnError)
	addCmd := flag.NewFlagSet("add", flag.ExitOnError)
	listCmd := flag.NewFlagSet("list", flag.ExitOnError)

	addBlockData := addCmd.String("data", "", "")

	switch os.Args[1] {
	case "new":
		newCmd.Parse(os.Args[2:])
	case "add":
		addCmd.Parse(os.Args[2:])
	case "list":
		listCmd.Parse(os.Args[2:])
	default:
		os.Exit(1)
	}

	if newCmd.Parsed() {
		c.createBlockchain()
	}
	if addCmd.Parsed() {
		if *addBlockData == "" {
			addCmd.Usage()
			os.Exit(1)
		}
		c.addBlock(*addBlockData)
	}
	if listCmd.Parsed() {
		c.list()
	}
}

각 명령어는 블록체인을 만들고, 블록을 추가하고, 블록들을 순회하기 위한 명령어다.

func main()

이제 메인에서 하는 일은 CLI 를 호출하는 것 밖에는 없다.

func main() {
	cli := CLI{}
	cli.Run()
}

그럼이제 한 번 테스트를 해보도록 하자. 먼저 새로운 블록체인을 만든다. 바이너리의 이름은 bc 다. 여담으로 targetBits16 으로 설정했기 때문에 채굴작업이 금방끝나도록 바꾸었다.

# chain.db 및 blocks 버킷 생성
$ ./bc new

# 두 개의 새로운 블록 추가

$ ./bc add -data "Send 1 BTC to Ivan"
$ ./bc add -data "Send 2 more BTC to Ivan"

# 블록 리스트 출력
$ ./bc list
PrevBlockHash: 00008fe72d2492f54bacd16fd25c1d44ef032276eafe4710f6ba94bcd5007083
Hash: 0000fb545ebc2170f8f448f1a7e28c45b5bc524ced964120cb8a4f57d4ea844d
Data: Send 2 more BTC to Ivan
pow: true

PrevBlockHash: 0000384db0036814c7a93dc4e77fce31a1a01a04c7888dd7eebfa28a5c5bd9c3
Hash: 00008fe72d2492f54bacd16fd25c1d44ef032276eafe4710f6ba94bcd5007083
Data: Send 1 BTC to Ivan
pow: true

PrevBlockHash:
Hash: 0000384db0036814c7a93dc4e77fce31a1a01a04c7888dd7eebfa28a5c5bd9c3
Data: Genesis Block
pow: true

여전히 갈 길은 멀다. 아직도 구현해야 할 블록체인 요소들은 많이 남아있다. 트랜잭션, 주소, 네트워크 등이 남아있다만, 아직 이해도가 부족하여 다음 포스팅까지는 시간이 다소 걸릴 예정이다.