지난 시간에 블록채굴에 대한 보상 내용이 담긴 코인베이스 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을 생성해줍니다.