블록체인/Mainnet

Go 언어로 블록체인 메인넷 만들기 - 거래#2

type TXOutput

이제 거래에서 주소를 사용 할 것이며 이후에 서명도 만들고 거래를 검증도 해야하기 때문에 TXInput, TXOuput 의 구조를 바꿔줄 필요가 있다. TXOutput 에서는 이전에 .ScriptPubKey 를 사용했지만 비트코인에서 구현하는 스트립트 언어를 구현하여 표현할 것이 아니기 때문에 지불 대상자의 공개키 해시(Public Key Hash)를 가지게 될 것이다. 이러한 공개키 해시는 주소에서 Base58CheckDecode 를 하면 얻을 수 있다.

type TXOutput struct {
	Value      uint64
	PubKeyHash []byte
}

func .NewTXOutput() *TXOutput

새로운 TXOutput 을 생성한다. 이전에는 만들지 않았지만 이번에 새로 만드는 것은 NewTransaction() 처럼 분리해야 할 필요성이 생겼기 때문이다.

func NewTXOutput(value uint64, address string) *TXOutput {
	txo := &TXOutput{value, nil}
	txo.Lock(address)

	return txo
}

여기서 .Lock() 메서드는 주소에 해당하는 공개키 해시로 출력을 잠근다.

func .Lock(string)

주소로 부터 공개키 해시(Public Key Hash)를 얻어온 다음 출력을 잠근다. 이렇게 잠긴 것을 해제하여 소비할 수 있는 것은 지불을 받는 당사자 밖에 없다.

func (out *TXOutput) Lock(address string) {
	pubKeyHash, _, err := base58.CheckDecode(address)
	if err != nil {
		log.Panic(err)
	}
	out.PubKeyHash = pubKeyHash
}

type TXInput

입력에서는 이후에 구현할 서명을 포함하여 서명을 검증하기 위한 발신자의 공개키가 필요하다.

type TXInput struct {
	Txid      []byte
	Vout      int
	Signature []byte
	PubKey    []byte
}

func .UsesKey([]byte) bool

공개키 해시가 입력에 사용된 .PubKey 와 동일한지 검사합니다. 이 함수는 이후 수정할 UTXO(Unspent Transaction Output)와 관련된 메서드 및 함수에서 사용된다.

func (in *TXInput) UsesKey(pubKeyHash []byte) bool {
	lockingHash := HashPubKey(in.PubKey)
	return bytes.Compare(pubKeyHash, lockingHash) == 0
}

func NewCoinbaseTX(string, string) *Transaction

코인베이스 트랜잭션을 생성할 때 주소를 받아오기 떄문에 TXOutput 을 생성할 때 Base58CheckDecode 로 처리하고 공개키 해시를 출력에 넣어야한다. 우리는 이 과정을 NewTXOutput 으로 이미 만들어놓았다.

func NewCoinbaseTX(data, to string) *Transaction {
	txin := TXInput{[]byte{}, -1, nil, []byte(data)}
	txout := NewTXOutput(subsidy, to)

	return NewTransaction([]TXInput{txin}, []TXOutput{*txout})
}

type Blockchain

Blockchain 타입에 있는 UTXO(Unspent Transaction Output)에 관련된 메서드에 일부 변화가 있다.

func .FindUnspentTransactions([]byte) []*Transactions

UTX(Unspent Transaction)을 찾을 떄 조건문에 변화가 있다. 먼저 소비된 출력을 찾을 때 쓰는 조건에서 기존에는 TXInput.ScriptSig 값으로 출력의 사용여부를 찾았으나 지금은 구현에 변화를 주었기 때문에 공개키 해시로 비교를 해주면 된다. 별도의 필드로 빠진 TXInput.Signature 의 경우 이후 거래 검증에 사용될 것이다.

 

또한 UTXO 를 찾을 때에 기존어는 TXOuput.ScriptPubKey 를 비교하였지만, 이제는 TXOutput.PubKeyHash 값으로 출력값을 잠그기때문에 이 또한 변경해준다.

func (bc *Blockchain) FindUnspentTransactions(pubKeyHash []byte) []*Transaction {
	bci := NewBlockchainIterator(bc)

	// ...
	for bci.HasNext() {
		for _, tx := range bci.Next().Transactions {
			txID := hex.EncodeToString(tx.ID)

		Outputs:
			for outIdx, out := range tx.Vout {
				// ...
				if bytes.Compare(out.PubKeyHash, pubKeyHash) == 0 {
					unspentTXs = append(unspentTXs, tx)
				}
			}

			if !tx.IsCoinbase() {
				for _, in := range tx.Vin {
					if in.UsesKey(pubKeyHash) {
						hash := hex.EncodeToString(in.Txid)
						spentTXOs[hash] = append(spentTXOs[hash], in.Vout)
					}
				}
			}
		}
	}

	return unspentTXs
}

func .FindUTXO([]byte) []TXOutput

UTXO 를 찾을 때도 주소에서 공개키 해시를 받는걸로 변경한다. 조건문도 마찬가지로 수정해야 한다.

func (bc *Blockchain) FindUTXO(pubKeyHash []byte) []TXOutput {
	var UTXOs []TXOutput
	unspentTXs := bc.FindUnspentTransactions(pubKeyHash)

	for _, tx := range unspentTXs {
		for _, out := range tx.Vout {
			if bytes.Compare(out.PubKeyHash, pubKeyHash) == 0 {
				UTXOs = append(UTXOs, out)
			}
		}
	}

	return UTXOs
}

func .GetBalance(string) uint64

잔액을 구할 때 기존과 마찬가지로 주소를 받는 것에는 변화가 없으나 주소의 구조가 바뀌었기때문에 Base58CheckDecode 를 통해 공개키 해시를 얻어서 .FindUTXO() 를 호출할 필요가 있다.

func (bc *Blockchain) GetBalance(address string) uint64 {
	var balance uint64

	pubKeyHash, _, err := base58.CheckDecode(address)
	if err != nil {
		log.Panic(err)
	}
	for _, out := range bc.FindUTXO(pubKeyHash) {
		balance += out.Value
	}

	return balance
}

func .Send(uint64, string, string) *Transaction

여기가 핵심파트다. 이전에 우리가 만든 KeyStore 를 이곳에서 사용한다. 로직자체는 크게 변한게 없지만, 다른 메서드와 마찬가지로 조건문의 일부 변화가 있기때문에 자세히 살펴보도록 하자. 공개키해시를 사용하여 검증하고 NewTXOutput() 메서드를 사용하여 출력을 구성한다.

func (bc *Blockchain) Send(value uint64, from, to string) *Transaction {
	// ...
	keyStore := NewKeyStore()

	wallet := keyStore.Wallets[from]
	UTXs := bc.FindUnspentTransactions(HashPubKey(wallet.PubKey))

	var acc uint64

Work:
	for _, tx := range UTXs {
		for outIdx, out := range tx.Vout {
			if bytes.Compare(out.PubKeyHash, HashPubKey(wallet.PubKey)) == 0 && acc < value {
				acc += out.Value
				txin = append(txin, TXInput{tx.ID, outIdx, nil, wallet.PubKey})
			}
			// ...
		}
	}
	// ...

	txout = append(txout, *NewTXOutput(value, to))
	if acc > value {
		txout = append(txout, *NewTXOutput(acc-value, from))
	}

	return NewTransaction(txin, txout)
}

거래를 진행할 때는 주소를 가져오기 때문에 그것을 기반으로 얻어와야 하는데, KeyStore 에서는 주소를 키로 사용하여 지갑을 얻어올 수 있도록 했기때문에 from 으로부터 Wallet 을 얻어올 수 있다. tx.Vout 을 루프로 돌고있는 내부 조건문을 살펴보면 from 이 가진 소유금액을 처리할 수 있도록 공개키해시를 비교하고 있다.

 

TXInput 을 구성할 때 TXInput.Signature 부분은 nil 로 처리한 것을 볼 수 있는데, 아직 디지털 서명을 구현하지 않았기 때문이다. 그 외에는 from 이 보낸 거래를 검증할 수 있도록 공개키를 추가해준다. 지금은 서명이 구현되지 않았기 때문에 완전하게 거래를 검증할 수는 없지만 설정해줄 필요성은 충분히 있다.

 

출력을 구성할 때는 NewTXOutput() 에 주소를 넘겨주는데, 알다시피 내부에서는 해당 주소에서 공개키해시를 구해 TXOutput.Lock() 처리를 하고 거래를 잠글 것이다.

결론

여기까지 거래를 수정했다면 이제 우리가 만든 것을 테스트해볼 수 있다. 주소를 만들고 거래를 생성하고 잔액을 호가인해보는 과정을 거쳐보자.

# 지갑 만들기
$ ./bc newwallet
Address: 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM

$ ./bc newwallet
Address: 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB

# 블록체인 생성
$ ./bc new -address 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM
0000184346f709a36306b255d6411f0b0e0413a3f5a8e3e4c3d278da1c5766bc

# 거래 전 잔액 확인하기
$ ./bc getbalance -address 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM
Balance of '16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM': 10

$ ./bc getbalance -address 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB
Balance of '1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB': 0

# 거래 생성하기
$ ./bc send -from 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM -to 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB -value 8
000021449c934cec995d87279c01c5b2a7d9968a101608573df14c0d2a94c317

# 거래 후 잔액 확인하기
$ ./bc getbalance -address 1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB
Balance of '1G1Jtj9LBu3KmuYVfNv7sjMFFx2Y3Cd5MB': 8

$ ./bc getbalance -address 16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM
Balance of '16vNFmJkd2va1LWJS6w2nsVRNKJeRAmUbM': 2

이제 다음 포스트에서는 디지털 서명을 구현해볼 것이다. 디지털 서명에서도 ECDSA 로 불리는 타원곡선 암호화 알고리즘을 사용하여 생성할 것이다. 물론 나도 아직 공부해야 할 부분이 많다.