이번 포스팅에서는 저번 포스팅에 이어서 일반적인 트랜잭션 데이터를 만들어 보도록 하겠다.
이전 글)
2022.06.22 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (2) 코인베이스 트랜잭션
< 목차 >
- 트랜잭션 클래스 수정
- wallet.ts 파일 수정
1. 트랜잭션 클래스 수정
현재 src/core/transaction/ 디렉토리 안에 있는 transaction.ts , txin.ts , txout.ts , unspentTxOut.ts 파일에서 작성된 트랜잭션 관련 클래스들은 다음과 같다.
// txin.ts 파일
export class TxIn {
public txOutId: string;
public txOutIndex: number; // txOuts 배열 인덱스값 / 코인베이스일 경우 블록의 높이로
public signature?: string; // wallet -> blockchain server 로 들어올 때 string으로 들어온다.
// TxIn에는 서명이 없을 수도 있다. (ex. 코인베이스 트랜잭션)
constructor(_txOutId: string, _txOutIndex: number, _signature: string | undefined = undefined) {
this.txOutId = _txOutId;
this.txOutIndex = _txOutIndex;
this.signature = _signature;
}
}
// txout.ts 파일
export class TxOut {
public account: string;
public amount: number;
constructor(_account: string, _amount: number) {
this.account = _account;
this.amount = _amount;
}
}
// unspentTxOut.ts 파일
export class UnspentTxOut {
public txOutId: string; // transaction hash값
public txOutIndex: number;
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;
}
}
// transaction.ts 파일
import { TxIn } from './txin';
import { TxOut } from './txout';
import { UnspentTxOut } from './unspentTxOut';
import { SHA256 } from 'crypto-js';
export class Transaction {
public hash: string; // 해당 트랜잭션의 고유한 값
public txIns: TxIn[];
public txOuts: TxOut[];
constructor(_txIns: TxIn[], _txOuts: TxOut[]) {
this.txIns = _txIns;
this.txOuts = _txOuts;
this.hash = this.createTransactionHash();
}
// 해당 메소드는 인스턴스 생성 후에 만들어진다.
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();
}
createUTXO(): UnspentTxOut[] {
const result: UnspentTxOut[] = this.txOuts.map((txout: TxOut, index: number) => {
return new UnspentTxOut(this.hash, index, txout.account, txout.amount);
});
return result;
}
}
이제 각각의 클래스 안에서 메소드를 추가하여 트랜잭션 데이터를 만들기 용이하게 하고자 한다. transaction 객체를 만들어 줄때 txIns[ ] 속성에 들어갈 txIn 객체들은 UTXO 배열 안에서 조회해야 한다. UnspentTxOut 클래스 안에서 트랜잭션을 발생시킨 계정의 unspentTxOut 객체를 가져오는 함수를 다음과 같이 만들어주었다.
// unspentTxOut.ts 파일, getMyUnspentTxOuts() 메소드 추가
export class UnspentTxOut {
public txOutId: string; // transaction hash값
public txOutIndex: number;
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;
}
// 필요한 인자값 : 전체 UTXO , 내 계정(account)
static getMyUnspentTxOuts(_account: string, _unspentTxOuts: UnspentTxOut[]): UnspentTxOut[] {
return _unspentTxOuts.filter((utxo: UnspentTxOut) => {
return utxo.account === _account;
});
}
}
static 메소드로 만들어준 getMyUnspentTxOuts( ) 함수는 계정(account)과 전체 UTXO 배열을 매개변수로 하고 있다. 해당 메소드를 사용하면 UTXO 배열 안에 존재하는 unspentTxOut 객체들 중에서 트랜잭션을 발생시킨 계정의 unspentTxOut 객체들을 가져올 수 있다.
다음으로 TxIn 클래스 안에서 UTXO 배열에서 가져온 unspentTxOut 객체들을 사용해 txIns[ ] 배열을 만들어주는 메소드를 만들어주었다.
// txin.ts 파일, createTxIns() 메소드 추가
export class TxIn {
public txOutId: string;
public txOutIndex: number; // txOuts 배열 인덱스값 / 코인베이스일 경우 블록의 높이로
public signature?: string; // wallet -> blockchain server 로 들어올 때 string으로 들어온다.
// TxIn에는 서명이 없을 수도 있다. (ex. 코인베이스)
constructor(_txOutId: string, _txOutIndex: number, _signature: string | undefined = undefined) {
this.txOutId = _txOutId;
this.txOutIndex = _txOutIndex;
this.signature = _signature;
}
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];
const item: TxIn = new TxIn(txOutId, txOutIndex, _receivedTx.signature);
txIns.push(item);
sum += amount;
if (sum >= _receivedTx.amount) return { sum, txIns };
}
return { sum, txIns };
}
}
createTxIns( ) 메소드는 receivedTx 와 myUTXO를 인자값으로 받고 있다. receivedTx는 지갑 서버쪽에서 블록체인 HTTP 서버쪽으로 전달한 데이터이며 다음과 같은 형태를 갖고 있다.
receivedTx = {
sender: '03a8aca5135a59c8493d69c75323d848b964dc1b930f25fdb84d7442de6fe7448c',
received: 'd8d268f4eb3c7c2701c615474c67f61503e0b11c',
amount: 10,
signature: {
r: '8e29f9a2e80fa683f027a14acdc29009567c1071eb0089880e7cb68d0f33a5b9',
s: '485333886b781d014b0d982cc22ecb81cefa785fbf57c7449b8adc966fe2eabe',
recoveryParam: 0
}
}
- sender : 트랜잭션을 발생시킨 계정 즉, 코인을 보내는 사람의 계정 (현재 우리는 계정이 아닌 공개키를 사용)
- received : 코인을 받는 사람의 계정
- amount : 보낼 금액
- signature : 코인을 보내는 사람의 서명
또 하나의 인자값인 myUTXO는 UTXO 배열에서 조회한 unspentTxOut 객체들 중 보내는 사람의 계정과 일치하는 unspentTxOut 객체들이 담겨있는 배열이다.
createTxIns( ) 메소드 안에서는 myUTXO 배열 안에 존재하는 unspentTxOut 객체들의 amount 속성값을 합산하게 되는데 합산한 값(sum)이 receivedTx 객체안의 amount 값보다 크거나 같게 될 경우에 해당하는 만큼의 unspentTxOut 객체들을 txIns[ ] 배열 안에 담아준다. 그리고 합산한 값(sum)과 txIns 배열을 객체 안에 담아서 반환해준다. sum을 반환해주는 이유는 향후 보내는 사람의 txOut 객체를 만들 때 amount 속성값을 구하는데 사용되기 때문이다.
이제 TxOut 클래스 안에서 txOuts[ ] 배열을 만들어주는 메소드를 추가하고자 한다.
// txout.ts 파일, createTxOuts() 메소드 추가
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;
}
// 필요한 인자값 : 보내는사람 계정, 받는사람 계정, sum, amount
/*
받는사람 계정, amount
보내는사람 계정, sum - amount
*/
static createTxOuts(_sum: number, _receivedTx: any): TxOut[] {
// ToDo : _receivedTx any 타입 변경
/*
_receivedTx.amount; // 보낼금액
_receivedTx.sender; // 보내는사람 공개키
_receivedTx.received; // 받는사람 계정
*/
const { sender, received, amount } = _receivedTx;
const senderAccount: string = Wallet.getAccount(sender);
// 받는사람 txOut
const receivedTxOut = new TxOut(received, amount);
// 보내는사람 txOut
const senderTxOut = new TxOut(senderAccount, _sum - amount);
if (senderTxOut.amount <= 0) return [receivedTxOut];
return [receivedTxOut, senderTxOut];
}
}
createTxOuts( ) 메소드를 새롭게 추가하였는데 인자값으로 sum 과 receivedTx를 받게끔 하였다. receivedTx 객체의 sender 속성값(보내는 사람의 공개키)을 Wallet.getAccount( ) 메소드의 인자값으로 전달하여 보내는 사람의 계정을 구하였다. 받는 사람의 txOut은 new TxOut( received, amount ) 을 통해 만들어주었다. 그리고 보내는 사람의 unspentTxOut 객체들의 amount 속성값을 합산한 sum 에서 보낼 금액을 의미하는 receivedTx.amount 값을 뺀 금액만큼 new TxOut( )의 인자값으로 집어넣어 보내는 사람의 txOut도 만들어주었다.
마지막으로 Transaction 클래스를 수정해보도록 하자.
// transaction.ts 파일, createTransaction() 메소드 추가
import { TxIn } from './txin';
import { TxOut } from './txout';
import { UnspentTxOut } from './unspentTxOut';
import { SHA256 } from 'crypto-js';
export class Transaction {
public hash: string; // 해당 트랜잭션의 고유한 값
public txIns: TxIn[];
public txOuts: TxOut[];
constructor(_txIns: TxIn[], _txOuts: TxOut[]) {
this.txIns = _txIns;
this.txOuts = _txOuts;
this.hash = this.createTransactionHash();
}
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();
}
createUTXO(): UnspentTxOut[] {
const utxo: UnspentTxOut[] = this.txOuts.map((txout: TxOut, index: number) => {
return new UnspentTxOut(this.hash, index, txout.account, txout.amount);
});
return utxo;
}
static createTransaction(_receivedTx: any, _myUTXO: UnspentTxOut[]): Transaction {
// ToDo : 트랜잭션 만들 때는 UTXO가 필수. utxo <- 내 계정과 일치하는
// 1. utxo -> txIns[]
const { sum, txIns } = TxIn.createTxIns(_receivedTx, _myUTXO);
// 2. txIn -> txOuts[]
const txOuts: TxOut[] = TxOut.createTxOuts(sum, _receivedTx);
// 3. new Transaction()
const tx = new Transaction(txIns, txOuts);
return tx;
}
}
Transaction 클래스 안에서는 트랜잭션 객체를 생성해주는 createTransaction( ) 메소드를 추가로 만들어주었다. 해당 메소드에서는 TxIn 클래스의 TxIn.createTxIns( ) 메소드 , TxOut 클래스의 TxOut.createTxOuts( ) 메소드를 사용해 txIns[ ] 배열과 txOuts[ ] 배열을 만들고 new Transaction( txIns, txOuts ) 를 통해 transaction 객체(tx)를 생성하여 return 해주었다.
2. wallet.ts 파일 수정
현재 지갑 서버 쪽에서 블록체인 HTTP 서버 쪽 "/sendTransaction" 라우터로 요청을 보냈을 때 다음과 같은 코드가 작동한다.
// 블록체인 HTTP 서버 (루트 디렉토리 index.ts 파일)
// sendTransaction 라우터
app.post('/sendTransaction', (req, res) => {
/* receivedTx 내용
{
sender: '02193cc6051f36c77b7dd92d21513b6517f5f8c7efca0f10441a8fa9c52b4fae2f',
received: 'c0b87bcc610be3bf7d3b26f6dd6ae0a63bb97082',
amount: 10,
signature: Signature {
r: BN { negative: 0, words: [Array], length: 10, red: null },
s: BN { negative: 0, words: [Array], length: 10, red: null },
recoveryParam: 0
}
}
*/
// 트랜잭션 entry point - sendTransaction()
try {
const receivedTx: ReceivedTx = req.body;
Wallet.sendTransaction(receivedTx); // 블록체인 네트워크의 entry point(진입점)
} catch (e) {
if (e instanceof Error) console.log(e.message);
}
res.json({});
});
지갑 서버로부터 전달받은 receivedTx 객체를 Wallet.sendTransaction( ) 메소드의 인자값으로 전달하여 아래와 같이 서명을 검증하는 부분까지 완료한 상황이다.
// src/core/wallet/wallet.ts 파일 내용
import { SHA256 } from "crypto-js";
import elliptic from "elliptic";
const ec = new elliptic.ec("secp256k1");
export type Signature = elliptic.ec.Signature;
export interface ReceivedTx {
sender: string;
received: string;
amount: number;
signature: Signature;
}
export class Wallet {
public publicKey: string;
public account: string;
public balance: number;
public signature: Signature;
constructor(_sender: string, _signature: Signature) {
this.publicKey = _sender;
this.account = Wallet.getAccount(this.publicKey);
this.balance = 0;
this.signature = _signature;
}
static sendTransaction(_receivedTx: ReceivedTx) {
// ToDo : 서명 검증
// 공개키를 사용해 서명 검증,
// hash값: 보내는사람:공개키, 받는사람:계정, 보낼금액
const verify = Wallet.getVerify(_receivedTx);
if (verify.isError) throw new Error(verify.error);
// ToDo : 보내는 사람의 지갑정보 최신화
// 현재 가지고 있는 정보:publicKey, 실제 transaction 안에 넣을 정보는 account 정보
const myWallet = new this(_receivedTx.sender, _receivedTx.signature);
// ToDo : Balance 확인
// ToDo : Transaction 만드는 과정
}
static getVerify(_receivedTx: ReceivedTx): Failable<undefined, string> {
const { sender, received, amount, signature } = _receivedTx;
const data: [string, string, number] = [sender, received, amount];
const hash: string = SHA256(data.join("")).toString();
// ToDo : 타원곡선 알고리즘 사용 -> 공개키를 이용해 서명 검증
const keyPair = ec.keyFromPublic(sender, "hex");
const isVerify = keyPair.verify(hash, signature);
// const isVerify = ec.verify(hash, signature, keyPair);
if (!isVerify) return { isError: true, error: "서명이 올바르지 않습니다." };
return { isError: false, value: undefined };
}
static getAccount(_publicKey: string): string {
return Buffer.from(_publicKey).slice(26).toString();
}
}
추가로 만들어줘야 할 부분은 다음과 같다.
- 코인을 보내는 사람의 계정 안에 있는 잔액 확인하기
- receivedTx 안에 있는 정보들을 사용해 트랜잭션 데이터 생성하기
코인을 보내는 사람의 계정에 있는 잔액을 확인하기 위해 Wallet 클래스 안에서 아래와 같은 getBalance( ) 메소드를 만들어주었다.
// 필요한 값 : account , unspentTxOut[]
static getBalance(_account: string, _unspentTxOuts: IUnspentTxOut[]): number {
return _unspentTxOuts
.filter((v) => {
return v.account == _account;
})
.reduce((acc, utxo) => {
return (acc += utxo.amount);
}, 0);
}
계정의 잔액을 조회하기 위해서는 UTXO 배열 안에 있는 unspentTxOut 객체들이 필요하기 때문에 getBalance( ) 메소드의 인자값으로 accout(계정) 과 UnspentTxOuts[ ] 배열을 받고 있다. 따라서 sendTransaction( ) 메소드 역시 UnspentTxOut[ ] 배열을 매개변수로 추가하고 Wallet 클래스의 constructor( ) 함수도 UnspentTxOuts[ ] 배열을 매개변수로 추가하였다. 그리고 this.balance = 0 부분을 this.balance = Wallet.getBalance( this.account , _unspentTxOuts ) 과 같이 수정하였다.
// src/core/wallet/ 디렉토리 wallet.ts 파일
import { Transaction } from '@core/transaction/transaction';
import { UnspentTxOut } from '@core/transaction/unspentTxOut';
import { SHA256 } from 'crypto-js';
import elliptic from 'elliptic';
const ec = new elliptic.ec('secp256k1');
export type Signature = elliptic.ec.Signature;
export interface ReceivedTx {
sender: string;
received: string;
amount: number;
signature: Signature;
}
export class Wallet {
public publicKey: string;
public account: string;
public balance: number;
public signature: Signature;
constructor(_sender: string, _signature: Signature, _unspentTxOuts: IUnspentTxOut[]) {
this.publicKey = _sender;
this.account = Wallet.getAccount(this.publicKey);
this.balance = Wallet.getBalance(this.account, _unspentTxOuts);
this.signature = _signature;
}
static sendTransaction(_receivedTx: any, _unspentTxOuts: IUnspentTxOut[]): Transaction {
// ToDo : 완성후 _receivedTx: any 부분 수정하기
// ToDo : 서명 검증
// 공개키를 사용해 서명 검증,
// hash값: 보내는사람:공개키, 받는사람:계정, 보낼금액
const verify = Wallet.getVerify(_receivedTx);
if (verify.isError) throw new Error(verify.error);
console.log(verify.isError);
// ToDo : 보내는 사람의 지갑정보 최신화
// 현재 가지고 있는 정보:publicKey, 실제 transaction 안에 넣을 정보는 account 정보
const myWallet = new this(_receivedTx.sender, _receivedTx.signature, _unspentTxOuts);
// ToDo : Balance 확인 (보내는 사람 계정 잔액과 보낼금액 비교)
if (myWallet.balance < _receivedTx.amount) throw new Error('잔액이 모자릅니다.');
// 보내는 사람 계정에 해당하는 utxo
const myUTXO: UnspentTxOut[] = UnspentTxOut.getMyUnspentTxOuts(myWallet.account, _unspentTxOuts);
const tx: Transaction = Transaction.createTransaction(_receivedTx, myUTXO);
return tx;
}
static getVerify(_receivedTx: ReceivedTx): Failable<undefined, string> {
const { sender, received, amount, signature } = _receivedTx;
const data: [string, string, number] = [sender, received, amount];
const hash: string = SHA256(data.join('')).toString();
// ToDo : 타원곡선 알고리즘 사용 -> 공개키를 이용해 서명 검증
const keyPair = ec.keyFromPublic(sender, 'hex');
const isVerify = keyPair.verify(hash, signature);
// const isVerify = ec.verify(hash, signature, keyPair);
if (!isVerify) return { isError: true, error: '서명이 올바르지 않습니다.' };
return { isError: false, value: undefined };
}
static getAccount(_publicKey: string): string {
return Buffer.from(_publicKey).slice(26).toString();
}
// 필요한 값 : account , unspentTxOut[]
static getBalance(_account: string, _unspentTxOuts: IUnspentTxOut[]): number {
return _unspentTxOuts
.filter((v) => {
return v.account == _account;
})
.reduce((acc, utxo) => {
return (acc += utxo.amount);
}, 0);
}
}
이제 보내는 사람의 계정(myWallet)에 있는 잔액과 보낼 금액(_receivedTx.amount) 을 비교해서 부족하지 않을 경우에만 트랜잭션 객체(tx)를 만들어주게끔 하였다. 트랜잭션 객체(tx)를 생성할 때는 앞서 만들어 놓은 UnspentTxOut 클래스의 UnspentTxOut.getMyUnspentTxOuts( ) 메소드를 사용해 보내는 사람의 계정에 해당하는 UTXO 배열(myUTXO)을 만들고 receivedTx 객체와 myUTXO 배열을 Transaction.createTransaction( ) 메소드의 인자값으로 사용하였다.
마지막으로 블록체인 HTTP 서버 쪽 "/sendTransaction" 라우터 안에서 호출되는 Wallet.sendTransaction( ) 메소의 인자값으로 unspentTxOuts[ ] 배열을 넣어주기만 하면 된다. unspentTxOuts[ ] 배열을 가져오는 메소드를 Chain 클래스 안에서 만들어 놓았으므로 ws.getUnspentTxOuts( ) 를 인자값으로 넣어주도록 하자.
// 블록체인 HTTP 서버 (루트 디렉토리 index.ts 파일)
// /sendTransaction 라우터 수정
app.post('/sendTransaction', (req, res) => {
/* receivedTx 내용
{
sender: '02193cc6051f36c77b7dd92d21513b6517f5f8c7efca0f10441a8fa9c52b4fae2f',
received: 'c0b87bcc610be3bf7d3b26f6dd6ae0a63bb97082',
amount: 10,
signature: Signature {
r: BN { negative: 0, words: [Array], length: 10, red: null },
s: BN { negative: 0, words: [Array], length: 10, red: null },
recoveryParam: 0
}
}
*/
// 트랜잭션 entry point -> sendTransaction()
try {
const receivedTx: ReceivedTx = req.body;
// 블록체인 네트워크의 entry point(진입점)
Wallet.sendTransaction(receivedTx, ws.getUnspentTxOuts());
// txIns , txOuts
// utxo[] - txIns + txOuts
// UTXO 내용을 최신화하는 함수의 인자값 : 트랜잭션 객체
} catch (e) {
if (e instanceof Error) console.log(e.message);
}
res.json({});
});
현재 지갑 서버 쪽에서 블록체인 HTTP 서버 쪽으로 전송한 트랜잭션 데이터를 사용해 트랜잭션 객체를 생성해주는 부분까지 작업을 완료하였다. 다음번 포스팅에서는 트랜잭션 객체가 생성되었을 때 UTXO 배열 안에서 사용된 unspentTxOut 객체들을 제거하고 새로 만들어진 txOut 객체들을 UTXO 배열 안에 추가하는 작업을 진행해보고자 한다. 그리고 생성된 트랜잭션 객체들을 모아놓는 트랜잭션 풀 부분도 만들어보고자 한다.
다음 글)
2022.06.24 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (4) 트랜잭션 풀 / 멤풀
'BlockChain' 카테고리의 다른 글
BlockChain - 블록체인 지갑 서버 만들기 (4) 트랜잭션 풀 / 멤풀 (3) | 2022.06.24 |
---|---|
BlockChain - 블록체인 지갑 서버 만들기 (2) 코인베이스 트랜잭션 (0) | 2022.06.22 |
BlockChain - [블록체인 네트워크] 트랜잭션 (0) | 2022.06.21 |
BlockChain - 블록체인 지갑 서버 만들기 (1) (0) | 2022.06.20 |
BlockChain - 개인키, 공개키, 서명, 지갑/계정 (1) | 2022.06.17 |