이제 채굴자의 코인베이스 외에 지갑과 지갑간의 일반적인 tx를 구현해보자.
1. intro
채굴시 생성되는 utxo는 출금이 없이 입금만 있지만,
이번처럼 지갑에서 지갑으로 이동하는 tx는 TxIn에서
txOutId, _txOutIndex, _signature를 전부 적절히 채워주어야 한다.
출금하려는 지갑의 주소로 된 utxo를 찾아 서명을 추가해 TxIn의 데이터를 만들고
입금할 계좌, 금액 데이터로 TxOut 를 만든다.
이들을 전부 모아 transaction class 객체로 만든 후, 블록체인에 기록하면 된다.
지갑을 2개 만들어 하나는 출금 지갑, 하나는 입금 지갑으로 사용하자.
지갑1에서 지갑2로 코인을 보내려면 만들어둔 웹 페이지에서 보내는 사람의 지갑 주소와 코인의 양을 입력하고
전송버튼을 누르면 된다.
예전에 tx에 관한 정보를 모아 객체로 만들어주는 코드를 이미 만들어둔 적이 있었다.
예시로, 지갑1에서 지갑2로 100개의 코인을 보내면 다음과 같은 객체가 생성된다.
{
sender: '039daaca527baa13b7b576ff7875183dd52a96c6af98b9f64378488c94dd097f14',
receiver: '801f804d1bbb0ec8e088a10aceb63031a58c1923',
amount: 100,
signature: {
r: 'a2a5d3b7fcedae6cb65676b57d7c8d8e50f528854a7b0392e312a52ad1dadb5c',
s: '7e9e7c1e90abeca06d725ddb707fee2c7295b251bc047f7f08a4f1533db7ea19',
recoveryParam: 1
}
}
이 tx 객체는 블록체인 네트워크로 전송되어 검증 및 승인을 요청한다.
2. 블록 체인상에서의 tx 검증
위와 같이 tx가 생성되었다면 블록체인 상에서는 이 tx가 유효한지 검증해야한다.
2.1 잔금 액수 검증
검증 과정에서 첫 번째로 할 일은 출금지갑의 잔고를 확인해 출금하기에 충분한 액수가 남아있는지를 확인하는 것이다.
이 과정은 블록체인 서버에서 Wallet class가 담당해줄 것이다.
채굴자 보상에 대한 코드를 작성할 때 이미 공개키로 지갑 주소를 만드는 함수와,
지갑 주소를 주면 그 지갑의 잔고 액수를 구해주는 함수를 만들었었다.
이를 이용해 Wallet의 생성자 함수에서 balance를 구할 수 있다.
/* src/core/wallet/wallet.ts */
// ...중략
export class Wallet {
public publicKey : string
public account : string
public balance : number
public siganture : Signature
constructor( _sender : string, _signature : Signature, _unspentTxOuts : unspentTxOut[]) {
// 매개변수에 utxo 데이터들이 추가되었다
this.publicKey = _sender
this.account = Wallet.getAccount(this.publicKey)
this.balance = Wallet.getBalance(this.account, _unspentTxOuts)
// 받은 publicKey를 가지고 지갑 주소, 그 지갑의 잔금을 가져온다.
this.signature = _signature
}
static getAccount(_publicKey : string) : string {
return Buffer.from(_publicKey).slice(26).toString()
}
static getBalance ( _account : string, _unspentTxOuts : IUnspentTxOut[]) : number {
return _unspentTxOuts
.filter ( v=> {
return (v.account == _account)
})
.reduce((acc,utxo) => {
return (acc += utxo.amount)
},0 )
}
// ...중략
}
생성자 함수의 매개변수에 utxo 데이터가 추가되었다.
다음으로 서명을 검증한 후 (이 코드도 이미 작성했다) 문제가 없다면 지갑을 최신화하고
utxo 정보와 함께 Transaction 객체를 만들어주면 된다.
저번에 만들었던 sendTransaction 함수에 다음과 같이 코드를 추가해보자.
/* src/core/wallet/wallet.ts */
static sendTransaction ( _receivedTx : any, _unspentTxOuts : unspentTxOuts : unspentTxOut[]) {
const verify = Wallet.getVerify(_receivedTx)
if(verify.isError) throw new Error (verify.error)
// 서명을 검증하는 함수는 저번에 작성했다 (#12)
const myWallet = new this ( _receivedTx.sender, _receivedTx.signature, _unspentTxOuts )
// 지갑을 최신화
if (myWallet.balance < _receivedTx.amount) throw new Error ('잔액이 부족합니다.')
// 발신인의 잔고가 충분하지 않을 경우 에러 출력
const myUTXO : unspentTxOut[] = unspentTxOut.getMyUnspentTxOuts(myWallet.account, _unspentTxOuts)
// 자기 주소로 된 utxo를 모아 가져오는 함수 getMyunspentTxOuts (#13)
}
이제 utxo 데이터가 있으므로 TxIn, TxOut 데이터를 만들어 줄 수 있고
(정확히는 TxIn, TxOut을 생성하는 함수를 선언할 수 있다)
이를 이용해 transaction class를 생성할 수 있는 createTransaction 함수를 만들 수 있다.
createTransaction 함수를 작성하기에 앞서 그 재료가 되는 TxIn, TxOut 데이터를 만들어주는
createTxIns, createOuts 함수를 먼저 작성해보자.
/* src/core/transaction/txin.ts */
export class TxIn {
// ...중략
static createTxIns ( _receivedTx : any, myUTXO : IUnspentTxOut[]) {
let sum = 0
let txins : TxIn[] = []
for ( let i = 0; i < myUTXO.length; i++) {
const { txOutId, txOutIndex, amount } = myUTXO[i]
// 여기서 myUTXO는 내 지갑 주소로된 utxo만 걸러낸 상태이다
const item : TxIn = new TxIn(txOutId, txOutIndex, _recievedTx.signature)
// 잔금의 출처와 발신인의 서명
txins.push(item)
// 만든 TxIn을 배열에 추가
sum += amount
if (sum > _recievedTx.amount) return { sum, txins }
// 가져온 utxo의 잔금의 합이 출금액보다 커질때까지 utxo를 가져다 사용한다.
// 출금액 이상의 utxo가 모였으면 리턴
}
return { sum, txins }
}
// TxIn을 모은 배열을 생성하는 것임에 주의할 것
}
/* src/core/transaction/txout.ts */
// ...중략
export class TxOut {
// ...중략
static createTxOuts ( sum : number, _receivedTx : any ) : TxOut[] {
const { sender, receiver, amount } = _receivedTx
const senderAccount : string = Wallet.getAccount(sender)
const recievedTxOut = new TxOut(receiver, amount)
// 수신인에게 입금된 액수만큼의 utxo 추가
const senderTxOut = new Txout (senderAccount, sum - amount)
// 발신인에게 남은 액수만큼의 utxo 추가
// (원래 있던 utxo는 사라짐)
// sum의 값이 필요하기 때문에 TxIn을 먼저 생성해준다.
if (senderTxOut.amount == 0) return [receivedTxOut]
// 보낼 돈의 양과 가져온 utxo의 합과 정확히 동일하다면
// 발신인은 새로운 utxo를 생성해줄 필요가 없으므로
// 수신인의 새로운 utxo만을 생성해 리턴
return [ receivedTxOut, senderTxOut ]
// 발신인에게 돈이 남았다면 둘 모두의 utxo를 리턴
}
}
이제 이것들을 가지고 tx를 생성하는 함수 createTransaction을 만들 수 있다.
/* src/core/transaction/transaction.ts */
export class Transaction {
// ...중략
static createTransaction ( _receivedTx :any, myUTXO : unspentTxOut[]) : Transaction {
const { sum, txins } = TxIn.createTxIns(_receivedTx, myUTXO)
const txouts : TxOut[] = TxOut.createTxOuts(sum, _receivedTx)
const tx = new Transaction(txins, txouts)
return tx
}
}
이렇게 만들어준 createTransaction 함수를 sendTransaction 함수 내부에서 호출한다.
/* src/core/wallet/wallet.ts */
static sendTransaction ( _receivedTx : any, _unspentTxOuts : unspentTxOuts : unspentTxOut[]) {
const verify = Wallet.getVerify(_receivedTx)
if(verify.isError) throw new Error (verify.error)
// 서명을 검증하는 함수는 저번에 작성했다 (#12)
const myWallet = new this ( _receivedTx.sender, _receivedTx.signature, _unspentTxOuts )
// 지갑을 최신화
if (myWallet.balance < _receivedTx.amount) throw new Error ('잔액이 부족합니다.')
// 발신인의 잔고가 충분하지 않을 경우 에러 출력
const myUTXO : unspentTxOut[] = unspentTxOut.getMyUnspentTxOuts(myWallet.account, _unspentTxOuts)
// 자기 주소로 된 utxo를 모아 가져오는 함수 getMyunspentTxOuts (#13)
const tx : Transaction = Transaction.createTransaction(_recievedTx, myUTXO)
// 지갑 서버에서 보낸 tx데이터와 utxo를 이용해 tx를 만들어주는 함수
return tx
// 이렇게 만든 tx는 tx pool에 머무르다가 다음 블럭이 생성되면 블럭의 data로 들어간다
}
tx가 완료되었다면 체인상에서 수신인과 발신인의 utxo를 업데이트 해주어야 한다.
/* src/core/blockchain/chain.ts */
export class Chain {
// ...중략
public updateUTXO ( _tx : Transaction ) : void {
const unspentTxOuts : unspentTxOut[] = this.getunspentTxOuts()
// chain상의 utxo를 전부 가져온다
const newUnspentTxOuts = _tx.txOuts.map((txout, index) => {
return new unspentTxOut( tx.hash, index, txout.account, txout.amount )
}
// _tx에서 새로 등록된 utxo를 가져와 추가한다
this.unspentTxOuts = unspentTxOuts
.filter((utxo) => {
const bool = tx.txIns.find((txIn: TxIn) => {
return utxo.txOutId == txIn.txOutid && utxo.txOutIndex == txIn.txOutIndex
// 만들어진 tx의 txIns를 참조해 사용된 utxo를 가져온다
})
return !bool
// 사용된 utxo를 제외한 나머지 utxo들을 리턴
})
.concat(newUnspentTxOuts)
// 사용된 utxo를 뺀 utxo 배열을 newUnspentTxOuts에 추가한 값을
// chain의 unspentTxOuts에 대입한다.
}
}
이렇게 업데이트된 utxo 정보는 블럭이 새로 생성될 때 반영된다.
addBlock 함수에서 chain에 새로운 블럭을 추가한 후, updateUTXO 함수를 실행해주면 된다.
/* src/core/blockchain/chain.ts */
public addBlock (data : ITransaction[]) : Failable<Block, string> {
// ...중략
this.blockchain.push(newBlock)
newBlock.data.forEach((_tx : ITransaction) => {
this.updateUTXO(_tx)
})
// miningBlock 함수에서 전달받은 transaction 데이터를 참조해 utxo 업데이트
return { isError : false, value : newBlock }
}
이렇게 만들어진 함수 sendTransaction을 index.ts에서 sendTransaction 라우터의 미들웨어에서 실행하면 된다.
/* index.ts */
app.post('/sendTransaction', (req, res) => {
try {
const receivedTx : ReceivedTx = req.body
console.log(receivedTx)
Wallet.sendTransaction(receivedTx, ws.getunspentTxOuts())
// utxo 데이터를 매개변수로 추가
}
catch (e) {
if ( e instance of Error ) console.error(e.message)
}
})
정리해보면 발신인이 수신인에게 돈을 전송하는 것을 요청한다.
> 발신인의 공개키, 액수, 서명와 수신인의 지갑주소를 가지고 tx 데이터 객체를 블록 체인 서버로 보내 tx를 요청한다.
> 서버는 tx 데이터 객체를 분석해 tx를 처리할지 말지를 결정한다.
> 우선 서명이 유효한지 검증한다.
> 발신인의 지갑을 최신화한 후 잔액이 충분한지 확인한다.
> 서명, 잔액이 문제가 없다면 본격적으로 블록에 넣은 tx 데이터를 블록체인 상에서 만들기 시작한다.
> 출금할 돈의 출처, 발신인의 서명을 이용해 TxIn을 만든다.
> 수신인과 수신 금액으로 TxOut을 만든다.
( 이 때, 거스름돈이 있다면 다시 발신인에게 차액만큼의 수신 금액으로 TxOut을 만든다. )
> 만든 TxIn들, TxOut들, utxo를 가지고 transaction class 객체를 만든다.
> 만든 transaction class 객체를 리턴한다. ( 나중에 새 블럭이 만들어질 때, 데이터로 들어간다)
> 수신인과 발신인의 utxo를 업데이트 한다.
리턴된 transaction class 객체는 다음 블록이 생성될 때까지 잠시 tx pool에 머물게 된다.
다음 글에서는 이 tx pool이 어떤 식으로 블록의 tx 데이터 핸들링에 관여하는지 알아보자.
'Blockchain' 카테고리의 다른 글
#16 transaction ch.6 - tx pool (0) | 2022.06.27 |
---|---|
#15 transaction ch.5 - utxo update (0) | 2022.06.27 |
#13 tx ch3. 채굴자 보상 (0) | 2022.06.23 |
#12 Transaction ch2.코인 전송 (0) | 2022.06.22 |
#11 Transaction ch1.개념 (0) | 2022.06.22 |