Go로 블록체인 만들기 #4

Min Seo Park
CAU_CLink
Published in
14 min readSep 8, 2020

Go 로 블록체인 만들기 시리즈 4번째

Part 4: 거래 1편

이번 Go로 블록체인 만들기 3편은 다음과 같이 구성되어있다.

  • There is no spoon
  • 비트코인 거래(Bitcoin Transaction)
  • 거래 출력값 (Transaction Outputs)
  • 거래 입력값 (Transaction Inputs)
  • The egg
  • 블록체인에 거래 기록 (Storing Transactions in Blockchain)
  • 작업증명 (Proof-of-Work)
  • Unspent Transaction Outputs
  • 코인 전송
  • 결론 (Conclusion)

There is no spoon

만약 웹 어플을 개발하고 그 안에 결제 시스템을 구현한다면, accountstransactions이 2개의 테이블들을 DB에 넣어야 할 것이다. 계정은 사용자에 대한 정보를 저장할 것이다; 또한, 개인적인 정보, 잔고 그리고 수신자 발신자간 자금 이동이 담긴 거래도 포함하고 있다. 비트코인은 다른 방식으로 구현된다. 비트코인 거래에는:

  1. 계정이 없다.
  2. 잔고 표시가 없다.
  3. 주소가 없다.
  4. 코인이 없다.
  5. 수신자와 송신자가 없다.

블록체인은 공개된 데이터베이스이기에, 지갑 주인의 민감한 정보를 공개하면 위험하다. 그래서 비트코인의 코인은 계좌에 모이지 않는다. 거래는 하나의 주소에서 다른 주소로 옮겨가지 않는다 : 잔고를 가지고 있는 칸이 따로 없다. 오직 거래만 저장할 뿐이다. 그렇다면 거래 내부에는 어떤 것이 있을까?

비트코인 거래 (Bitcoin Transaction)

거래는 입력값과 출력값의 조합이다 :

<code 4_1 : 거래의 구조>

새로운 거래의 입력값은 이전 거래의 출력값을 참조한다.(물론 예외도 있지만, 이는 추후에 언급하겠다.) 출력값은 코인이 실제로 저장되는 곳이다. 아래의 그림은 거래들의 상호작용 과정을 설명한다:

알아두어야 할 점:

  1. 입력값과 연결되지 않은 출력값도 있다.
  2. 특정 입력값은 다수의 거래에 있는 출력값을 참조할 수 있다.
  3. 입력값은 출력값을 참조해야만 한다.

이번 글에서 우리는 “돈”, “코인”, “사용”, “전송”, “계좌”와 같은 단어들을 사용할 것이다. 그러나 비트코인에는 그러한 개념들이 존재하지 않는다. 그저 값을 스크립트로 잠구고, 오직 잠군 사람에 의해서만 풀리는 형식으로만 진행된다.

거래 출력값 (Transaction Outputs)

출력값부터 구현해보자 :

<code 4_2 : 거래의 출력값>

사실, 출력값이 “코인”을 저장하는 것이다. (위에 Value 칸이 있다.) 여기서 저장한다는 것은 ScriptPubKey가 저장되어 있는 퍼즐과 함께 잠궜다는 것이다. 내부적으로, 비트코인이 스크립팅 할 때는 Script라는 언어를 사용한다. 이는 출력값을 잠그고 푸는 논리를 정의하는데 사용된다. 이 언어는 매우 원시적이고 해킹이나 오용을 피하기 위해 정의되었다. 자세한 설명은 여기에서 볼 수 있다.

비트코인에서는, 값 칸에 BTC단위가 아닌 사토시 단위로 저장되어있다. 사토시는 비트코인에서 가장 단위이고, 0.00000001 BTC가 1사토시이다.

아직 거래는 구현되지 않았기 때문에, 관련된 전체 논리 구조는 아직 구현하지 않을 것이다. ScriptPubKey는 임의의 데이터를 저장할 것이다. (임의로 정의된 지갑주소)

이러한 스크립팅 언어를 갖는다는 것은 비트코인이 스마트 컨트랙트 플랫폼에서도 사용될 수 있다는 것이다.

출력값의 특징 중 중요한 것은 나누어지지 않는 것이다. 즉, 일부분만을 참조할 수 없다. 새로운 거래에서 출력값이 참조되면, 전부가 사용되는 것이다. 참조된 값이 필요한 값보다 크면, 잔돈만큼 생성되고 보낸 이에게 돌아간다. 현실세계에서 $1짜리를 살 때, $5를 지불하면 $4만큼의 잔돈을 받는다.

거래 입력값 (Transaction Inputs)

아래에 입력값이 있다 :

<code 4_3 : 거래의 입력값>

앞서 말한것처럼 입력값은 이전 출력값을 참조한다 : Txid는 거래의 ID를 저장하고 Vout는 거래의 출력값의 index값을 저장한다. ScriptSig는 출력값의 ScriptPubKey에 사용될 데이터를 제공하는 스크립트이다. 만약 데이터가 옳다면, 출력값은 풀리게 되고 그 값은 새로운 출력값을 만드는데 사용된다; 만약에 데이터가 옳지 않다면, 출력값은 입력값에 참조될 수 없다. 메카니즘으로 자신의 소유가 아닌 코인을 사용할 수 없음이 확실해진다.

다시 한번 말하지만 아직 거래를 구현하지 않았기 때문에, ScriptSig는 임의로 정의된 지갑 주소를 저장할 것이다. 다음 글에서 공개키와 서명 부분을 구현할 것이다.

정리해보자. 출력값은 “코인”을 저장하는 곳이다. 모든 출력값은 출력값을 푸는 논리를 결정하는 푸는 스크립트와 함께 존재한다. 모든 거래는 최소 1개의 입력, 출력값을 가진다. 한 입력값은 이전 거래의 출력값을 참조하며 스크립트를 푸는데 사용되는 데이터를 (the ScriptSig field) 제공하며 그 값을 새로운 출력값을 만드는데 사용한다.

그러면 입력값 그리고 출력값 중 어떤 것이 먼저 오는 것일까?

The egg

비트코인에서는, 알이 닭보다 먼저 온다. 입력값을 참조하는 출력값 논리는 전형적인 “닭이 먼저인가 알이 먼저인가” 상황이다 : 입력값들은 출력값들을 만들고 출력값들은 입력값의 기반이 된다. 비트코인에서는 출력값이 입력값 앞에 온다.

채굴자가 블록을 채굴하면, 코인베이스 거래가 추가된다. 코인베이스 거래는 이전 출력값이 없는 특별한 형태의 거래이다. 출력값 즉, 코인을 특별한 기반없이 만들어낸다. 닭 없는 알이며 블록을 채굴한 채굴자에 대한 보상이다.

모두 알다시피, 제네시스 블록은 블록체인의 시작이다. 블록체인의 첫번째 출력값을 담은 블록이다. 이전 거래나 출력값 자체가 없기에, 이전 출력값이 필요없다.

코인베이스 거래를 만들어보자:

<code 4_4 : 새로운 코인베이스 거래>

코인베이스 거래는 오직 1개의 입력값이 있다. 우리가 구현한 코드에서는 Txid는 비어있고Vout의 값은 -1로 되어있다. 또한, 코인베이스 거래는 ScriptSig에 스크립트를 저장하지 않는다. 대신에 임의의 데이터가 저장되어 있다.

비트코인의 첫 코인베이스 거래에는 아래의 메세지가 적혀있었다 :
“The Times 03/Jan/2009 Chancellor on brink of second bailout for banks”.
여기에서 직접 볼 수 있다 .

subsidy 는 보상 금액이다. 비트코인에서 이 값이 어디 저장되어 있지는 않다. 채굴된 전체 블록의 개수에 따라 바뀐다. 첫 블록을 채굴할 때는 50BTC를 보상받는다. 그리고 매 210000블록마다 반감기가 오게 된다. 우리는 그냥 일정한 보상을 받는 형식으로 구현할 것이다.

블록체인에 거래 기록 (Storing Transactions in Blockchain)

이제부터 모든 블록은 최소 1개의 거래를 저장하며 거래 없이는 블록을 채굴할 수 없다. 그래서 우리는 Block의 Data칸 대신, 거래를 저장하려고 한다:

<code 4_5 : 블록의 새로운 구조>

NewBlock그리고 NewGenesisBlock도 맞추어 바뀌어야 한다:

<code 4_6 : 블록생성 함수>

다음으로 바꿀 곳은 새로운 블록체인의 만드는 부분이다:

<code 4_7 : 새로운 블록체인 생성>

이제, 함수는 제네시스 블록을 채굴한 보상을 받을 주소를 받을 것이다.

작업증명 (Proof-of-Work)

작업증명은 거래 저장소인 블록체인의 일관성과 무결성을 보장하기 위하여 블록 내에 저장되어있는 거래들을 지속적으로 생각해야만 한다. 그래서 이제부터 ProofOfWork.prepareData방법을 수정해야 한다:

<code 4_8 : 블록내 Data>

pow.block.Data대신에 pow.block.HashTransactions()를 사용할 것이다 :

<code 4_9 : 거래의 해시화>

우리는 데이터를 대표하는 유일값을 제공하기 위해 해시 함수를 사용한다. 블록 내 모든 거래들은 단순 해시값으로 유일값을 가지게 된다. 이를 달성하기 위해, 각 거래들의 해시값을 모으고 그 모은 값의 해시값을 취할 것이다.

비트코인은 조금 더 정교한 기술을 사용한다: Merkle tree가 한 블록 내에 있는 모든 거래들을 대표한다. 그리고 트리의 루트 해시값을 PoW시스템에 사용한다. 전체 거래를 다운 받지 않고 루트 해시값만으로도 특정 거래가 블록에 있는지를 확인할 수 있다.

지금까지 구현한 것이 잘되는지 확인해보자:

$ blockchain_go createblockchain -address Ivan 00000093450837f8b52b78c25f8163bb6137caf43ff4d9a01d1b731fa8ddcc8aDone!

이제 첫 블록 채굴 보상을 받게 되었다. 그렇다면 잔고 확인은 어떻게 할까?

Unspent Transaction Outputs

사용되지 않은 모든 출력값을 찾으려고 한다 (UTXO). Unspent라는 의미는 이 출력값은 그 어떤 입력값에 의해 참조되지 않았다는 것이다. 위의 그림을 참고해보자 :

  1. tx0, output 1;
  2. tx1, output 0;
  3. tx3, output 0;
  4. tx4, output 0.

잔고 확인을 할 때, 우리는 전부가 아닌 우리가 소유한 키로 풀 수 있는 것들만 파악하면 된다.(지금은 일단 키가 구현되어 있지 않기 때문에 사용자 정의된 주소를 대신 사용할 것이다.) 일단, 잠금-풀림 방법을 입력값 출력값에 정의해보자:

<code 4_10 : 개인키로 출력값 해제>

unlockingData로 스크립트 칸을 비교하려고 한다. 이 부분은 뒷 편에서 주소와 개인키를 구현하며 더 개선될 것이다.

사용되지 않은 출력값을 보유한 거래들을 찾는 것이 다음 단계이다 :

<code 4_11 : 사용되지 않은 출력값 총 검색>

블록에 거래들이 저장되어 있기에, 블록체인 내 모든 블록들을 확인해야 한다. 출력값부터 시작하자:

<code 4_12 : 특정 주소 소유의 거래 찾기>

만약 한 출력값이 우리가 찾으려는 사용되지 않은 출력값의 주소와 같은 주소로 잠겨 있다면, 제대로 찾은 것이 맞다. 취하기 전에, 먼저 출력값이 이미 입력값에 의해서 참조되진 않았는지 확인한다:

<code 4_13 : 참조되지 않은 출력값 찾기>

이미 참조된 출력값들은 생략한다.(이미 그 값들은 다른 출력값으로 옮겨졌을 것이기에, 우리는 사용할 수 없다.) 출력값을 확인한 후에 주어진 주소에 의하여 잠긴 모든 출력값들을 풀 수 있는 모든 입력값들을 모은다. (코인베이스에는 해당되지 않는다, 그들은 출력값을 풀지 않기 때문이다.):

<code 4_13 : 잠긴 출력값을 풀어주는 입력값 찾기>

이 함수는 사용되지 않은 출력값을 포함하는 거래들의 목록을 반환한다. 잔고를 계산하기 위해서 거래를 모아서 출력값만을 반환하는 함수가 1개 더 필요하다.

<code 4_14 : 출력값만을 반환하는 함수>

이게 전부다! 이제 getbalance명령어를 구현할 수 있다:

<code 4_15 : 계좌 잔고를 구하는 명령어>

계좌 잔고는 해당 계좌에 의해서 잠긴 모든 UTXO 값들의 합이다. 제네시스 블록을 채굴한 후의 잔고를 파악하자:

$ blockchain_go getbalance -address IvanBalance of 'Ivan': 10

자 이제 계좌 잔고를 뽑아낼 수 있다!

코인 전송 (Sending Coins)

이제 코인을 다른 이에게 보내보자. 이를 위해서는 새로운 거래를 만들고 블록에 넣은 후에 채굴해야 한다. 지금까지는, (특수 형태인)코인베이스 거래만을 구현하였다. 이제부터는 일반적인 거래를 만들려고 한다 :

<code 4_16 : 일반거래>

새로운 출력값을 만들기 전에, 소비되지 않은 출력값들을 모두 찾은 후에 충분한 값이 있는지를 확인해봐야 한다. 이 일을 FindSpendableOutputs가 실행한다. 그 후에, 발견된 각 출력에 대해 이를 참조하는 입력값이 생성된다. 다음으로 2개의 출력값을 만들어 낸다:

  1. 받는이의 주소와 잠긴 것 하나. 이것이 실제로 코인을 다른 주소로 전송하는 것이다.
  2. 보낸이의 주소와 잠긴 것 하나. 잔돈 부분이다. 사용되지 않은 출력값이 새로운 거래에서의 실제 요구된 양보다 많다면 생성된다. 나누어지지 않고 새롭게 생성된 것이다.

FindSpendableOutputs는 전에 정의된 FindUnspentTransactions 방법을 기반으로 한다 :

<code 4_17 : 사용되지 않은 출력값>

이 방법은 모든 사용되지 않은 출력값에 반복적으로 실행되고 그 값이 누적된다. 누적값이 전송하고 싶은 양보다 크거나 같게 되면 중지하고 거래 ID로 그룹화된 누적값과 인덱스값들을 반환한다. 보내고자 하는 값보다 더 큰 값이 소비되면 안된다.

이제 Blockchain.MineBlock 를 수정해보자 :

<code 4_18 : MineBlock 수정>

마지막으로 send 명령어를 구현해보자:

<code 4_19 : send 명령어>

코인 전송은 거래를 생성하고 이를 블록에 넣어 체인에 등록한다는 것을 의미한다. 비트코인은 이를 바로 수행하지는 않는다. 대신에, 모든 새로운 거래들은 기억 풀 (memory pool, mem pool)에 넣고 채굴자가 채굴할 준비가 되었다면 mem pool에 있는 거래를 블록에 담아 예비 블록을 생성한다. 거래들은 블록에 담기고 그 블록이 체인에 기록될 때만 검증된다.

코인 전송이 잘되는지 확인해보자:

$ blockchain_go send -from Ivan -to Pedro -amount 600000001b56d60f86f72ab2a59fadb197d767b97d4873732be505e0a65cc1e37Success!$ blockchain_go getbalance -address Ivan
Balance of ‘Ivan’: 4
$ blockchain_go getbalance -address Pedro
Balance of ‘Pedro’: 6

자 이제 더 많은 거래들을 생성하고 다수의 출력값을 전송하는 작업이 잘되는지 확인해보자:

$ blockchain_go send -from Pedro -to Helen -amount 2
00000099938725eb2c7730844b3cd40209d46bce2c2af9d87c2b7611fe9d5bdf
Success!$ blockchain_go send -from Ivan -to Helen -amount 2
000000a2edf94334b1d94f98d22d7e4c973261660397dc7340464f7959a7a9aa
Success!

이제 헬렌의 코인들은 2개의 출력값에 잠겨 있다 : 페드로에게 받은 것 1개, 이반에게 받은 것 1개. 이제 이들을 다른 이들에게 보내보자 :

$ blockchain_go send -from Helen -to Rachel -amount 3
000000c58136cffa669e767b8f881d16e2ede3974d71df43058baaf8c069f1a0
Success!$ blockchain_go getbalance -address Ivan
Balance of ‘Ivan’: 2
$ blockchain_go getbalance -address Pedro
Balance of ‘Pedro’: 4
$ blockchain_go getbalance -address Helen
Balance of ‘Helen’: 1
$ blockchain_go getbalance -address Rachel
Balance of ‘Rachel’: 3

Looks fine! Now let’s test a failure:

$ blockchain_go send -from Pedro -to Ivan -amount 5
panic: ERROR: Not enough funds
$ blockchain_go getbalance -address Pedro
Balance of ‘Pedro’: 4
$ blockchain_go getbalance -address Ivan
Balance of ‘Ivan’: 2

결론 (Conclusion)

쉽지는 않았지만 거래 구현을 완료하였다. 하지만, 몇가지 암호화폐의 주요 특징들이 아직 구현되지 않았다 :

  1. 주소. 아직 개인키를 기반으로한 주소가 구현되어 있지 않다.
  2. 보상. 여기서 받은 코인은 수익성이 없다!
  3. UTXO 세트. 잔고 확인을 위해서 블록체인 전체를 훑어 보았을 때, 블록의 개수가 많다면 시간이 상당히 오래걸릴 것이다. 또한, 뒷 부분에 있는 거래를 증명하려면 시간이 더 걸릴 것이다. UTXO 세트는 이 문제를 해결하고 거래 관련 일을 빠르게 수행하기 위해서 고안되었다.
  4. Mempool. 거래들이 블록에 저장되기 전에 이곳에 저장된다. 현재 구현에서는, 비효율적이지만 블록당 오직 1개의 거래만 저장 되어있다.

참조링크 :

  1. Full source codes
  2. Transaction
  3. Merkle tree
  4. Coinbase

--

--

Min Seo Park
CAU_CLink

Interested in Blockchain, Project Financing and Smart city and Love DJing and EDM