1. intro
본격적으로 tx를 구현해보자.
tx를 구성하는건 입금 기록, 출금 기록, utxo (unspent transaction output) 크게 3가지를 꼽을 수 있다.
지갑 잔고가 50 BTC였던 지갑 A 에서 10BTC를 지갑 B로 코인을 전송했다고 하자.
i) 출금 기록
출금 기록은 출금한 지갑 주소, 출금 액수 2가지가 주요 구성 성분이다.
A : 10BTC
ii) 입금 기록
입금 기록은 입금받은 지갑 주소와 액수 2가지가 기록된다.
B : 10BTC
iii) UTXO
UTXO에는 잔금 목록이 업데이트 된다.
Before : (A : 50BTC)
After : ( A : 50-10BTC, B : 10BTC )
모든 거래는 이처럼 입금과 출금, utxo에 대한 데이터가 남지만 단 한 가지 유형의 전송만은 출금 데이터 없이
입금 데이터와 utxo 데이터만을 가진다.
바로 최초에 채굴자가 블럭을 생성할 때 받는 채굴 보상의 입금이다.
이 최초의 입금은 특별하지만 관여하는 데이터가 그만큼 적기 때문에 보다 구현과 이해가 쉽다.
따라서 일반적인 거래를 구현하기 전에, 튜토리얼로 채굴자 보상의 입금의 케이스를 먼저 구현해보자.
2. input, output
우선 input과 output에 대한 개념을 명확히하고 넘어가자. 둘 모두 정보의 집합이지만 담고 있는 정보가 조금씩 다른데,
각각 출금 기록과 입금 기록이라고 볼 수 있는데,
- Output은 코인이 어디로 보내지는지, 얼마나 보내지는지에 대한 정보를 담는다.
- Input은 지금 보내는 코인이 어디에서 왔는지, 정말 지갑의 주인이 맞는지, 잔액이 충분한지에 대한 정보를 담는다.
이 중 헷갈리는 것이 지금 보내는 코인이 어디에서 왔는지 그 소스를 추적하는 것인데,
그 코인이 어디에서 왔는지에 대한 대답은, 이 input의 코인이 직전에 어떤 output에서 왔는지이다.
input은 늘 전의 output을 참조해야한다.
그 output에 대한 정보를 제시해야만, 이를 블럭체인 상에서 확인하고 input이 유효해지는 것이다.
그렇기 때문에 앞으로 만들 input에 관련된 class에는 직전의 tx'Out'Id, tx'Out'Index가 담겨있게 된다.
정리해서 다른 말로 표현하자면, input에서 키를 이용해 잠긴 코인을 풀어서 새로운 주소의 output으로 보내
다시 잠궈버리며, 받은 돈을 출금 할때는 다시 input에서 풀고 output으로 보내 잠궈버리는 것을 반복하면 된다.
3. 객체 생성
입금 관련 데이터, 출금 관련 데이터, 잔금(utxo) 관련 데이터를 모두 tx class에 모아줘야 비로소 하나의 tx에 대한
데이터가 전부 모인다.
우선 transaction class를 구성할 각각의 class들을 구성해보자. 기본적인 속성은 다음과 같다.
/* @types/transaction.d.ts */
declare interface ITxOut {
account : string
amount : number
}
// 출금 정보
declare interface ITxIn {
txOutId : string
txOutIndex : number
// 입금할 돈이 어디서 왔는지 그 근원
signature : string | undefined
}
// 입금 정보
declare interface IUnspentTxOut {
txOutId : string
txOutIndex : number
account : string
amount : number
}
// utxo 정보
declare interface ITransaction {
hash : string
txOuts : ITxOut[]
txIns : ITxIn[]
}
// 정보들을 종합한 tx 객체
우선 TxIn, TxOut, unspentTxOut(utxo) class를 작성해준다.
/* src/core/txout.ts */
import { Wallet } from '@core/wallet/wallet'
export class TxOut {
public account : string
public amount : number
constructor ( _account : string, _amount : number) {
this.account = _account
this.amount = _amount
}
static createTxOuts ( sum : number, _receivedTx :any ) :TxOut[] {
const { sender, receiver, aomunt } = _receivedTx
// 받은 tx 데이터를 구조 분해 할당
const senderAccount : string = Wallet.getAccount(sender)
// 발신자 공개키로 발신자의 지갑주소 확인
const receivedTxOut = new TxOut(receiver, amount)
// 받은 만큼의 amount를 가진 utxo를 새로 생성
const senderTxOut = new TxOut(senderAccount, sum - account)
// 발신자에게 있던 기존 utxo에서 보낸 amount만큼을 뺀 새로운 액수를 가진
// utxo 생성
it ( senderTxOut.amount == 0 ) return [ receivedTxOut ]
// 여기서 amount는 윗 줄의 sum - amount가 되는데, 이 값이 음수일 경우,
// 즉 출금액이 잔고보다 클 경우 (잔액 부족) 보내는 사람을 만들지 않는다.
return [ receivedTxOut, senderTxOut ]
// 잔고가 충분하다면 수신인 ,발신인을 리턴
}
}
/* src/core/transaction/txin.ts */
export class TxIn {
public txOutId : string
public txOutIndex : number
public signature? : string
// 원래는 signature class여야 하지만 여기서는 string으로 받는다.
constructor ( _txOutId : string, _txOutIndex : number, _signature : string | undefined ) {
this.txOutId = _txOutId
this.txOutIndex = _txOutIndex
this.signature = _signature
}
}
/* src/core/transaction/unspentTxOut.ts */
export class unspentTxOut {
public txOutId : string
public txOutIndex : number
// utxo 또한 잔금의 개념이므로 그 출처가 필요하다. 그래서 txOutId와 txOutIndex 속성을 가진다.
public account : string
public amount : number
constructor( _txOutId : string, _txOutIndex : number, _account : string, _amount : number ) [
this.txOutId = _txOutId
this.txOutIndex = _txOutIndex
this.account = _account
this.amount = _amount
}
static getMyUnspentTxOuts( _account : string, _unspentTxOuts : unspentTxOut[]) : unspentTxOut[] {
return _unspentTxOuts.filter((utxo) => {
utxo.account === _account
})
// 전체 utxo중 내 지갑 주소로 되어있는 utxo들만 가져온다.
}
}
이 3가지 정보를 모아서 transaction에 넘겨 transaction class를 생성한다.
/* src/core/transaction/transaction.ts */
import { SHA256 } from 'crypto-js'
import { TxIn } from './txin'
import { TxOut } from './txout'
import { unspentTxOut } from './unspentTxOut'
export class Transaction {
public hash : string
public txIns : TxIn[]
public txOuts : TxOut[]
// input, output에 대한 정보를 모두 배열로 가져온다
constructor ( _txIns : TxIn[], _txOuts : TxOut[] ) {
this.txIns = _txIns
this.txOuts = _txOuts
this.hash = this.createTransactionHash()
// txIns, txOuts를 이용해 tx의 hash값을 만들어줄 수 있다.
}
// hash 생성 함수
createTransactionHash() : string {
const txoutContent : string = this.txOuts.map( v=> Object.values(v).join('')).join('')
const txinContent : string = this.txIns.map( v => Object.values(v).join('')).join('')
console.log(txoutContent, txinContent)
return SHA256(txoutContent + txinContent).toString()
}
// UTXO 생성 함수
createUTXO () : unspentTxOut[] {
let result : unspentTxOut[] = this.txOuts.map ((a,k) => {
return new unspentTxOut(this.hash, k, v.account, v.amount)
})
// 출금 데이터들을 가져다 utxo로 만들어준다.
// A 가 B에게 출금한 기록(들)을 가져다 B의 utxo로 만들어주는 과정
return result
}
}
4. Block, Chain class에 tx에 관한 함수, 매개변수 추가
지금까지는 블럭의 data 항목에 그냥 문자열을 배열에 담아 사용했지만,
원래 data에는 tx에 관한 데이터들을 배열로 담아야 한다.
이제 실제 블럭체인이 동작하는 것처럼 data 배열 안의 문자열을 tx로 바꿔보자.
우선 BLock.d.ts에서 IBlock의 data 속성의 데이터 타입을 ITransaction[]으로 바꿔주면
이와 연관된 class를 사용한 곳에서 모두 에러가 나올 것이다.
주로 data를 매개변수로 받는 함수들이 그럴텐데,
config.ts의 GENESIS는 빈 배열로 바꿔주고, block.ts, block.test.ts는 데이터 타입과 매개변수로 들어가는 _data의
데이터 타입을 바꿔준다.
chain.ts, chain.test.ts의 addBlock 함수의 매개변수를 바꿔주면 된다. 수정 후, npx jest를 입력해 에러가 없는지
한 번 테스트 해본 후 넘어가자.
5. 채굴 tx 구현
채굴 보상, 블럭의 첫 번째 tx는 코인 베이스라고 한다.
처음으로 config.ts에 채굴자에게 보상을 얼마나 줄지 그 값을 config.ts에 정해준다.
/* src/core/config.ts */
// ...중략
export const MINING_COMPENSATION : number = 17
채굴자에게 보상이 지급된다면 이는 utxo에 추가되고 이 정보가 블록체인 상에 저장되어야 한다.
채굴자는 자신의 utxo를 블록체인 서버에 요청함으로써 잔금의 추가를 확인할 수 있다.
/* src/core/blockchain/chain.ts */
import { unspentTxOut } from '@core/transaction/unspentTxOut'
export class Chain {
public blockchain : Block[]
private unspentTxOuts : unspentTxOut[] // utxo 속성을 chain class에 추가
constructor() {
this.blockchain = [Block.getChain()]
this.unspentTxOuts = [] // 채굴전엔 잔금이 없으므로 빈 배열을 가진다
}
// ...중략
public getunspentTxOuts : unspentTxOut[] {
return this.unspentTxOuts
}
// 체인 상의 utxo를 전부 배열에 담아 가져온다
public appendUTXO (utxo : unspentTxOut[]) : void {
this.unspentTxOuts.push(...utxo)
}
// 기존 utxo 목록 (배열)에 새로운 utxo를 추가한다
}
이 함수를 채굴자에게 보상을 지급하는 함수에서 호출하면 된다.
/* src/core/blockchain/chain.ts */
//...중략
public miningBlock ( _account : string ) {
const txin : ITxIn = new TxIn('', this.getLatestBlock().height + 1)
// 새로운 TxIn class를 생성할 때, 돈의 출처와 서명이 필요했는데
// 채굴자 보상은 이 돈을 보낸 사람이 존재하지 않고, 이전 tx도 없으므로
// 전 tx의 Id도, 서명도 존재하지 않는다.
// 다만 index는 존재하는데, 여기서는 이 값을 tx마다 달라지게 하기위해
// 새로 생성될 블럭의 height 값으로 임의로 정했다.
// 나중에 데이터를 hash화 할때 이에 의해 hash값이 달라질 것이다.
const txout : ITxOut = new TxOut ( _account, MINING_COMPENSATION )
// 채굴자 지갑 주소와 채굴 보상 액수를 인수로 새 TxOut class 생성
// 이런 txin, txout이 여러개 모여 배열이 되어 새 transaction class를 생성한다.
// 지금은 1개씩만 만들었다.
const transaction : Transaction = new Transaction([txin], [txout])
// 만든 txin, txout 데이터를 기반으로 새로운 transaction class 생성
return this.addBlock([transaction])
// 만든 transaction(들)을 매개변수로 addBlock 함수 실행
}
함수를 잘 보면 채굴에 대한 TxOut, TxIn은 만들어졌지만 이 데이터를 참조하는 utxo는 생성해주지 않았다.
(돈을 준다는 기록은 있지만 실제로 돈이 채굴자에게 들어오진 않은 상태)
이는 채굴자에게로의 돈 지급 시점이 블럭에 대한 tx 데이터를 만드는 때가 아니라
채굴자가 만든 블럭이 검증을 거쳐 실제로 네트워크 상의 블록체인에 추가된 후가 되어야 하기 때문이다.
(검증 과정에서 블럭이 체인에 추가되는 것이 실패할 경우 채굴 보상이 지급되어선 안 되므로)
따라서 utxo의 업데이트는 miningBlock 이후 실행되는 addBlock 함수에서 실행된다.
/* src/core/blockchain/chain.ts */
public addBlock ( data : ITransaction ) : Failable < Block, string > {
// ...중략
this.blockchain.push(newBlock)
// 새로 생성된 블럭이 체인에 추가된 이후, utxo를 업데이트 한다
newBlock.data.forEach((_tx : ITransaction) => {
this.updateUTXO(_tx)
// 새 블럭 안에 기록된 tx들 각각을 참조해 utxo 업데이트
})
}
이제 index.ts로 가서 /mineBlock 라우터의 미들웨어에 해당 기능을 넣어줘야 한다.
addBlock 대신 addBlock 함수 호출 + utxo 지급 기능을 가진 miningBlock 함수를 넣어주면 된다.
/* index.ts */
app.post('/mineBlock', (reqm res) => {
const { data } = req.body
const newBlock = ws.miningBlock(data) // addBlock이 아닌 miningBlock 함수 추가
if(newBlock.isError == true) {
return res.status(500).send(newBlock.error)
}
const msg : Message = {
type : MessageType.latest_block,
payload : {}
}
ws.broadcast(msg)
// 채굴이 잘 되었다면 다른 네트워크 참여자들이 정보를 업데이트 할 수 있도록
// 메시지를 보낸다
res.jsom(newBlock.value)
})
6. 채굴 tx 보상 확인
이제 채굴에 대한 보상이 잘 들어왔는지 확인을 해보자.
wallet class에 지갑 주소를 입력하면 그 지갑의 잔고 (utxo)를 리턴해주는 함수 getBalance는 다음과 같이 작성할 수 있다.
/* src/core/wallet/wallet.ts */
static getBalance(_account : string, _unspentTxOuts : IUnspentTxout[] ) : number {
return _unspentTxOuts
.filter ( _unspentTxOuts => {
return ( _unspentTxOuts.account == _account )
})
// utxo 데이터를 전부 가져와 주어진 account 앞으로 된 utxo만 걸러낸다
.reduce ( (acc, utxo) => {
return (acc += utxo.amount)
}, 0)
// 걸러진 데이터의 잔금을 전부 더해서 리턴하면 그게 지갑의 잔고가 된다
}
채굴을 n번 하면 unspentTxOut class 객체가 n개 생성된다.
getunspentTxOuts 함수를 이용해 생성된 객체들을 확인하면 이 역시 n개의 unspentTxOut class 객체를 출력해줄 것이다.
이를 콘솔로 출력해 확인한 후,
지갑 주소와 쌓인 utxo 데이터를 매개변수로 주고 잔고액수를 확인해 출력한다. (getBalance)
/* src/core/blockchain/chain.test.ts */
import { Chain } from '@core/blockchain/chain'
import { Wallet } from '@core/wallet/wallet'
describe('체인 기능 확인', () => {
let ws : Chain = new Chain()
it('채굴 기능 확인', () => {
ws.miningBlock('10187335f40af237c8fe4764bdabbf6f34c340ff')
ws.miningBlock('10187335f40af237c8fe4764bdabbf6f34c340ff')
ws.miningBlock('10187335f40af237c8fe4764bdabbf6f34c340ff')
console.log(ws.getunspentTxOuts())
const balance = Wallet.getBalance('10187335f40af237c8fe4764bdabbf6f34c340ff', ws.getunspentTxOuts())
console.log(balance)
})
})
채굴을 3번 진행했고, 이 채굴에 의해 발생한 utxo는 체인 상에 저장된다.
체인에서 생성된 utxo를 불러오면 채굴 시점에서 생성된 unspentTxOut 객체를 3개 가진 배열이 출력될 것이다.
이 다음, 채굴한 지갑(상단의 miningBlock 함수의 매개변수였던)의 잔고를 확인하기 위해
해당 지갑의 주소와 함께 체인의 utxo를 가져와 잔고를 확인해 콘솔로 출력한다.
각각의 결과는 다음과 같다.
[
unspentTxOut {
txOutId: '4d6d905ad04b0fd5b6fe36a760addb95bb60aa8d8c771b725e12523b19a13021',
txOutIndex: 0,
account: '10187335f40af237c8fe4764bdabbf6f34c340ff',
amount: 17
},
unspentTxOut {
txOutId: '4d6d905ad04b0fd5b6fe36a760addb95bb60aa8d8c771b725e12523b19a13021',
txOutIndex: 0,
account: '10187335f40af237c8fe4764bdabbf6f34c340ff',
amount: 17
},
unspentTxOut {
txOutId: '4d6d905ad04b0fd5b6fe36a760addb95bb60aa8d8c771b725e12523b19a13021',
txOutIndex: 0,
account: '10187335f40af237c8fe4764bdabbf6f34c340ff',
amount: 17
}
]
51
'Blockchain' 카테고리의 다른 글
#15 transaction ch.5 - utxo update (0) | 2022.06.27 |
---|---|
#14 transaction ch4. - transaction (0) | 2022.06.24 |
#12 Transaction ch2.코인 전송 (0) | 2022.06.22 |
#11 Transaction ch1.개념 (0) | 2022.06.22 |
#10 지갑, 개인키 (0) | 2022.06.20 |