오늘은 지난번에 공부했었던 지갑에 관련하여 오늘은 지갑 서버를 만들어볼것인데 지갑서버와 연결할 블록체인 HTTP 서버와 P2P 네트워크는 전에 미리 만들어 두었던것들을 사용하여 연습해보겠습니다.
아래의 사진은 전체적인 지갑서버와 블록체인 HTTP 서버와 P2P 네트워크의 구조입니다.
/** Wallet.ts */
import { randomBytes } from 'crypto';
import { SHA256 } from 'crypto-js';
import elliptic from 'elliptic';
import fs from 'fs';
import path from 'path';
/** 파일을 저장할 위치 설정 / __dirname = 현재디렉토리 */
const dir = path.join(__dirname, '../data');
const ec = new elliptic.ec('secp256k1');
export class Wallet {
public privateKey: string;
public publicKey: string;
public account: string;
public balace: number;
// 인자값에 privateKey가 없으면 새로 개인키를 생성 후 지갑을 생성하고 인자값에 privateKey가 있으면 원래 있던 값으로 지갑을 생성합니다.
constructor(_privateKey: string = '') {
this.privateKey = _privateKey || this.getPrivateKey();
this.publicKey = this.getPublicKey();
this.account = this.getAccount();
this.balace = 0;
Wallet.createWallet(this);
}
/** 생성한 지갑을 파일시스템을 사용하여 파일로 저장 */
static createWallet(myWallet: Wallet): void {
// 파일명은 지갑 주소
const filename = path.join(dir, myWallet.account);
// 내용은 개인키
const filecontent = myWallet.privateKey;
fs.writeFileSync(filename, filecontent);
}
/** 생성한 지갑 리스트 가져오기 */
static getWalletList(): string[] {
const files: string[] = fs.readdirSync(dir);
return files;
}
/** 생성한 지갑의 개인키 가져오는 함수 */
static getWalletPrivateKey(_account: string): string {
const filePath = path.join(dir, _account);
const filecontent = fs.readFileSync(filePath); // return Buffer
return filecontent.toString();
}
/** 서명 만드는 함수 */
static createSign(_obj: any): elliptic.ec.Signature {
const {
sender: { account, publicKey }, // 보내는 사람
received, // 받는사람
amount, // 금액
} = _obj;
/** hash = 공개키 + 받는사람 + 금액 toString() */
const hash: string = SHA256([publicKey, received, amount].join('')).toString();
/** 개인키 가져오기 */
const privateKey: string = Wallet.getWalletPrivateKey(account);
/** 개인키를 컴퓨터가 읽을수 있도록 설정 */
const keyPair: elliptic.ec.KeyPair = ec.keyFromPrivate(privateKey);
return keyPair.sign(hash, 'hex');
}
// 개인키 생성 함수
public getPrivateKey(): string {
return randomBytes(32).toString('hex');
}
// 공개키 생성 함수
public getPublicKey(): string {
const keyPair = ec.keyFromPrivate(this.privateKey);
return keyPair.getPublic().encode('hex', true);
}
// 지갑 주소 생성 함수
public getAccount(): string {
return Buffer.from(this.publicKey).slice(26).toString();
}
}
- 지갑 생성 버튼을 눌렀을때 해당 지갑의 정보를 파일로 저장할것이니
fs
를 사용하여data
디렉토리에 파일을 저장하게 설정해주었고 파일명은 지갑주소, 내용은 개인키로 설정해주었습니다.constructor(_privateKey: string = '') {
이 부분을 보면constructor()
생성자 함수에서 인자값으로 개인키를 받고 있는데 만약 인자값에 개인키가 있다면 해당 개인키를 가지고 인스턴스를 생성해주고 개인키가 없다면 새로 개인키를 생성한뒤 인스턴스를 생성해주었습니다, 왜냐하면Wallet Class
에서 개인키, 공개키, 서명, 지갑주소를 만드는 메서드를 사용하고 있기 때문에 후에 해당 개인키에 대한 지갑정보를 가져오려면 인자값의 개인키로 해당 개인키에 맞는 지갑 정보를 가져오게끔 한것입니다.
지갑서버를 만들기에 앞서 지갑 클라이언트 역할을 할 view를 먼저 만들어주었습니다.
/* index.html */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
<title>Document</title>
</head>
<body>
<h1>hello wallet</h1>
<button id="wallet_btn">지갑 생성</button>
<ul id="wallet_list">
<li>Coin : SwanCoin</li>
<li>
account :
<span class="account"></span>
</li>
<li>
private key :
<span class="privateKey"></span>
</li>
<li>
public key :
<span class="publicKey"></span>
</li>
<li>
balace :
<span class="balace"></span>
</li>
</ul>
<form id="transaction_form">
<ul>
<li>received : <input id="received" placeholder="보낼 계정" /></li>
<li>amount : <input id="amount" placeholder="보낼 금액" /></li>
</ul>
<input type="submit" value="전송" />
</form>
<h1>지갑목록</h1>
<button id="wallet_list_btn">지갑 목록 버튼</button>
<div class="wallet_list2">
<ul>
목록버튼을 눌러주세요.
</ul>
</div>
<script type="text/javascript">
const walletBtn = document.querySelector('#wallet_btn');
const walletListBtn = document.querySelector('#wallet_list_btn');
const transactionForm = document.querySelector('#transaction_form');
const createWallet = async () => {
const response = await axios.post('/newWallet', null);
console.log(response.data);
view(response.data);
};
const view = (wallet) => {
const account = document.querySelector('.account');
const privateKey = document.querySelector('.privateKey');
const publicKey = document.querySelector('.publicKey');
const balace = document.querySelector('.balace'); // 제외
account.innerHTML = wallet.account;
privateKey.innerHTML = wallet.privateKey;
publicKey.innerHTML = wallet.publicKey;
balace.innerHTML = wallet.balace;
};
const getView = async (account) => {
const response = await axios.get(`/wallet/${account}`);
view(response.data);
};
const getWalletList = async () => {
const walletList = document.querySelector('.wallet_list2 > ul');
const response = await axios.post('/walletList', null);
const list = response.data.map((account) => {
return `<litoken interpolation">${account}')">${account}</li>`;
});
walletList.innerHTML = list;
};
const submitHandler = async (e) => {
e.preventDefault();
const publicKey = document.querySelector('.publicKey').innerHTML;
const account = document.querySelector('.account').innerHTML;
const data = {
sender: {
publicKey,
account,
},
received: e.target.received.value,
amount: parseInt(e.target.amount.value),
};
const response = await axios.post('/sendTransaction', data);
};
walletBtn.addEventListener('click', createWallet);
walletListBtn.addEventListener('click', getWalletList);
transactionForm.addEventListener('submit', submitHandler);
</script>
</body>
</html>
<지갑 화면>
새로운 지갑을 생성할 지갑 생성 버튼을 만들고 해당 지갑에 대한 내용을 보여줄 수 있게 해주었습니다.
후에 다른 지갑으로 코인을 보낼 수 있도록 보낼 지갑 주소와 금액을 입력할 수 있는input
을 만들어주고 Transaction을 발생시킬 전송 버튼도 만들어주었습니다.
지금까지 생성된 지갑 주소를 확인 할 수 있게 지갑 목록 버튼도 만들어주었습니다.
/* Wallet Server */
import express from 'express';
import nunjucks from 'nunjucks';
import { Wallet } from './wallet';
import axios from 'axios';
const app = express();
const userid = process.env.USERID || 'ash991213';
const userpw = process.env.USERPW || '1234';
const baseURL = process.env.BASEURL || 'http://localhost:3000';
const baseAuth = Buffer.from(userid + ':' + userpw).toString('base64');
// authorization <-
const request = axios.create({
baseURL,
headers: {
Authorization: 'Basic ' + baseAuth,
'Content-type': 'application/json',
},
});
app.use(express.json());
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.get('/', (req, res) => {
res.render('index.html');
});
// 지갑 생성
app.post('/newWallet', (req, res) => {
res.json(new Wallet());
});
// 지갑 리스트
app.post('/walletList', (req, res) => {
const list = Wallet.getWalletList();
res.json(list);
});
// 지갑 View
app.get('/wallet/:account', (req, res) => {
const { account } = req.params;
const privateKey = Wallet.getWalletPrivateKey(account);
res.json(new Wallet(privateKey));
});
// 트랜잭션 보내기
app.post('/sendTransaction', async (req, res) => {
console.log('Wallet Server');
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({});
});
app.listen(3005, () => {
console.log('Wallet 서버 실행');
});
- 지갑 생성 버튼을 눌러
app.post('/newWallet)
로 요청을하면 미리 만들어둔Wallet Class
로 새로운 인스턴스를 생성하여 응답으로 보내줄것입니다.- 지갑 목록 버튼을 눌렀을때는
Wallet Class
에getWalletList()
메서드로 미리 만들어둔 지갑 목록을 보내줄것 입니다.- 지갑 목록에 있는 지갑을 클릭할때는
Wallet Class
에getWalletPrivateKey()
메서드로 해당 지갑의 개인키를 가져오고 받은 개인키를 다시 Wallet Class를 통해 해당 개인키에 맞는 공개키, 지갑 정보등을 보내줄것입니다.- 코인을 보낼 지갑 주소와 보낼 금액을 적은뒤 전송 버튼을 누르면 Submit Handler가 실행되어 공개키, 지갑주소를 sender 객체에 담고, 보낼 지갑 주소인 recevied와 보낼 금액인 amount값을 data 객체에 담아
app.post('/sendTransaction')
이쪽으로 요청을 보내게 됩니다. 해당 요청을 받은 지갑 서버는 받은 data를 가지고 서명(signature)을 만들어서 BlockChain HTTP 서버에 다시 요청을 보내줍니다.
import { P2PServer } from '@src/core/serve/p2p';
import peers from './peer.json';
import express from 'express';
import { ReceviedTx, Wallet } from '@src/core/wallet/wallet';
const app = express();
const ws = new P2PServer();
enum MessageType {
latest_block = 0,
all_block = 1,
receivedChain = 2,
}
interface Message {
type: MessageType;
payload: any;
}
app.use(express.json());
// http://ash991213:1234@localhost:3000
// userid 와 password를 url로 받는데 base64로 인코딩 되어있기 때문에 디코딩해서 사용해야 합니다.
app.use((req, res, next) => {
const baseAuth: string = (req.headers.authorization || '').split(' ')[1];
if (baseAuth === '') return res.status(401).send();
const [userid, password] = Buffer.from(baseAuth, 'base64').toString().split(':');
if (userid !== 'ash991213' || password !== '1234') return res.status(401).send();
next();
});
app.get('/', (req, res) => {
res.send('block_chain');
});
// 내 블록체인 조회
app.get('/chains', (req, res) => {
res.json(ws.getChain());
});
// 블록채굴 API
app.post('/mineBlock', (req, res) => {
const { data } = req.body; // Transaction 객체를 채우기 위한 정보로 지갑주소값이 들어감
const newBlock = ws.miningBlock(data); // 받는 사람의 지갑 주소를 인자값으로 보내줌
if (newBlock.isError) return res.status(500).send(newBlock.error);
const message: Message = {
type: MessageType.latest_block,
payload: {},
};
ws.broadcast(message);
res.json(newBlock.value);
});
// 연결된 socket 조회
app.get('/peers', (req, res) => {
const sockets = ws.getSockets().map((s: any) => {
return s._socket.remoteAddress + ':' + s._socket.remotePort;
});
res.json(sockets);
});
// ws 연결 요청 API
app.post('/addPeers', (req, res) => {
peers.forEach((peer) => {
ws.connectToPeer(peer);
});
});
// 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('BlockChain 서버 실행');
ws.listen();
});
블록체인 HTTP 서버에서는 Transaction 내용을 블록체인 네트워크에 전송하기전에 지갑 서버에서 만든 서명을 검증해야 합니다. 요청 body에 담긴
txObject
내용을 receivedTx에 할당하고 Wallet Class에 snedTransaction() 메소드를 통해 검증을 할것입니다.
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 };
}
sendTransaction()
함수의 인자값으로 받은receivedTx
안에 서명을 검증하기 위해getVerify()
함수에 인자값으로 넣어주고 실행하였습니다.
getVerify
함수를 살펴보면 지난 시간에 했던것처럼elliptic
( 타원 곡선 알고리즘 라이브러리 )를 사용하여 서명을 검증해 주었습니다.
이렇게 서명까지 완료되었습니다. 다음에는 현재 가지고 있는 데이터인publicKey
,received
,amount
,signature
를 가지고Transaction
내용을 만들어 주도록 하겠습니다.