지난 시간에 블록채굴에 대한 보상 내용이 담긴 코인베이스 Transaction을 생성해보았으니 오늘은 일반적인 거래내역이 담긴 Transaction을 만들어 보겠습니다.
export class unspentTxOut {
public txOutId: string;
public txOutIndex: number;
public account: string;
public amount: number;
constructor(_txOutId: string, _txOutIndex: number, _account: string, _amount: number) {
this.txOutId = _txOutId; // Transaction의 해쉬값
this.txOutIndex = _txOutIndex; // Transaction의 Output에 있는 각 객체의 Index번호
this.account = _account; // 지갑 주소
this.amount = _amount; // 금액
}
// 내 UTXO 내용을 가져옴
static getMyUspentTxOuts(_account: string, _unspentTxOuts: unspentTxOut[]): unspentTxOut[] {
// 전체 UTXO와 내 계정정보를 가지고 내 UTXO 정보를 가져와야합니다.
return _unspentTxOuts.filter((utxo: unspentTxOut) => {
return utxo.account === _account;
});
}
}
지난번에 만든
unspentTxOut Class
에 나의UTXO
내용을 가져오는getMyUspentTxOuts
메서드를 만들어 내 지갑주소애 해당하는UTXO
내용을 가져오게 하였습니다.
export class TxIn {
public txOutId: string; // Transaction 해시값
public txOutIndex: number; // Transaction Output 리스트의 배열의 인덱스 값
public signature?: string; // signature가 있을수도 있고 없을수도 있다.
constructor(_txOutId: string, _txOutIndex: number, _signature: string | undefined = undefined) {
this.txOutId = _txOutId;
this.txOutIndex = _txOutIndex;
this.signature = _signature;
}
static createTxIns(_receivedTx: any, _myUTXO: unspentTxOut[]) {
let sum = 0;
let txins: TxIn[] = [];
for (let i = 0; i < _myUTXO.length; i++) {
const { txOutId, txOutIndex, amount } = _myUTXO[i];
const item: TxIn = new TxIn(txOutId, txOutIndex, _receivedTx.signature);
txins.push(item);
sum += amount;
if (sum >= _receivedTx.amount) return { sum, txins };
}
return { sum, txins };
}
}
Transaction
의txin
내용을 만들기 위해txin class
에서createTxIns
메서드를 추가하여 내가 보내려는 금액보다 내UTXO
안에 있는 객체들의amout
를 합친 값이 더 클 경우에 바로 반복문을 종료하고 지금까지txins
에push
한 내용들만return
해줍니다.
조금 풀어서 설명하면 만약 내가 보내려는 금액이 100BTC 이고 내UTXO
에 60BTC, 60BTC, 50BTC가 있다고하면 반복문을 통해 60BTC , 60BTC만txins
에 추가하고return
하는 함수입니다.
import { Wallet } from '../wallet/wallet';
export class TxOut {
public account: string; // 지갑 주소
public amount: number; // 금액
constructor(_account: string, _amount: number) {
this.account = _account;
this.amount = _amount;
}
// 받는 사람 지갑 주소, 보내는 사람 지갑 주소, sum , amount
static createTxOut(_sum: number, _receivedTx: any): TxOut[] {
const { sender, received, amount } = _receivedTx;
const senderAccount: string = Wallet.getAccount(sender);
// 보내는 사람 txOut
const senderTxOut = new TxOut(senderAccount, _sum - amount);
// 받는 사람 txOut
const receivedTxOut = new TxOut(received, amount);
if (senderTxOut.amount === 0) return [receivedTxOut];
return [senderTxOut, receivedTxOut];
}
}
Transaction
의Output
내용을 만들기 위해txout Class
에서createTxOut
메서드를 추가하여 보내는 사람의TxOut
내용은UTXO
에서 가져온TxIns
의 금액에서 보내려는 금액_sum
을 뺀 값으로TxOut
을 생성해주고 받는 사람의TxOut
내용에는 보내는 사람이 보내려는 금액을 넣어TxOut
을 생성하도록 하였습니다.
만약 USER A가 보내려는 금액이 USER A의TxIns
의 금액과 같을경우에는 보내는 USER A의TxOut
의 금액이 0이기 때문에 이럴 경우에는 받는 사람의UTXO
만return
하도록 처리해주었습니다.
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[];
constructor(_txIns: TxIn[], _txOuts: TxOut[]) {
this.txIns = _txIns; // Transaction의 input의 내용들
this.txOuts = _txOuts; // Transaction의 output의 내용들
this.hash = this.createTransactionHash(); // Transaction의 고유한 값
}
// Transaction hash 만들기
createTransactionHash(): string {
// txoutput 내용의 각 객체의 value값들을 스트링으로 이어붙임
const txoutConmtent: string = this.txOuts.map((v) => Object.values(v).join('')).join('');
// txinput 내용의 각 객체의 value값들을 스트링으로 이어붙임
const txinContent: string = this.txIns.map((v) => Object.values(v).join('')).join('');
// 이어붙인 txoutConmtent + txinContent 값을 해쉬화하여 리턴
return SHA256(txoutConmtent + txinContent).toString();
}
// UTXO 생성하기 -> 배열안에 객체 형태로 저장
createUTXO(): unspentTxOut[] {
// txOutId = Transaction의 해쉬값
// txOutIndex = Transaction의 Output에 있는 각 객체의 Index번호
// account = 지갑 주소
// amount = 금액
return this.txOuts.map((v, i) => {
return new unspentTxOut(this.hash, i, v.account, v.amount);
});
}
static createTransaction(_receivedTx: any, _myUTXO: unspentTxOut[]): Transaction {
// Todo : 본인의 해당하는 UTXO -> uspentTxOut.ts -> getMyUspentTxOuts()
// UTXO -> TxIn 내용 생성 -> txin.ts -> createTxIns()
const { sum, txins } = TxIn.createTxIns(_receivedTx, _myUTXO);
// TxIn -> TxOut 내용 생성 -> txout.ts -> createTxOut()
const txOuts: TxOut[] = TxOut.createTxOut(sum, _receivedTx);
// 새로운 Transaction 생성 new Transaction()
const transaction = new Transaction(txins, txOuts);
return transaction;
}
}
마지막으로
Transaction Class
에서Transaction
을 생성하는createTransaction
메서드를 생성해주었습니다.TxIn Class
의createTxIns
메서드를 가지고TxIns
를 생성하고TxOut Class
의createTxOut
메서드를 가지고TxOut
을 생성해준뒤TxIns
내용과TxOuts
내용을 가지고 새로운Transaction
을 생성해주고 만든Transaction
을return
하도록 처리하였습니다.
/* 지갑 서버 */
// 트랜잭션 보내기
app.post('/sendTransaction', async (req, res) => {
const {
sender: { account, publicKey },
received,
amount,
} = req.body;
const signature = Wallet.createSign(req.body);
const txObj = {
sender: publicKey, // 공개키
received, // 받는 지갑주소
amount, // 금액
signature, // 서명
};
const response = await request.post('/sendTransaction', txObj);
res.json({});
});
Wallet
서버에서req.body
로 받은 보내는 지갑 주소와 공개키, 받는 지갑 주소, 보낼 금액을 가지고signature
을 생성해준뒤 공개키, 받는지갑 주소, 금액, 서명을txObj
객체에 담아서 블록체인 HTTP 서버로 요청을 보내줍니다.
/* 블록체인 HTTP 서버 */
app.post('/sendTransaction', (req, res) => {
try {
const receivedTx: ReceviedTx = req.body;
// 새로운 Transaction을 생성합니다.
const transaction = Wallet.sendTransaction(receivedTx, ws.getUnspentTxOuts());
// TransactionPool에 Transaction 내용 추가
ws.appendTransactionPoll(transaction);
// UTXO 내용 수정 / UTXO 내용을 최신화하는 함수를 생성합니다. 인자값 Transaction
ws.updateUTXO(transaction);
// 트랜잭션이 발동할때마다 브로드 캐스트해주어야 합니다.
const mesasge: Message = {
type: MessageType.receivedTx,
payload: transaction,
};
// 다른 노드에게 브로드 캐스트로 트랜잭션 내용을 전달해줍니다.
ws.broadcast(mesasge);
} catch (e: any) {
if (e instanceof Error) console.error(e.message);
}
res.json([]);
});
/* wallet.ts - sendTransaction 메서드 */
static sendTransaction(_receivedTx: any, _unspentTxOuts: unspentTxOut[]): Transaction {
// Todo : 서명 검증
// 보내는사람:공개키, 받는사람:계정, 보낼금액
const verify = Wallet.getVerify(_receivedTx);
if (verify.isError) throw new Error(verify.error);
console.log(verify.isError);
// Todo : 보내는 사람의 지갑 정보 최신화
const myWallet = new this(_receivedTx.sender, _receivedTx.signature, _unspentTxOuts);
// Todo : Balance 확인
if (myWallet.balance < _receivedTx.amount) throw new Error('잔액이 부족합니다.');
// Todo : Transaction 만드는 과정
const myUTXO: unspentTxOut[] = unspentTxOut.getMyUspentTxOuts(myWallet.account, _unspentTxOuts);
const transaction: Transaction = Transaction.createTransaction(_receivedTx, myUTXO);
return transaction;
}
/* wallet.ts - getVerify 메서드 */
// 서명 검증
static getVerify(_receivedTx: ReceviedTx): Failable<undefined, string> {
const { sender, received, amount, signature } = _receivedTx;
const data: [string, string, number] = [sender, received, amount];
const hash: string = SHA256(data.join('')).toString(); // data를 hash화
// Todo : 타원곡선 알고리즘 사용
const keyPair = ec.keyFromPublic(sender, 'hex');
// 서명 검증하는 부분 ( 데이터 위.변조 , 신원증명 )
const isVerify = keyPair.verify(hash, signature);
if (!isVerify) return { isError: true, error: '서명이 올바르지 않습니다.' };
return { isError: false, value: undefined };
}
블록체인 HTTP서버에서
Wallet
서버에서 받은txObj
객체와 보내려는 사람의UTXO
내용을가지고sendTransactrion
메소드로Trnasaction
을 생성해줍니다.
Wallet Class
에 있는sendTransaction
메소드를 보면 우선signature
를getVerify
메소드를 사용하여 검증을 해주고 보내려는 사람의 최신화된 지갑정보를 만들고 혹시 보내려는 사람의 지갑에 금액이 보내고 싶은 금액보다 적을경우에는Error
로 빠지도록 처리하였습니다.
이제 보내려는 사람의 해당하는UTXO
정보와 인자값으로 받은txObj
내용을 가지고Transaction Class
에서 미리 만들어둔createTransaction
메서드로Transaction
을 생성해줍니다.