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 로 불리는 타원곡선 암호화 알고리즘을 사용하여 생성할 것이다. 물론 나도 아직 공부해야 할 부분이 많다.