이제 가장 중요한 부분이라 할 수 있는 Transaction을 구현할 것이다.
블록체인이란 무엇이었는지 다시 한번 살펴보겠다.
블록체인(Blockchain)이란 데이터 분산 저장 기술의 일종으로 관리 대상의 데이터를 block 단위로 P2P(peer to peer) 방식을 기반으로 chain 형태로 연결하여 저장하는 기술이다. 저장된 데이터는 모든 참여 노드에 기록되며 운영자에 의한 임의 조작이 불가능하다.
그렇다면 여기서 관리 대상의 데이터란 무엇일까?
비트코인에서는 블록 내에 거래 내역을 담아 서명하고 처리하여 관리하는데,
이러한 외부 거래를 기록하기 위해 컴퓨터 시스템 내에서 처리하는 과정에서 전송되는 데이터가 있다.
바로 Transaction이다.
(암호화폐 상에서의 트랜잭션은 코인을 송금하는 거래내역 및 서명된 정보를 말한다.)
결국 블록체인이 'Transaction'을 안전하고 신뢰 가능하게 저장하고 관리하는 것이다.
사전 지식은 이 정도로 하고, 이제 시작해보도록 하겠다.
Type Transaction
// Coin transaction
type Transaction struct {
ID []byte
Txin []TXInput
Txout []TXOutput
}
트랜잭션의 구조체이다.
ID와 input, output을 가지고 있다.
위 이미지를 보면서 이해해보자.
트랜잭션은 여러 개의 input, output으로 이루어질 수 있다.
(Tx is abbreviation for "transaction)
예를 들어 A가 B에게 4 coin을 주기 위해 10 coin을 전송했다면,
4 coin은 B에게 가고 나머지 6 coin은 A에게 돌아온다.
이때 output은 2개가 되며
여기서 돌아온 6 coin이 다른 Tx에서 사용하게 될 새로운 input이 되는 것이다.
잘 이해가 안 될 수 있다.
블록체인 암호화폐에서는 어느 한 사람의 잔액을 계산하는 방식이 우리가 보통 생각하는 방식과 매우 다른데,
잠시 후 이를 이해하고 난 후 다시 생각해보도록 하자.
위 이미지를 보며 기억하고 넘어가야 할 것은
다른 input과 연결되지 않은 output이 존재하며,
하나의 Tx는 여러 Tx의 output을 참조할 수 있다.
마지막으로 input은 반드시 output을 참조해야만 한다는 것이다.
그래서 input과 output은 여러 개가 있을 수 있기 때문에 transaction 구조체에
이 2개의 타입이 배열로 들어가 있는 것이다.
Type TXInput
// Transaction input
type TXInput struct {
Txid []byte
TxoutIdx int
ScriptSig string // Unlock script
}
먼저 입력이다.
Txid는 트랜잭션의 ID를 저장하고, TxoutIdx는 참조한 output의 인덱스 번호를 저장한다.
ScriptSig에는 아직 지갑을 구현하지 않았기 때문에 임의의 문자열로 대신할 것이다.
개인적으로 입력은 수입의 출처를 기록한다고 생각하며 이해했다.
Type TXOutput
// Transaction output
type TXOutput struct {
Value int
ScriptPubKey string // Lock script
}
다음으로 출력이다.
얼마나(value) 누구에게(ScriptPubkey) 가는지 기록하는 친구이다.
Function NewCoinbaseTX
// Creates a new coinbase transaction
func NewCoinbaseTX(to, data string) *Transaction {
txin := TXInput{[]byte{}, -1, data}
txout := TXOutput{subsidy, to}
tx := Transaction{nil, []TXInput{txin}, []TXOutput{txout}}
return &tx
}
제네시스 블록과 같이 Base가 될 트랜잭션이 필요하다.
txin := TXInput{[]byte{}, -1, data}
입력에는 이전의 출력을 참조하여 생성되는 트랜잭션이 아니기 때문에 TXInput의 TxoutIdx의 값은 -1로 고정한다.
txout := TXOutput{subsidy, to}
출력은 subsidy와 해당 블록의 subsidy를 받을 임의의 주소를 넣어준다.
여기서 block subsidy란
각 블록에서 발행되는 새로운 비트코인의 양이다. 블록체인에 추가되는 각 블록을 통해 블록 생성자는 일정량의 새로운 비트코인을 얻을 수 있다.
기본적으로 bitcoin에 관한 문서를 찾아보며 만들고 있기 때문에
const subsidy = 10
위와 같이 변수명을 설정해보았다.
현재 블록 보조금(block subsidy)은 10으로 고정하였다.
이제 지금까지 추가한 것을 적용해보도록 하겠다.
type Block struct {
TimeStamp int32 `validate:"required"`
Hash []byte `validate:"required"`
PrevHash []byte `validate:"required"`
Transactions []*Transaction `validate:"required"` // Data -> Transactions
Nonce int `validate:"min=0"`
}
가장 먼저 Block 구조체이다.
당연하게도 새롭게 추가된 Transactions가 Data를 대체하게 되었다.
// Prepare new block
func NewBlock(transactions []*Transaction, prevHash []byte) *Block {
newblock := &Block{int32(time.Now().Unix()), nil, prevHash, transactions, 0}
pow := NewProofOfWork(newblock)
nonce, hash := pow.Run()
newblock.Hash = hash[:]
newblock.Nonce = nonce
return newblock
}
다음으로 NewBlock이다.
기존에 data를 transactions으로 바꿔주었다.
// Generate genesis block
func GenerateGenesis(tx *Transaction) *Block {
return NewBlock([]*Transaction{tx}, []byte{})
}
제네시스 블록을 생성하는 함수도 알맞게 변경해주었다.
// Get All Blockchains
func GetBlockchain(address string) *Blockchain {
...
err = db.Update(func(tx *bolt.Tx) error {
bc := tx.Bucket([]byte("blocks"))
if bc == nil {
cb := NewCoinbaseTX(address, "init base")
genesis := GenerateGenesis(cb)
...
} else {
last = bc.Get([]byte("last"))
}
return nil
})
if err != nil {
log.Fatal(err)
}
bc := Blockchain{db, last}
return &bc
}
// Add Blockchain
func (bc *Blockchain) AddBlock(transactions []*Transaction) {
...
newBlock := NewBlock(transactions, lastHash)
...
}
GetBlockchain과 AddBlock도 수정해주었다.
이번엔 작업 증명 부분이다.
func (pow *ProofOfWork) prepareData(nonce int) []byte {
data := bytes.Join(
[][]byte{
[]byte(pow.block.PrevHash),
pow.block.HashTransactions(),
IntToHex(int64(pow.block.TimeStamp)),
IntToHex(int64(targetBits)),
IntToHex(int64(nonce)),
},
[]byte{},
)
return data
}
다른 것들과 같은 맥락으로
// before
[]byte(pow.block.Data),
기존에 data였던 부분을,
// after
pow.block.HashTransactions(),
트랜잭션을 해시해서 넣어주었다.
이 함수는 바로 살펴보도록 하겠다.
Function HashTransactions
// Hash transactions
func (b *Block) HashTransactions() []byte {
var txHashes [][]byte
var txHash [32]byte
for _, tx := range b.Transactions {
txHashes = append(txHashes, tx.GetHash())
}
txHash = sha256.Sum256(bytes.Join(txHashes, []byte{}))
return txHash[:]
}
Tx를 하나하나 해싱하여 배열에 추가한 후, 빈 byte로 이어 한 번에
sha256 알고리즘을 이용하여 변환한 후, 반환해준다.
Function GetHash
// Hashes the transaction and returns the hash
func (tx Transaction) GetHash() []byte {
var writer bytes.Buffer
var hash [32]byte
enc := gob.NewEncoder(&writer)
err := enc.Encode(tx)
if err != nil {
log.Panic(err)
}
hash = sha256.Sum256(writer.Bytes())
return hash[:]
}
HashTransactions()에서 등장한 GetHash 함수이다.
Tx 구조체를 인코딩하고, sha256 알고리즘을 이용하여 변환한 후 반환하는 함수이다.
결국 기존에 []byte[] byte 타입이던 Data와 달리 Transactions는 [] byte 타입으로의 변환이 필요하기 때문에
추가된 함수들이다.
UTXO(Unspent Transaction Output)
이제 초반에 언급했던 각 주소별로의 잔액을 계산하는 기능을 구현할 것이다.
UTXO란 말 그대로 아직 소비되지 않은 출력이다.
즉, 사용되지 않은 암호화폐를 모아 특정 사용자의 잔액을 계산하게 되는 것이다.
현재 암호화폐를 구현하고 있는 것이기 때문에, 당연하게도 '송금'하는 기능이 필요하다.
지금부터 구현해보도록 하겠다.
다시 한번 위 이미지를 살펴보면, 소비되지 않은 출력이 무엇인지 쉽게 이해할 수 있다.
Tx 3의 ouput 1이 utxo에 해당한다.
우린 저러한 utxo들을 모아 잔액을 계산하고, 해당 미사용 출력들을 필요한 만큼 참조하여 입력을 만들어
새로운 트랜잭션을 만들게 되는 과정이 블록체인에서의 송금이다.
그렇다면 내가 누군가에게 코인을 송금한다고 가정했을 때, 현재 내가 소유하고 있는 코인의 양을 구할 수 있어야 한다.
Function FindUnspentTxs
// Returns a list of transactions containing unspent outputs
func (bc *Blockchain) FindUnspentTxs(address string) []*Transaction {
var unspentTXs []*Transaction
spentTXOs := make(map[string][]int)
bcI := bc.Iterator()
for {
block := bcI.getNextBlock()
for _, tx := range block.Transactions {
txID := hex.EncodeToString(tx.ID)
Outputs:
for outIndex, out := range tx.Txout {
// Was the output spent?
if spentTXOs[txID] != nil {
for _, spentOut := range spentTXOs[txID] {
if spentOut == outIndex {
continue Outputs
}
}
}
if out.ScriptPubKey == address {
unspentTXs = append(unspentTXs, tx)
continue Outputs
}
}
if !tx.IsCoinbase() {
for _, in := range tx.Txin {
if in.ScriptSig == address {
inTxID := hex.EncodeToString(in.Txid)
spentTXOs[inTxID] = append(spentTXOs[inTxID], in.TxoutIdx)
}
}
}
}
if len(block.PrevHash) == 0 {
break
}
}
return unspentTXs
}
어우 어지럽다.
여기보고 저기보며 살펴보면 더 어지러울 수 있으니 위에서부터
천천히 살펴보도록 하겠다.
var unspentTXs []*Transaction
spentTXOs := make(map[string][]int)
bcI := bc.Iterator()
unspentTXs : 반환하게 될 utxo가 있는 트랜잭션들을 담을 변수이다.
spentTXOs : 이미 사용된 출력들을 담아 활용하기 위해 선언된 변수이다.
bcI : 아시다시피 우리는 지금 블록들을 순회해야 한다.
for outIndex, out := range tx.Txout {
// Was the output spent?
if spentTXOs[txID] != nil {
for _, spentOut := range spentTXOs[txID] {
if spentOut == outIndex {
continue Outputs
}
}
}
...
}
Outputs 라벨의 첫 부분이다.
현재 순회 중인 Tx의 ID값을 통해 내가 지금 탐색 중인 Tx에 이미 소비된 출력이 있는지 확인하고,
만약 있다면
// 기억이 가물가물하신 분들을 위해
// Transaction input
type TXInput struct {
...
TxoutIdx int // <-- 이 친구
...
}
// 출력이 소비되었음을 확인하고 continue
if spentOut == outIndex {
continue Outputs
}
Tx 입력의 output을 지칭하는 인덱스 번호를 이용하여 해당 출력을 식별하고 다음으로 넘어가게 된다.
그 과정에서 걸러지지 않은 출력들은
for outIndex, out := range tx.Txout {
...
if out.ScriptPubKey == address {
unspentTXs = append(unspentTXs, tx)
continue Outputs
}
}
출력의 ScriptPublicKey에 저장된 주소는 곧 해당 주소로 코인이 보내졌다는 뜻이므로
위와 같이 unspentTXs에 추가한다.
지금 ScriptPublicKey를 임의의 주소와 비교하여 확인하고 있지만, 이는 아직 주소(지갑)를 구현하지 않아
임시로 사용한 방식이며, 다음 포스팅에서 변경될 것이다.
if !tx.IsCoinbase() {
for _, in := range tx.Txin {
if in.ScriptSig == address {
inTxID := hex.EncodeToString(in.Txid)
spentTXOs[inTxID] = append(spentTXOs[inTxID], in.TxoutIdx)
}
}
}
마지막으로 입력을 탐색한다.
해당 입력이 참조한 출력을 알아내어 해당 출력은 소비되었음을 기록하게 된다.
이렇게 기록된 값은 조금 전 살펴본 Outputs 라벨의 첫 부분에서 활용된다.
이렇게 UTXO가 포함된 TX들을 얻었다.
이 함수를 활용하여 잔고를 구해보자.
Function getBalance
func (cli *Cli) getBalance(address string) {
bc := GetBlockchain(address)
defer bc.db.Close()
balance := 0
utxs := bc.FindUnspentTxs(address)
for _, tx := range utxs {
for _, out := range tx.Txout {
if out.ScriptPubKey == address {
balance += out.Value
}
}
}
fmt.Printf("Balance of '%s': %d\n", address, balance)
}
Cli의 메서드로 getBalance를 작성하였다.
utxs := bc.FindUnspentTxs(address)
utxos를 얻어낸 후,
for _, tx := range utxs {
for _, out := range tx.Txout {
if out.ScriptPubKey == address {
balance += out.Value
}
}
}
주소와 출력의 ScriptPublicKey를 비교하여
해당 주소 소유의 코인인지 확인한 후 모두 더해준다.
이제 특정 주소 소유의 잔고를 확인할 수 있다.
실행 시 모습 :
$ go run . getbalance -address a
Balance of 'a': 10
(제네시스 블록 채굴 후 확인한 모습)
잔고는 확인할 수 있지만 아직 송금은 불가하다.
a가 b에게 송금한다고 가정했을 때, 새로운 트랜잭션이 생성되어야 한다.
새 트랜잭션을 생성하는 함수 작성 이전에,
Function FindUTXOs
// Finds and returns unspend transaction outputs for the address
func (bc *Blockchain) FindUTXOs(address string, amount int) (int, map[string][]int) {
unspentOutputs := make(map[string][]int)
unspentTXs := bc.FindUnspentTxs(address)
accumulated := 0
Work:
for _, tx := range unspentTXs {
txID := hex.EncodeToString(tx.ID)
for index, txout := range tx.Txout {
if txout.ScriptPubKey == address && accumulated < amount {
accumulated += txout.Value
unspentOutputs[txID] = append(unspentOutputs[txID], index)
if accumulated >= amount {
break Work
}
}
}
}
return accumulated, unspentOutputs
}
우선 트랜잭션들을 순회하며 출력들만 반환해주는 함수 먼저 구현하도록 하겠다.
이번 친구는 딱히 복잡하지 않다.
UTXO를 가진 트랜잭션들을 받아서 주소와 출력의 ScriptPublicKey를 비교하여
해당 주소 소유의 코인인지 확인한 후 송금하려는 양보다 누적량이 많거나 같아지면
Work 라벨을 종료한다.
Function NewUTXOTransaction
// Creates a new transaction
func NewUTXOTransaction(from, to string, amount int, bc *Blockchain) *Transaction {
var inputs []TXInput
var outputs []TXOutput
accumulated, validOutputs := bc.FindUTXOs(from, amount)
if accumulated < amount {
log.Panic("ERROR: Not enough funds")
}
// Build a list of inputs
for txid, outs := range validOutputs {
for _, out := range outs {
txID, err := hex.DecodeString(txid)
if err != nil {
log.Panic(err)
}
input := TXInput{txID, out, from}
inputs = append(inputs, input)
}
}
// Build a list of outputs
outputs = append(outputs, TXOutput{amount, to})
if accumulated > amount {
outputs = append(outputs, TXOutput{accumulated - amount, from})
}
tx := Transaction{nil, inputs, outputs}
tx.SetID()
return &tx
}
func FindUTXOs를 활용해주었다.
accumulated, validOutputs := bc.FindUTXOs(from, amount)
if accumulated < amount {
log.Panic("ERROR: Not enough funds")
}
송금 이전에 잔고가 충분한지 먼저 확인해준다.
// Build a list of inputs
for txid, outs := range validOutputs {
for _, out := range outs {
txID, err := hex.DecodeString(txid)
if err != nil {
log.Panic(err)
}
input := TXInput{txID, out, from}
inputs = append(inputs, input)
}
}
다음으로 입력을 생성하는 부분이다.
// Build a list of outputs
outputs = append(outputs, TXOutput{amount, to})
if accumulated > amount {
outputs = append(outputs, TXOutput{accumulated - amount, from})
}
tx := Transaction{nil, inputs, outputs}
tx.SetID()
그리고 출력까지 생성해준 후 Transaction 구조체를 만들어주고,
SetID 메서드를 이용하여 ID를 설정해주었다.
이제 송금 메서드를 작성해주겠다.
Function send
func (cli *Cli) send(from, to string, amount int) {
bc := GetBlockchain(from)
defer bc.db.Close()
tx := NewUTXOTransaction(from, to, amount, bc)
bc.AddBlock([]*Transaction{tx})
fmt.Println("Send Complete!!")
}
트랜잭션을 생성하고, 블록을 생성한 후 체인에 추가해준다.
이 과정에서 AddBlock, NewBlock의 data는 transaction으로 적절하게 수정해주었다.
실행 시 모습 :
$ go run . getbalance -address a
02568a5587d199c73f8d88bfa510bbd5366a790dea0af951880cbd30c063215b
Balance of 'a': 10
$ go run . send -from a -to b -amount 3
0020781bc613d1cec72801cccac8e58863625ddeb580cd77eb0ca5eaf5ab4362
Successfully Added
Send Complete!!
$ go run . getbalance -address a
Balance of 'a': 7
$ go run . getbalance -address b
Balance of 'b': 3
$ go run . send -from b -to a -amount 4
2022/01/28 20:13:18 ERROR: Not enough funds
panic: ERROR: Not enough funds
아직 블록체인을 생성하는 커맨드가 없는 상태이다.
getbalance 사용 시 기존에 blockchain이 없으면 제네시스 블록을 발행하고,
해당 블록 채굴 보상을 잔고를 확인하려했던 주소가 받게 해두었다.
자세한 코드의 변경사항 혹은 직접 테스트해보고 싶다면 아래 주소에서 확인할 수 있다.
https://github.com/hou27/blockchain_go/tree/part4
참고자료
'Blockchain' 카테고리의 다른 글
Go로 만드는 블록체인 part 6 - UTXO 집합 (0) | 2022.02.01 |
---|---|
Go로 만드는 블록체인 part 5 - Wallet (0) | 2022.01.29 |
Go로 만드는 블록체인 part 3 - Persistence (0) | 2022.01.23 |
Go로 만드는 블록체인 part 2 - Proof of Work (0) | 2022.01.10 |
Go로 만드는 블록체인 part 1 - Base of Blockchain (0) | 2021.12.09 |