이번 포스팅에서는 저번 포스팅에서 못다한 작업들을 마무리하고자 한다.
이전 글)
2022.06.23 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (3) 트랜잭션
현재 특정 지갑 프로그램(클라이언트)을 사용하는 A 계정 소유자가 B 계정에게 10 BTC를 전송하겠다는 요청을 보내면 지갑 서버 쪽에서는 서명과 함께 트랜잭션 관련 데이터를 만들고 블록체인 HTTP 서버 쪽에 해당 데이터를 전송하고 있다. 그리고 트랜잭션 관련 데이터를 전달받은 블록체인 HTTP 서버는 해당 데이터를 토대로 트랜잭션 객체를 생성해주게 된다. 추가로 진행해줘야 할 부분은 트랜잭션 객체가 생성되었을 때 UTXO 배열 안에서 사용된 unspentTxOut 객체들을 제거하고 트랙잭션을 통해 새로 생겨난 unspentTxOut 객체들을 UTXO에 추가시켜주는 작업이다. 다시말해, 트랜잭션 객체가 생성되고 나서 UTXO 내용을 업데이트 해주는 부분이라고 생각하면 된다.
UTXO 내용을 업데이트 해줬다면 이제 "트랜잭션 풀" 이라는 공간을 만들어줘야 한다. 트랜잭션이 발생하고 트랜잭션 객체가 생성되었을 때 만들어진 트랜잭션들은 "트랜잭션 풀(Transaction Pool)"이라는 공간 안에 담기게 된다. 그리고 블록체인 네트워크 상에서 서로 연결된 노드들은 트랜잭션 풀의 내용을 공유한다. 특정 노드에서 블록을 마이닝 할 경우 트랜잭션 풀 안에 있는 트랜잭션 객체들을 사용해 새로 생성되는 블록의 data 속성값으로 집어넣게 된다. 따라서 블록이 마이닝될 때 트랜잭션 풀을 업데이트해주는 부분 역시 구현이 필요하다. ( 참고로 비트코인 네트워크에서는 이러한 트랜잭션 풀을 멤풀(Mempool)이라고 한다. )
마지막으로 블록체인 네트워크 상의 노드들이 트랜잭션 풀의 내용을 공유하는 부분에 대한 작업이 필요한데, 해당 부분은 트랜잭션 객체가 생성되었을 때 트랜잭션 객체 자체를 broadcast 하는 방식으로 간단하게 구현해보고자 한다.
< 목차 >
- UTXO 업데이트
- 트랜잭션 풀(멤풀) 만들기
- 트랜잭션 broadcast
1. UTXO 업데이트
앞서 설명한 것과 같이 UTXO 배열 안에서 사용된 unspentTxOut 객체들을 제거하고 트랜잭션을 통해 새로 생겨난 unspentTxOut 객체들을 추가해주는 작업을 진행해 보도록 하겠다. Chain 클래스 안에서 트랜잭션 객체(_tx)를 인자값으로 받는 updateUTXO( ) 메소드를 아래와 같이 만들어주었다.
// chain.ts 파일, Chain 클래스, updateUTXO() 메소드 추가
updateUTXO(_tx: ITransaction): void {
// UnspentTxOuts
/**
* {
* txOutId
* txOutIndex
* account
* amount
* }
*/
const unspentTxOuts: UnspentTxOut[] = this.getUnspentTxOuts();
// UTXO에 추가할 unspentTxOut 객체 생성
const newUnspentTxOuts = _tx.txOuts.map((txout, index) => {
return new UnspentTxOut(_tx.hash, index, txout.account, txout.amount);
});
// UTXO에서 사용한 unspentTxOut 객체 제거 & 생성된 unspentTxOut 객체 UTXO에 추가
const tmp = unspentTxOuts
.filter((_v: UnspentTxOut) => {
const bool = _tx.txIns.find((v: TxIn) => {
return _v.txOutId === v.txOutId && _v.txOutIndex === v.txOutIndex;
});
// !undefined == true
// !{ } == false
return !bool;
})
.concat(newUnspentTxOuts);
let unspentTmp: UnspentTxOut[] = [];
const result = tmp.reduce((acc, utxo) => {
const find = acc.find(({ txOutId, txOutIndex }) => {
return txOutId === utxo.txOutId && txOutIndex === utxo.txOutIndex;
});
if (!find) acc.push(utxo);
return acc;
}, unspentTmp);
this.unspentTxOuts = result;
}
updateUTXO( ) 메소드 안에서 실행되는 코드의 내용은 다음과 같다.
- this.getUnspentTxOuts( ) 를 사용해 UTXO 배열(unspentTxOuts)을 가져온다.
- 트랜잭션 객체(_tx)의 txOuts[ ] 배열 안에 있는 txOut 객체들을 사용해 새로 생성될 unspentTxOut 객체들(newUnspentTxOuts)을 만들어준다.
- unspentTxOuts 배열에서 .filter( ) 메소드와 .concat( ) 메소드를 이용해 사용된 txOut 객체들은 제거하고 생성된 unspentTxOut 객체들은 추가해준다. 그리고 그 결과를 tmp 변수에 할당한다.
현재 우리가 만든 지갑 프로그램(클라이언트)에서는 A 계정에서 B 계정으로 코인을 전송했을 때 UTXO를 업데이트해서 A 계정의 작액 변화를 실시간으로 조회할 수 있도록 하고자 한다. 이에 블록체인 HTTP 서버 쪽 "/sendTransaction" 라우터로 요청이 들어왔을 때 updateUTXO( ) 를 호출하게 된다. 또한 블록을 마이닝 할 때 실행되는 addBlock( ) 함수 안에서도 updateUTXO( ) 함수를 호출해 블록이 생성될 때 만들어지는 코인베이스 트랜잭션과 함께 블록의 data 속성값에 들어가는 트랜잭션 객체들로 UTXO를 업데이트해줘야만 한다. 따라서 다음의 과정을 추가적으로 진행해주었다.
→ 4. tmp.reduce( ) 메소드를 사용해 updateUTXO( ) 메소드가 두번 호출되더라도 UTXO 배열 안에 중복되는 unspentTxOut 객체들이 추가되지 않도록 해준다.
2. 트랜잭션 풀(멤풀) 만들기
updateUTXO( ) 메소드를 만들어 주었으니 이제 Chain 클래스 안에서 다음과 같이 트랜잭션 풀을 만들어주도록 하자.
// chain.ts 파일, Chain 클래스, transactionPool 속성 추가
private blockchain: Block[];
private unspentTxOuts: IUnspentTxOut[];
private transactionPool: ITransaction[];
constructor() {
this.blockchain = [Block.createGENESIS(Block.getGENESIS())];
this.unspentTxOuts = [];
this.transactionPool = [];
}
그리고 트랜잭션 풀을 반환하는 getTransactionPool( ) 메소드 , 트랜잭션 풀에 트랜잭션 객체들을 추가하는 appendTransactionPool( ) 메소드 , 트랜잭션 풀을 최신화 해주는 updateTransactionPool( ) 메소드를 만들어주었다.
// chain.ts 파일, Chain 클래스, 메소드 추가
// 트랜잭션 풀 return
public getTransactionPool(): ITransaction[] {
return this.transactionPool;
}
// 트랜잭션 풀에 트랜잭션 추가
public appendTransactionPool(_transaction: ITransaction): void {
this.transactionPool.push(_transaction);
}
// 트랜잭션 풀 최신화
public updateTransactionPool(_newBlock: IBlock) {
let txPool: ITransaction[] = this.getTransactionPool();
_newBlock.data.forEach((tx: ITransaction) => {
txPool = txPool.filter((txp) => {
txp.hash !== tx.hash;
});
});
this.transactionPool = txPool;
}
updateTransactionPool( ) 메소드에 대해 설명을 조금 보태면 블록(_newBlock)을 인자값으로 받아서 블록 안에 있는 data 속성값의 트랜잭션 객체들을 통해 트랜잭션 풀을 최신화 해주게 된다.
이제 트랜잭션 풀과 관련된 메소드들을 사용해 Chain 클래스 안에서 다음의 메소드들을 수정해줘야 한다.
- miningBlock( ) 메소드 안의 this.appendUTXO( ) 삭제 , this.addBlock( ) 메소드의 인자값으로 전달하는 data 배열 안에서 this.getTransactionPool( ) 메소드로 트랜잭션 풀에 있는 트랜잭션 내용 추가
- addBlock( ) 메소드 안에서 블록이 생성될 때 this.updateUTXO( ) 메소드와 this.updateTransactionPool( ) 메소드 호출
- addToChain( ) 메소드 안에서 this.updateUTXO( ) 메소드와 this.updateTransactionPool( ) 메소드 호출
- replaceChain( ) 메소드 안에서 this.updateUTXO( ) 메소드와 this.updateTransactionPool( ) 메소드 호출
// src/core/blockchain/ 디렉토리 chain.ts 파일, Chain 클래스 메소드 수정
import { Block } from '@core/blockchain/block';
import { DIFFICULTY_ADJUSTMENT_INTERVAL } from '@core/config';
import { Transaction } from '@core/transaction/transaction';
import { TxIn } from '@core/transaction/txin';
import { TxOut } from '@core/transaction/txout';
import { UnspentTxOut } from '@core/transaction/unspentTxOut';
export class Chain {
private blockchain: Block[];
private unspentTxOuts: IUnspentTxOut[];
private transactionPool: ITransaction[];
constructor() {
this.blockchain = [Block.createGENESIS(Block.getGENESIS())];
this.unspentTxOuts = [];
this.transactionPool = [];
}
public getUnspentTxOuts(): IUnspentTxOut[] {
return this.unspentTxOuts;
}
public appendUTXO(utxo: IUnspentTxOut[]): void {
this.unspentTxOuts.push(...utxo);
}
public getChain(): Block[] {
return this.blockchain;
}
public getLength(): number {
return this.blockchain.length;
}
public getLatestBlock(): Block {
return this.blockchain[this.blockchain.length - 1];
}
// 트랜잭션 풀 return
public getTransactionPool(): ITransaction[] {
return this.transactionPool;
}
// 트랜잭션 풀에 트랜잭션 추가
public appendTransactionPool(_transaction: ITransaction): void {
this.transactionPool.push(_transaction);
}
// 트랜잭션 풀 최신화
public updateTransactionPool(_newBlock: IBlock) {
let txPool: ITransaction[] = this.getTransactionPool();
_newBlock.data.forEach((tx: ITransaction) => {
txPool = txPool.filter((txp) => {
txp.hash !== tx.hash;
});
});
this.transactionPool = txPool;
}
public miningBlock(_account: string): Failable<Block, string> {
// ToDo : Transaction 객체 만들어주는 코드
const txin: ITxIn = new TxIn('', this.getLatestBlock().height + 1);
const txout: ITxOut = new TxOut(_account, 50);
const coinbaseTransaction: Transaction = new Transaction([txin], [txout]);
// const utxo = coinbaseTransaction.createUTXO(); 삭제
// this.appendUTXO(utxo); 삭제
// ToDo : addBlock() 호출
return this.addBlock([coinbaseTransaction, ...this.getTransactionPool()]);
}
public addBlock(data: ITransaction[]): Failable<Block, string> {
const previousBlock = this.getLatestBlock();
const adjustmentBlock: Block = this.getAdjustmentBlock(); // -10번째 블록 구하기
const newBlock = Block.generateBlock(previousBlock, data, adjustmentBlock);
const isValid = Block.isValidNewBlock(newBlock, previousBlock);
if (isValid.isError) return { isError: true, error: isValid.error };
this.blockchain.push(newBlock);
// UTXO 업데이트
newBlock.data.forEach((_tx: ITransaction) => {
this.updateUTXO(_tx);
});
// ToDo : 트랜잭션 풀 내용 최신화
// 블록이 생성되었다면 트랜잭션 풀에 있는 트랜잭션 제거
this.updateTransactionPool(newBlock);
return { isError: false, value: newBlock };
}
public addToChain(_receivedBlock: Block): Failable<undefined, string> {
const isValid = Block.isValidNewBlock(_receivedBlock, this.getLatestBlock());
if (isValid.isError) return { isError: true, error: isValid.error };
this.blockchain.push(_receivedBlock);
// UTXO 업데이트
_receivedBlock.data.forEach((_tx: ITransaction) => {
this.updateUTXO(_tx);
});
// 트랜잭션 풀 업데이트
this.updateTransactionPool(_receivedBlock);
return { isError: false, value: undefined };
}
// 체인 검증 코드
public isValidChain(_chain: Block[]): Failable<undefined, string> {
// ToDo : 제네시스 블록을 검사하는 코드 (생략)
// ToDo : 나머지 체인에 대한 검증 코드
for (let i = 1; i < _chain.length; i++) {
const newBlock = _chain[i];
const previousBlock = _chain[i - 1];
const isValid = Block.isValidNewBlock(newBlock, previousBlock);
if (isValid.isError) return { isError: true, error: isValid.error };
}
return { isError: false, value: undefined };
}
// 체인 교체 코드
public replaceChain(receivedChain: Block[]): Failable<undefined, string> {
const latestReceivedBlock: Block = receivedChain[receivedChain.length - 1];
const latestBlock: Block = this.getLatestBlock();
if (latestReceivedBlock.height === 0) {
return { isError: true, error: '받은 최신 블록이 제네시스 블록' };
}
if (latestReceivedBlock.height <= latestBlock.height) {
return { isError: true, error: '자신의 블록이 더 길거나 같습니다.' };
}
// 체인 바꿔주는 코드 (내 체인이 더 짧다면)
this.blockchain = receivedChain;
// UTXO & 트랜잭션 풀 업데이트
this.blockchain.forEach((_block: IBlock) => {
this.updateTransactionPool(_block);
_block.data.forEach((_tx: ITransaction) => {
this.updateUTXO(_tx);
});
});
return { isError: false, value: undefined };
}
public updateUTXO(_tx: ITransaction): void {
// UnspentTxOuts
/**
* {
* txOutId
* txOutIndex
* account
* amount
* }
*/
const unspentTxOuts: UnspentTxOut[] = this.getUnspentTxOuts();
// UTXO에 추가할 unspentTxOut 객체 생성
const newUnspentTxOuts = _tx.txOuts.map((txout, index) => {
return new UnspentTxOut(_tx.hash, index, txout.account, txout.amount);
});
// UTXO에서 사용한 unspentTxOut 객체 제거 & 생성된 unspentTxOut 객체 UTXO에 추가
const tmp = unspentTxOuts
.filter((_v: UnspentTxOut) => {
const bool = _tx.txIns.find((v: TxIn) => {
return _v.txOutId === v.txOutId && _v.txOutIndex === v.txOutIndex;
});
// !undefined == true
// !{ } == false
return !bool;
})
.concat(newUnspentTxOuts);
let unspentTmp: UnspentTxOut[] = [];
const result = tmp.reduce((acc, utxo) => {
const find = acc.find(({ txOutId, txOutIndex }) => {
return txOutId === utxo.txOutId && txOutIndex === utxo.txOutIndex;
});
if (!find) acc.push(utxo);
return acc;
}, unspentTmp);
this.unspentTxOuts = result;
}
/**
* 생성 기준으로 블록 높이가 -10 인 블록 구하기
*/
public getAdjustmentBlock() {
// ToDo : 블록의 interval을 상수로 정해놓기. (블록 몇 개를 기준으로 난이도를 측정할 것인가)
// 현재 마지막 블록에서 - 10 (DIFFICULTY_ADJUSTMENT_INTERVAL)
const currentLength = this.getLength();
const adjustmentBlock: Block =
this.getLength() < DIFFICULTY_ADJUSTMENT_INTERVAL
? Block.getGENESIS()
: this.blockchain[currentLength - DIFFICULTY_ADJUSTMENT_INTERVAL];
return adjustmentBlock; // 블록 자체를 반환
}
}
수정된 내용들을 살펴보면 블록체인 네트워크 상에서 연결된 노드들이 서로 통신하면서 체인에 블록을 추가하거나 체인을 교체해야 하는 상황이 발생했을 때 updateUTXO( ) 메소드와 updateTransactionPool( ) 메소드를 통해 UTXO와 트랜잭션 풀을 최신화시켜주고 있는 것을 알 수 있다.
3. 트랜잭션 broadcast
마지막으로 블록체인 네트워크 상에서 서로 연결된 노드들이 트랜잭션 풀의 내용을 공유하는 부분을 구현해주기만 하면 된다. 웹 소켓을 이용한 P2P 네트워크 통신이기 때문에 message를 만들어서 연결된 노드들끼리 트랜잭션 풀에 추가되는 트랜잭션 객체의 내용을 broadcast 해주는 방식으로 구현하였다.
우선 블록체인 HTTP 서버 쪽의 "/sendTransaction" 라우터 부분을 수정하고자 하는데 기존의 "/sendTransaction" 라우터 부분은 다음과 같다.
// 기존 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;
console.log('receivedTx : ', receivedTx);
Wallet.sendTransaction(receivedTx, ws.getUnspentTxOuts()); // 블록체인 네트워크의 entry point(진입점)
} catch (e) {
if (e instanceof Error) console.log(e.message);
}
res.json({});
});
현재 "/sendTransaction" 라우터로 요청이 들어왔을 때 Wallet.sendTransaction( ) 메소드가 실행되면서 트랜잭션 객체를 생성하고 있다. 이제 생성된 트랜잭션 객체(tx)를 ws.appendTransactionPool( tx ) 메소드를 통해 트랜잭션 풀에 추가하고 ws.updateUTXO( tx ) 메소드를 통해 UTXO 내용을 최신화시켜주도록 하자. 그리고 생성된 트랜잭션 객체를 다른 노드들에게 broadcast 하기위해 message를 만든 다음 ws.broadcast( ) 를 호출해주었다.
// 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;
console.log('receivedTx : ', receivedTx);
const tx = Wallet.sendTransaction(receivedTx, ws.getUnspentTxOuts()); // 블록체인 네트워크의 entry point(진입점)
ws.appendTransactionPool(tx); // 추가
// txIns , txOuts
// utxo[] - txIns + txOuts
// UTXO 내용을 최신화하는 함수의 인자값 : 트랜잭션 객체
ws.updateUTXO(tx); // 추가
// 트랜잭션 broadcast
const message: Message = {
type: MessageType.receivedTx,
payload: tx,
};
ws.broadcast(message);
} catch (e) {
if (e instanceof Error) console.log(e.message);
}
res.json({});
});
enum MessageType {
latest_block = 0,
all_block = 1,
receivedChain = 2,
receivedTx = 3,
}
interface Message {
type: MessageType;
payload: any;
}
const message: Message = {
type: MessageType.receivedTx,
payload: tx,
};
연결된 다른 노드들에게 위와 같은 message를 만들어서 broadcast 하고 있는 상황이므로 message를 받을 수 있게끔 src/server/ 디렉토리 안의 p2p.ts 파일 쪽에서 해당 내용에 대한 코드를 추가해줘야만 한다.
messageHandler(socket: WebSocket) {
const callback = (_data: string) => {
// JSON.parse(Buffer.from(_data).toString());
const result: Message = P2PServer.dataParse<Message>(_data);
const send = P2PServer.send(socket);
switch (result.type) {
// type이 두가지 이상 생겼을 때 case문으로 처리 가능
case MessageType.latest_block: {
const message: Message = {
type: MessageType.all_block,
payload: [this.getLatestBlock()],
};
send(message);
break;
}
case MessageType.all_block: {
const message: Message = {
type: MessageType.receivedChain,
payload: this.getChain(),
};
const [receivedBlock] = result.payload; // [this.getLatestBlock()]
const isValid = this.addToChain(receivedBlock);
// addToChain 이 성공했을 때는 추가적인 요청이 불필요. break
if (!isValid.isError) break;
send(message);
break;
}
case MessageType.receivedChain: {
const receivedChain: IBlock[] = result.payload;
// 체인 바꿔주는 코드
// 긴 체인 선택하기
this.handleChainResponse(receivedChain);
break;
}
case MessageType.receivedTx: {
const receivedTransaction: ITransaction = result.payload;
if (receivedTransaction === null) break;
const withTransaction = this.getTransactionPool().find((_tx: ITransaction) => {
return _tx.hash === receivedTransaction.hash;
});
// 내 풀에 받은 트랜잭션 내용이 없다면 추가.
if (!withTransaction) {
this.appendTransactionPool(receivedTransaction);
const message: Message = {
type: MessageType.receivedTx,
payload: receivedTransaction,
};
this.broadcast(message);
}
break;
}
}
};
socket.on('message', callback);
}
P2PServer 클래스 안의 messageHandler( ) 메소드에서 switch 문을 사용해 message의 type 속성값에 따라 다른 처리들을 해주고 있는 상황이다. case MessageType.receivedTx: { } 코드블록을 추가하여 트랜잭션 객체가 broadcast 되었을 때 어떤 작업을 진행할지에 대한 처리를 해주었다.
case MessageType.receivedTx: { } 코드블록 내용을 잠시 살펴보면 message를 전달받은 노드 쪽에서 자신이 갖고 있는 트랜잭션 풀을 조회하여 message의 payload 속성값으로 전달받은 트랜잭션 객체가 없다면 자신의 트랜잭션 풀에 추가하고 다시 message를 만들어 연결된 다른 노드들에게 broadcast 하는 방식이다.
이로써 트랜잭션 내용에 대한 broadcast 부분까지 완료하였다. 지갑 프로그램(클라이언트)에서 지갑 서버쪽으로 요청을 보냈을 때 계정의 잔액을 즉각적으로 조회할 수 있는 라우터 하나만 추가로 만들어주고 "지갑 서버 만들기" 시리즈를 끝마치고자 한다.
👉 지갑 서버 쪽 server.ts 파일 "/wallet/:account" 라우터 수정
// wallet/ 디렉토리 server.ts 파일, 지갑 서버
// view
app.get('/wallet/:account', async (req, res) => {
const { account } = req.params;
console.log('wallet', account);
const privateKey = Wallet.getWalletPrivateKey(account); // privateKey 가져오기
const myWallet = new Wallet(privateKey);
// 잔액 가져오기
const response = await request.post('/getBalance', { account });
myWallet.balance = response.data.balance;
res.json(myWallet);
});
👉 블록체인 HTTP 서버 쪽 index.ts 파일, "/getBalance" 라우터 추가
// 루트 디렉토리 index.ts 파일, "/getBalance" 라우터 추가
app.post('/getBalance', (req, res) => {
const { account } = req.body;
// const account = Wallet.getAccount(publicKey);
const balance = Wallet.getBalance(account, ws.getUnspentTxOuts());
res.json({
balance,
});
});
'BlockChain' 카테고리의 다른 글
BlockChain - 블록체인 지갑 서버 만들기 (3) 트랜잭션 (0) | 2022.06.23 |
---|---|
BlockChain - 블록체인 지갑 서버 만들기 (2) 코인베이스 트랜잭션 (0) | 2022.06.22 |
BlockChain - [블록체인 네트워크] 트랜잭션 (0) | 2022.06.21 |
BlockChain - 블록체인 지갑 서버 만들기 (1) (0) | 2022.06.20 |
BlockChain - 개인키, 공개키, 서명, 지갑/계정 (1) | 2022.06.17 |