이번 포스팅에서는 이전까지의 내용들을 토대로 트랜잭션을 만들어주는 과정에 대해 다뤄보고자 한다.
이전 글)
2022.06.20 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (1)
< 목차 >
- transaction.d.ts 파일 만들기
- 트랜잭션 클래스 만들기
- 블록체인 HTTP 서버 /mineBlock api 수정
- 테스트 코드 작성
1. transaction.d.ts 파일 만들기
우선 @types/ 디렉토리 안에서 트랙잭션 내용을 만들 때 사용할 객체들의 타입을 정의해 주도록 하자.
이전 글에서 트랜잭션에 대한 설명과 함께 트랜잭션 interface들을 만들어 보았으므로 해당 부분에 대한 설명은 생략하도록 하겠다.
참고)
2022.06.21 - [BlockChain] - BlockChain - [블록체인 네트워크] 트랜잭션
// @types/ 디렉토리 transaction.d.ts 파일
declare interface ITxOut {
account: string; // 해당하는 사람의 주소
amount: number; // 잔액. (객체 안의 amount 속성값이 하나의 단위이다.)
}
declare interface ITxIn {
txOutId: string; // ITransaction {} 의 hash 값
txOutIndex: number; // ITransaction에 있는 txouts 배열의 인덱스
signature?: string | undefined;
}
declare interface ITransaction {
hash: string; // txIns, txOuts를 이용해 만든 hash값
txOuts: ITxOut[];
txIns: ITxIn[];
}
// TxIn은 UnspentTxOut[]를 참조해서 만들어진다.
// TxIn 만들 때 UnspentTxOut[]에서 삭제
// TxOut 만들 때 UnspentTxOut[]에 생성
declare interface IUnspentTxOut {
txOutId: string; // TxOut을 담고 있는 트랙잭션의 hash값
txOutIndex: number; // 트랙잭션의 txOuts 배열에서의 인덱스값
account: string;
amount: number;
}
2. 트랜잭션 클래스 만들기
이제 트랜잭션 내용을 만들어줄 클래스들을 만들어보도록 하자.
src/core/ 디렉토리 안에 transaction 디렉토리를 생성해 준 다음 transaction.ts , txin.ts , txout.ts , unspentTxOut.ts 파일을 만들어 주도록 하자. 그리고 각각의 파일 안에서는 다음과 같이 transaction , txIn , txOut , unspentTxOut 객체를 생성하는 클래스를 만들어주었다.
// txin.ts 파일
// txIn 객체를 생성해주는 클래스
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;
}
}
txIn 객체를 만들어주는 TxIn 클래스의 경우 constructor( ) 함수의 인자값으로 txOutId , txOutIndex , signature를 받게 되는데 코인베이스 트랜잭션의 경우 서명(signature) 부분이 필요없으므로 _signature: string | undefined = undefined 와 같이 처리해주었다.
// txout.ts 파일
// txOut 객체를 생성해주는 클래스
export class TxOut {
public account: string;
public amount: number;
constructor(_account: string, _amount: number) {
this.account = _account;
this.amount = _amount;
}
}
txOut 객체의 경우 속성값으로 보낼계정(account) 과 보낼금액(amount)이 있어야 하므로 위와 같이 TxOut 클래스를 만들어주었다.
// unspentTxOut.ts 파일
// unspentTxOut 객체를 생성해주는 클래스
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;
}
}
unspentTxOut 객체의 경우 txOut 객체 안에 있는 내용들을 토대로 만들어진다. unspentTxOut 객체의 속성값 중 account 속성과 amount 속성은 txOut 객체의 내용들로 구성되며 txOutId는 transaction 객체의 hash값 , txOutIndex는 transaction 객체 안의 txOuts 배열에서 TxOut 객체의 인덱스 값이 된다.
// transaction.ts 파일
// transaction 객체 생성해주는 클래스
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;
}
}
우선 transaction 객체 안에 들어가는 속성들은 txIn 객체들을 담아놓은 txIns[ ] 배열 , txOut 객체들을 담아놓은 txOuts[ ] 배열 , transaction 객체를 구분하는 고유한 값인 hash값이다. transaction 객체의 hash값은 txIns 배열과 txOuts 배열 안에 있는 객체들의 속성값으로 만들어지기 때문에 Transaction 클래스 안에서 createTransactionHash( ) 라는 메소드를 따로 만들어주었다.
createTransactionHash( ) 함수를 사용해 transaction 객체의 hash값까지 만들어주었다면 txOuts[ ] 배열 안에 있는 txOut 객체들을 사용해 unspentTxOut 객체를 만든 다음 unspentTxOut 객체들이 담길 UTXO 공간 역시 만들어 줘야 한다. 해당 부분은 createUTXO( ) 함수를 만들어서 구현했으며 createUTXO( ) 메소드의 return 값은 unspentTxOut 객체들이 담긴 utxo: UnspentTxOut[ ] 배열이 되도록 하였다.
3. 블록체인 HTTP 서버 /mineBlock api 수정
현재 루트디렉토리 안에 위치한 index.ts 파일에는 블록체인 인터페이스를 관리하기 위한 http 서버 코드가 작성되어 있다.
"/mineBlock" 라우터로 요청이 들어왔을 때 블록이 생성되면서 체인 상에 추가되게 되는데 해당 부분의 코드는 현재 다음과 같이 작성되어 있다.
// 블록 채굴 api
app.post("/mineBlock", (req, res) => {
const { data } = req.body;
const newBlock = ws.addBlock(data);
if (newBlock.isError) return res.status(500).send(newBlock.error);
res.json(newBlock.value);
});
그리고 Chain 클래스 안에서 만들어진 addBlock( ) 함수는 다음과 같다.
public addBlock(data: string[]): Failable<Block, string> {
const previousBlock = this.getLatestBlock();
// -10 번째 블록 구하기
const adjustmentBlock: Block = this.getAdjustmentBlock();
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);
return { isError: false, value: newBlock };
}
블록이 생성될 때 블록 안에 들어가는 data의 타입이 string[ ] 배열로 되어있으므로 해당 부분을 수정해야만 한다. 블록 생성시 들어가게 되는 데이터는 트랜잭션 데이터여야 하므로 Chain 클래스 안에 miningBlock( ) 이라는 함수를 만들어서 transaction 객체를 addBlock( ) 함수의 인자값으로 전달할 수 있도록 하였다. 그리고 "/mineBlock" 라우터로 요청이 들어왔을 때 ws.mineBlock( ) 함수가 호출되게끔 하였다. 이 때 ws.mineBlock( ) 메소드의 인자값으로는 계정(account) 데이터가 들어간다.
// 수정된 블록 채굴 api
app.post('/mineBlock', (req: Request, res: Response) => {
const { data } = req.body;
// Transaction 객체를 채우기 위한 정보로 account 전달
const newBlock = ws.miningBlock(data); // data == account
if (newBlock.isError) return res.status(500).send(newBlock.error);
const msg: Message = {
type: MessageType.latest_block,
payload: {},
};
ws.broadcast(msg);
res.json(newBlock.value);
});
// chain.ts 파일 Chain 클래스 내용 수정
private blockchain: Block[];
private unspentTxOuts: IUnspentTxOut[]; // 추가
constructor() {
this.blockchain = [Block.createGENESIS(Block.getGENESIS())];
this.unspentTxOuts = []; // 추가
}
// 추가
public getUnspentTxOuts(): IUnspentTxOut[] {
return this.unspentTxOuts;
}
// 추가
public appendUTXO(utxo: IUnspentTxOut[]): void {
this.unspentTxOuts.push(...utxo);
}
// 추가
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]);
}
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);
return { isError: false, value: newBlock };
}
Chain 클래스 안에서 많은 부분이 추가된 것을 확인할 수 있는데 우선 UTXO 라는 공간을 배열의 형태로 Chain 클래스 안에서 만들어주었다. ( public unspentTxOuts: IUnspentTxOut[ ] ) 그리고 UTXO 배열을 조회할 수 있는 getUnspentTxOuts( ) 메소드도 만들어주었다.
추가된 miningBlock( ) 메소드를 살펴보면 txin 객체의 txOutId 에는 빈 string 값이 들어가고 txOutIndex 부분에는 이전 블록의 height에서 +1 된 값이 들어간 것을 확인할 수 있는데 이는 miningBlock( ) 함수 안에서 생성되는 coinbaseTransaction 객체가 코인베이스 트랜잭션이기 때문이다. 코인베이스 트랜잭션은 블록이 생성될 때 data 속성 안에 들어가는 첫번째 트랜잭션으로 블록을 생성한 계정(채굴자)에게 보상(코인)을 지급하는 내용의 트랜잭션이다. 따라서 txout 객체 안에 들어가는 account 속성값 역시 채굴자의 계정이 되며 amount 값은 블록 생성시 받게되는 코인의 개수가 된다.
miningBlock( ) 메소드 안에서 코인베이스 트랜잭션 내용의 coinbaseTransaction 객체를 생성한 다음 coinbaseTransaction.createUTXO( ) 메소드로 UTXO 배열 안에 담을 unspentTxOut 객체들을 만들어준다. 그리고 appendUTXO( ) 메소드를 사용해 UTXO 배열 안에 unspentTxOut 객체들을 넣어주었다. 최종적으로 miningBlock( ) 메소드 안에서 addBlock( ) 함수를 호출해 coinbaseTransaction 객체로 구성된 배열을 인자값으로 넣어주었다. 이는 코인베이스 트랜잭션을 생성될 블록의 데이터로 하기 위함이다. ( 사실 miningBlock( ) 메소드가 아닌 addBlock( ) 메소드 안에서 생성된 블록에 대한 검증이 끝났을 때 UTXO 배열 안에 unspentTxOut 객체들을 추가해서 업데이트 해줘야만 한다. 위에서 작성한 this.appendUTXO(utxo) 부분은 추후 수정할 예정이다. )
참고로 addBlock( ) 메소드의 인자값이었던 data: string[ ] 을 data: ITransaction[ ] 으로 수정하였기 때문에 Block.d.ts 파일과 block.ts 파일 안에서 사용되는 data의 타입을 전부 data: ITransaction[ ]으로 변경해주는 것을 잊지 말자.
4. 테스트 코드 작성
테스트 코드를 작성하여 지금까지의 내용을 점검해보도록 하자.
import { Chain } from "@core/blockchain/chain";
describe("Chain 함수 테스트", () => {
let node: Chain = new Chain(); // [ GENESIS ]
it("miningBlock() 함수 테스트", () => {
for (let i = 1; i <= 5; i++) {
node.miningBlock("15cc1446493edbe8843c40ed11526e9252f937f8");
}
console.log(node.getLatestBlock().data);
console.log(node.getUnspentTxOuts());
});
});
보이는 바와 같이 코인베이스 트랜잭션 내용이 생성된 블록의 data로 들어간 것과 코인베이스 트랜잭션의 txOut 객체들이 UTXO 배열 안에 담긴 것을 확인할 수 있다.
다음번 포스팅에서는 코인베이스 트랜잭션이 아닌 일반적인 트랜잭션을 만들어보고자 한다.
다음 글)
2022.06.23 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (3) 트랜잭션
👉 src/core/blockchain/ 디렉토리 chain.ts 파일
// chain.ts 파일
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';
export class Chain {
private blockchain: Block[];
public unspentTxOuts: IUnspentTxOut[];
constructor() {
this.blockchain = [Block.createGENESIS(Block.getGENESIS())];
this.unspentTxOuts = [];
}
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];
}
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]);
}
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);
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);
return { isError: false, value: undefined };
}
// 체인 검증 코드
public isValidChain(_chain: Block[]): Failable<undefined, string> {
// ToDo : 제네시스 블록을 검사하는 코드
// const genesis = _chain[0]
// 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;
return { isError: false, value: undefined };
}
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; // 블록 자체를 반환
}
}
👉 루트 디렉토리 index.ts 파일 ( 블록체인 인터페이스 관리용 HTTP 서버 )
// BlockChain HTTP 서버
import { P2PServer } from './src/server/p2p';
import peers from './peer.json';
import express, { Request, Response } from 'express';
import { ReceivedTx, Wallet } from '@core/wallet/wallet';
import { MessageType, Message } from './src/server/p2p';
console.log(peers);
const app = express();
const ws = new P2PServer();
app.use(express.json());
// 다른 사람이 내 노드의 블록을 조회하는 것을 방지하기 위함.
// header에 있는 authorization 조회
app.use((req, res, next) => {
// console.log(req.headers.authorization);
// req.headers.authorization 타입 -> string | undefined
const baseAuth: string = (req.headers.authorization || '').split(' ')[1];
if (baseAuth === '') return res.status(401).send();
const [userid, userpw] = Buffer.from(baseAuth, 'base64').toString().split(':');
if (userid !== 'web7722' || userpw !== '1234') return res.status(401).send();
next();
});
app.get('/', (req: Request, res: Response) => {
res.send('bit-chain');
});
// 블록 내용 조회 api
app.get('/chains', (req: Request, res: Response) => {
res.json(ws.getChain());
});
// 블록 채굴 api
app.post('/mineBlock', (req: Request, res: Response) => {
const { data } = req.body;
// Transaction 객체를 채우기 위한 정보로 account 전달
const newBlock = ws.miningBlock(data);
if (newBlock.isError) return res.status(500).send(newBlock.error);
const msg: Message = {
type: MessageType.latest_block,
payload: {},
};
ws.broadcast(msg);
res.json(newBlock.value);
});
app.post('/addToPeer', (req: Request, res: Response) => {
const { peer } = req.body;
ws.connectToPeer(peer);
});
app.get('/addPeers', (req: Request, res: Response) => {
peers.forEach((peer) => {
ws.connectToPeer(peer);
});
});
// 연결된 sockets 조회
app.get('/peers', (req: Request, res: Response) => {
// 배열 안에 있는 소켓 정보 가져오기 (socket 주소)
const sockets = ws.getSockets().map((s: any) => s._socket.remoteAddress + ':' + s._socket.remotePort);
res.json(sockets);
});
// 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
}
}
*/
try {
const receivedTx: ReceivedTx = req.body;
Wallet.sendTransaction(receivedTx);
} catch (e) {
if (e instanceof Error) console.log(e.message);
}
res.json({});
});
app.listen(3000, () => {
console.log('server onload 3000');
ws.listen();
});
'BlockChain' 카테고리의 다른 글
BlockChain - 블록체인 지갑 서버 만들기 (4) 트랜잭션 풀 / 멤풀 (3) | 2022.06.24 |
---|---|
BlockChain - 블록체인 지갑 서버 만들기 (3) 트랜잭션 (0) | 2022.06.23 |
BlockChain - [블록체인 네트워크] 트랜잭션 (0) | 2022.06.21 |
BlockChain - 블록체인 지갑 서버 만들기 (1) (0) | 2022.06.20 |
BlockChain - 개인키, 공개키, 서명, 지갑/계정 (1) | 2022.06.17 |