이번 포스팅에서는 저번 포스팅에 이어서 일반적인 트랜잭션 데이터를 만들어 보도록 하겠다.
이전 글)
2022.06.22 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (2) 코인베이스 트랜잭션
BlockChain - 블록체인 지갑 서버 만들기 (2) 코인베이스 트랜잭션
이번 포스팅에서는 이전까지의 내용들을 토대로 트랜잭션을 만들어주는 과정에 대해 다뤄보고자 한다. 이전 글) 2022.06.20 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (1) BlockChain - 블록체
bitkunst.tistory.com
< 목차 >
- 트랜잭션 클래스 수정
- 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 - 블록체인 지갑 서버 만들기 (4) 트랜잭션 풀 / 멤풀
이번 포스팅에서는 저번 포스팅에서 못다한 작업들을 마무리하고자 한다. 이전 글) 2022.06.23 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (3) 트랜잭션 BlockChain - 블록체인 지갑 서버 만들기
bitkunst.tistory.com
'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 |