이번 포스팅부터는 블록체인 네트워크에 트랜잭션 데이터를 전송할 수 있는 지갑 서버를 만들어보고자 한다. 지갑/계정을 만들기 위한 기본적인 개념에 대해서는 이전 글에서 다루었으니 참고하길 바란다.
참고)
2022.06.17 - [BlockChain] - BlockChain - 개인키, 공개키, 서명, 지갑/계정
아래의 그림은 앞으로 우리가 만들게 될 지갑 서버와 블록체인 인터페이스를 관리하는 HTTP 서버의 전체적인 구조이다.
블록체인 인터페이스를 관리하는 서버(BlockChain HTTP Server)와 블록체인 P2P 네트워크는 기존에 만들어놓은 것들을 사용하고자 한다. 아래 게시글을 참고하길 바란다.
참고)
2022.06.14 - [BlockChain] - BlockChain - 블록체인 P2P 네트워크 만들기 (1)
2022.06.15 - [BlockChain] - BlockChain - 블록체인 P2P 네트워크 만들기 (2)
2022.06.16 - [BlockChain] - BlockChain - 블록체인 P2P 네트워크 만들기 (3)
< 목차 >
- 지갑 클래스 만들기
- 지갑 서버 만들기
- 블록체인 HTTP 서버에서 검증하기
1. 지갑 클래스 만들기
우선 이전 포스팅에서 테스트 코드로써 구현했었던 지갑 관련 내용들을 클래스를 사용해 만들어보고자 한다.
프로젝트의 루트 디렉토리 안에서 wallet/ 디렉토리를 생성한 다음 해당 디렉토리 안에 지갑 클래스를 작성할 wallet.ts 파일과 지갑 서버를 만들어줄 server.ts 파일을 생성해주었다.
wallet.ts 파일의 내용은 다음과 같다.
// wallet/ 디렉토리 wallet.ts 파일
import { randomBytes } from "crypto";
import elliptic from "elliptic";
import { SHA256 } from "crypto-js";
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 account: string;
public privateKey: string;
public publicKey: string;
public balance: number;
// privateKey를 인자값으로 넣을 경우, 해당 privateKey를 이용해 지갑 내용 생성
// 인자값이 없을 경우, 기존에 만들어 놓은 메소드를 사용해 지갑 내용 생성
constructor(_privateKey: string = "") {
this.privateKey = _privateKey || this.getPrivateKey();
this.publicKey = this.getPublicKey();
this.account = this.getAccount();
this.balance = 0;
Wallet.createWallet(this);
}
static createWallet(myWallet: Wallet) {
// 파일시스템(fs)을 이용해 개인키를 저장할 파일 만들기
// writeFileSync()의 인자값 : 1.파일명 , 2.파일 안에 작성될 내용
const filename = path.join(dir, myWallet.account);
const filecontent = myWallet.privateKey;
fs.writeFileSync(filename, filecontent);
}
public getPrivateKey(): string {
return randomBytes(32).toString("hex");
}
public getPublicKey(): string {
// 개인키 -> 공개키
// 현재 개인키의 type은 string -> elliptic이 해석할 수 있게끔 변환 작업 필요
const keyPair = ec.keyFromPrivate(this.privateKey);
return keyPair.getPublic().encode("hex", true);
}
public getAccount(): string {
return Buffer.from(this.publicKey).slice(26).toString();
}
}
개인키를 생성하는 방법과 만들어진 개인키를 사용해 공개키와 지갑/계정을 생성하는 방법에 대해서는 이전 포스팅에서 자세히 다루었으므로 생략하도록 하겠다.
2022.06.17 - [BlockChain] - BlockChain - 개인키, 공개키, 서명, 지갑/계정
Wallet 클래스 안에서 개인키, 공개키, 계정을 생성하는 과정은 모두 메소드 함수로 구현하였다. 주의깊게 볼 것은 constructor( ) 함수의 인자값으로 privateKey를 받고 있는데 다음과 같은 방법을 사용하여 privateKey가 인자값으로 들어갈 경우 해당 privateKey를 사용해서 인스턴스를 생성하고 인자값이 없을 경우 기존에 만들어 놓은 메소드를 사용하여 인스턴스를 생성하도록 하였다.
constructor(_privateKey: string = '') {
this.privateKey = _privateKey || this.getPrivateKey();
}
Wallet.createWallet( ) 메소드 안에서는 지갑을 만들어주는 코드를 작성하였는데 이 때 파일시스템(fs)을 사용하였다. 실제 지갑 프로그램을 통해 지갑을 생성할 경우 개인키를 안전하게 저장하는 것이 가장 중요한 이슈이다. 따라서 루트 디렉토리 안에 data 디렉토리를 만들어준 뒤 Wallet.createWallet( ) 메소드가 호출될 때마다 data 디렉토리 안에 계정명을 파일명으로 가지고 privateKey 값을 내용으로 하는 파일을 생성해주도록 하였다.
2. 지갑 서버 만들기
지갑 서버를 만들기 이전에 실제 클라이언트가 보게 될 화면을 먼저 만들어주고자 한다. 실제 지갑 프로그램들과 유사하게 만들고자 하였으나,, CSS는 과감히 생략하였다..
루트 디렉토리 안에 views/ 디렉토리를 생성한 다음 해당 디렉토리 안에 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 : Bit-coin</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>
balance :
<span class="balance"></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);
view(response.data);
console.log(response.data);
};
const submitHandler = async (e) => {
e.preventDefault();
// 본인의 account값과 publicKey값 가져오기
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);
};
const view = (wallet) => {
const account = document.querySelector('.account');
const publicKey = document.querySelector('.publicKey');
const privateKey = document.querySelector('.privateKey');
const balance = document.querySelector('.balance');
account.innerHTML = wallet.account;
publicKey.innerHTML = wallet.publicKey;
privateKey.innerHTML = wallet.privateKey;
balance.innerHTML = wallet.balance;
};
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 `<li onClick="getView('${account}')">${account}</li>`;
});
walletList.innerHTML = list;
};
walletBtn.addEventListener('click', createWallet);
walletListBtn.addEventListener('click', getWalletList);
transactionForm.addEventListener('submit', submitHandler);
</script>
</body>
</html>
wallet/ 디렉토리 안에 미리 만들어 놓은 server.ts 파일에서 지갑 서버를 구현하는 코드를 다음과 같이 만들어주었다. express와 nunjucks를 사용하여 서버를 만들었으며 블록체인 인터페이스를 관리하는 HTTP 서버 쪽으로 요청을 보낼 때는 axios를 사용하였다.
// wallet/ 디렉토리 server.ts 파일
import express from "express";
import nunjucks from "nunjucks";
import { Wallet } from "./wallet";
import axios from "axios";
const app = express();
const userid = process.env.USERID || "web7722";
const userpw = process.env.USERPW || "1234";
const baseURL = process.env.BASEURL || "http://localhost:3000";
const baseAuth = Buffer.from(userid + ":" + userpw).toString("base64");
// axios 디폴트 세팅
// axios.create() 메소드 : axios 인스턴스를 반환
// 디폴트값을 세팅할 때 사용하는 메소드
const request = axios.create({
// baseURL , header, authorization 내용 세팅
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");
});
app.post("/newWallet", (req, res) => {
res.json(new Wallet());
});
app.listen(3005, () => {
console.log("server onload #port: 3005");
});
실제 브라우저에서 나타나는 화면은 위와 같이 구성되어있으며 "지갑생성" 버튼을 누를 경우 지갑 서버로 axios 요청을 보내 지갑을 생성한 다음 서버 쪽 로컬 디렉토리의 data 디렉토리 안에 계정명을 파일명으로 하고 privateKey 내용이 적힌 파일이 생성되게끔 하였다.
이제 "지갑목록 버튼"을 클릭했을 때 생성된 계정들이 나타나게끔 하는 라우터와 지갑 목록에서 해당 계정을 클릭했을 때 계정 정보를 조회할 수 있는 라우터를 만들어주도록 하자.
// wallet/ 디렉토리 server.ts 파일 내용 추가
// list
app.post("/walletList", (req, res) => {
// console.log('wallet list');
const list = Wallet.getWalletList();
res.json(list);
});
// view
app.get("/wallet/:account", (req, res) => {
const { account } = req.params;
console.log("wallet", account);
const privateKey = Wallet.getWalletPrivateKey(account); // privateKey 가져오기
res.json(new Wallet(privateKey));
});
"/walletList" 라우터와 "/wallet/:account" 라우터를 위와 같이 추가해줬으며 Wallet 클래스 안에 getWalletList( ) 메소드와 getWalletPrivateKey( ) 메소드를 만들어주었다.
// wallet/ 디렉토리 wallet.ts 파일 내용 추가
// Wallet 클래스 메소드 추가
static getWalletList(): string[] {
// readdirSync() 인자값 : 디렉토리명
// 디렉토리 안에 있는 파일 목록을 가져온다.
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 filecontent.toString();
}
계정명을 파일명으로 하고 privateKey 값을 내용으로 하는 파일들이 data/ 디렉토리 안에 만들어져 있으므로 getWalletList( ) 메소드와 getWalletPrivateKey( ) 메소드 모두 파일시스템을 이용하여 계정명과 privateKey 값을 가져올 수 있도록 하였다.
위와 같이 "지갑목록 버튼"을 눌렀을 때 생성된 지갑의 리스트를 볼 수 있으며 해당 지갑을 클릭했을 때 지갑 정보를 조회할 수 있다. 이제 received 항목과 amount 항목을 채우고 "전송" 버튼을 눌렀을 때 블록체인 인터페이스를 관리하는 HTTP 서버 쪽으로 데이터를 전송하는 과정을 구현해주면 된다.
index.html 안에 작성된 submitHandler( ) 함수를 살펴보면 axios.post( '/sendTransaction', data ) 를 통해 지갑 서버 쪽으로 데이터와 함께 요청을 보내고 있는 것을 확인할 수 있다. 이 때 data 안에 들어가는 값은 다음과 같다.
const submitHandler = async (e) => {
e.preventDefault();
// 본인의 account값과 publicKey값 가져오기
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);
};
해당 요청을 받은 지갑 서버는 보내는 사람의 account 정보를 이용해 signature를 만든 뒤 블록체인 HTTP 서버 쪽으로 서명과 데이터를 전송해주면 된다. wallet/ 디렉토리 안의 server.ts 파일 안에 다음과 같은 "/sendTransaction" 라우터를 추가해주도록 하자.
// wallet/ 디렉토리 server.ts 파일 내용 추가
// sendTransaction
app.post('/sendTransaction', async (req, res) => {
console.log(req.body);
const {
sender: { publicKey, account },
received,
amount,
} = req.body;
// 서명 만들 때 필요한 값: SHA256(보낼사람:공개키 + 받는사람:계정 + 보낼금액).toString()
// HASH + PrivateKey -> 서명
const signature = Wallet.createSign(req.body);
// txObject 내용 -> 보낼사람:공개키, 받는사람:계정, 보낼금액, 서명
const txObject = {
sender: publicKey, // publicKey를 사용해 account를 구할 수 있다.
received,
amount,
signature,
};
// 블록체인 인터페이스 관리 HTTP 서버에 요청
const response = await request.post('/sendTransaction', txObject);
console.log(response.data);
res.json({});
});
Wallet.createSign( ) 메소드를 사용해 서명(signature)을 만들어주고 있는 것을 확인할 수 있는데 wallet/ 디렉토리 안의 wallet.ts 파일에서 Wallet 클래스의 메소드로 createSign( ) 함수를 다음과 같이 만들어주었다.
// wallet/ 디렉토리 wallet.ts 파일 내용 추가
static createSign(_obj: any): elliptic.ec.Signature {
const {
sender: { publicKey, account },
received,
amount,
} = _obj;
// hashing
const hash: string = SHA256([publicKey, received, amount].join('')).toString();
// privateKey
const privateKey: string = Wallet.getWalletPrivateKey(account);
// 서명(signature)
const keyPair: elliptic.ec.KeyPair = ec.keyFromPrivate(privateKey); // string 형태의 privateKey를 타원곡선 알고리즘에 활용하기
return keyPair.sign(hash, 'hex');
}
Wallet 클래스 안에 static 메소드로 전달받은 데이터와 elliptic 라이브러리의 메소드를 활용해 서명(signature)을 만들어주는 createSignature( ) 함수를 만들어 주었다. 서명을 만드는 과정은 앞서 여러차례 설명하였으므로 생략하겠다.
이렇게 만든 서명(signature)과 트랜잭션 데이터를 txObject 라고 하는 객체 안에 담아 request.post( '/sendTransaction', txObject ) 를 사용해 블록체인 인터페이스 관리용 HTTP 서버에 전송하였다.
여기서 axios.create( ) 메소드를 사용해 만들어준 request 변수에 대해 짚고 넘어갈 필요가 있다.
// wallet/ 디렉토리 server.ts 파일 내용
const userid = process.env.USERID || 'web7722';
const userpw = process.env.USERPW || '1234';
const baseURL = process.env.BASEURL || 'http://localhost:3000';
const baseAuth = Buffer.from(userid + ':' + userpw).toString('base64');
const request = axios.create({
// baseURL , header, authorization 내용 세팅
baseURL,
headers: {
Authorization: 'Basic ' + baseAuth,
'Content-type': 'application/json',
},
});
axios.create( ) 메소드는 axios 인스턴스를 반환하게 되는데 axios를 사용해 요청을 보낼 때 디폴트 세팅값을 넣어주기 위한 용도로 사용된다. 위의 코드에서는 baseURL 값을 지정해 놓았으며 headers 속성으로 들어가는 Authorization 속성값과 "Content-type" 속성값을 지정해주었다. 이렇게 axios.create( ) 를 사용해 request 변수를 만들어준다면 axios로 요청을 보낼 때 다음과 같이 사용 가능하다.
axios.post( "http://localhost:3000" ) === request.post( "/" )
블록체인 인터페이스를 관리하는 HTTP 서버의 경우 인증된 사용자만 조회가 가능하도록 처리해야 하는 이슈가 있다. 이 때 Authorization: Basic 방식을 사용하게 되는데 해당 방식으로 인증되지 않은 사용자의 경우 조회가 불가능하도록 처리를 해주어야만 한다. 블록체인 인터페이스 관리 HTTP 서버 파일에서 다음의 라우터를 추가해주도록 하자.
// 루트 디렉토리 index.ts 파일 (블록체인 인터페이스 관리 http 서버)
// 다른 사람이 내 노드의 블록을 조회하는 것을 방지하기 위함.
// header에 있는 authorization 조회
app.use((req, res, next) => {
// 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();
// console.log(userid, userpw);
next();
});
app.use( (req, res, next) => { } ) 를 사용해 요청 헤더의 authorization 부분에 userid = "web7722" , userpw = "1234" 정보를 실어 요청을 보내온 사용자에 한해서만 서버 조회가 가능하도록 처리해주었다.
3. 블록체인 HTTP 서버에서 검증하기
이제 루트 디렉토리 안에 있는 index.ts 파일(블록체인 인터페이스 관리용 HTTP 서버)에서 "/sendTransaction" 라우터를 다음과 같이 추가해주도록 하자.
// 루트 디렉토리 안의 index.ts 파일
import { P2PServer } from './src/server/p2p';
import peers from './peer.json';
import express, { Request, Response } from 'express';
import { ReceivedTx, Wallet } from '@core/wallet/wallet';
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();
// console.log(userid, userpw);
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;
const newBlock = ws.addBlock(data);
if (newBlock.isError) return res.status(500).send(newBlock.error);
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();
});
블록체인 인터페이스를 관리하는 HTTP 서버 쪽에서는 트랜잭션 내용을 블록체인 네트워크에 전송하기 전에 지갑 서버에서 만든 서명(signature)에 대한 검증을 먼저 진행해줘야만 한다.
"/sendTransaction" 라우터에서는 req.body 안에 담긴 receivedTx 객체( 지갑 서버에서 전송한 txObject )를 사용해 지갑 서버 쪽에서 만든 서명에 대한 검증 작업을 진행해준 다음 트랜잭션을 내용을 만들어 블록체인 네트워크에 전송하는 부분을 구현해주고자 한다. 해당 과정은 Wallet.sendTransaction( ) 메소드를 통해 구현해주었다.
여기서 사용된 Wallet 클래스는 지갑 서버 쪽에서 사용하는 Wallet 클래스가 아닌 src/core/wallet/ 디렉토리 안의 wallet.ts 파일 안에서 작성된 Wallet 클래스로 서명에 대한 검증과 트랜잭션 내용을 만들어 주는 작업을 해주는 클래스이다.
src/core/wallet/ 디렉토리 안에 있는 wallet.ts 파일의 내용은 다음과 같다.
// 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();
}
}
static 메소드를 사용해 getVerify( ) 함수를 만들어주었는데 해당 함수 안에서는 공개키를 사용해 서명을 검증해주는 부분을 구현하였다. elliptic 라이브러리에서 제공해주는 함수를 사용하였으며 해당 부분 역시 이전 포스팅에서 다뤘던 내용이므로 생략하도록 하겠다.
서명에 대한 검증이 완료되었다면 현재 가지고 publicKey 와 account 정보를 이용해 트랜잭션 내용을 만들어줘야만 한다. 해당 부분에 대한 내용은 다음번 포스팅에서 이어서 작성하도록 하겠다.
다음 글)
2022.06.21 - [BlockChain] - BlockChain - [블록체인 네트워크] 트랜잭션
2022.06.22 - [BlockChain] - BlockChain - 블록체인 지갑 서버 만들기 (2) 코인베이스 트랜잭션
아래는 현재까지 작성된 파일들이다.
👉 지갑 서버 server.ts 파일
// 지갑 서버 servet.ts 파일
import express from 'express';
import nunjucks from 'nunjucks';
import { Wallet } from './wallet';
import axios from 'axios';
const app = express();
const userid = process.env.USERID || 'web7722';
const userpw = process.env.USERPW || '1234';
const baseURL = process.env.BASEURL || 'http://localhost:3000';
const baseAuth = Buffer.from(userid + ':' + userpw).toString('base64');
// axios 디폴트 세팅
// axios.create() 메소드 : axios 인스턴스를 반환
// 디폴트값을 세팅할 때 사용하는 메소드
const request = axios.create({
// baseURL , header, authorization 내용 세팅
baseURL,
headers: {
Authorization: 'Basic ' + baseAuth,
'Content-type': 'application/json',
},
});
// axios.post('http://localhost:3000') === request.post('/')
app.use(express.json());
app.set('view engine', 'html');
nunjucks.configure('views', {
express: app,
watch: true,
});
app.get('/', (req, res) => {
res.render('index');
});
app.post('/newWallet', (req, res) => {
res.json(new Wallet());
});
// list
app.post('/walletList', (req, res) => {
// console.log('wallet list');
const list = Wallet.getWalletList();
res.json(list);
});
// view
app.get('/wallet/:account', (req, res) => {
const { account } = req.params;
console.log('wallet', account);
const privateKey = Wallet.getWalletPrivateKey(account); // privateKey 가져오기
res.json(new Wallet(privateKey));
});
// sendTransaction
app.post('/sendTransaction', async (req, res) => {
console.log(req.body);
const {
sender: { publicKey, account },
received,
amount,
} = req.body;
// 서명 만들 때 필요한 값: SHA256(보낼사람:공개키 + 받는사람:계정 + 보낼금액).toString()
// HASH + PrivateKey -> 서명
const signature = Wallet.createSign(req.body);
// txObject 내용 -> 보낼사람:공개키, 받는사람:계정, 보낼금액, 서명
const txObject = {
sender: publicKey, // publicKey를 사용해 account를 구할 수 있다.
received,
amount,
signature,
};
console.log(txObject);
const response = await request.post('/sendTransaction', txObject);
console.log(response.data);
res.json({});
});
app.listen(3005, () => {
console.log('server onload #port: 3005');
});
👉 지갑 서버 wallet.ts 파일
// 지갑 서버 wallet.ts 파일
import { randomBytes } from 'crypto';
import elliptic from 'elliptic';
import { SHA256 } from 'crypto-js';
import fs from 'fs'; // Nodejs 파일시스템(fs)
import path from 'path';
// const dir = __dirname; // 현재 디렉토리
const dir = path.join(__dirname, '../data');
const ec = new elliptic.ec('secp256k1');
export class Wallet {
public account: string;
public privateKey: string;
public publicKey: string;
public balance: number;
// privateKey를 인자값으로 넣을 경우, 해당 privateKey를 이용해서 생성
// 인자값이 없을 경우 기존에 만들어 놓은 메소드를 이용해 생성
constructor(_privateKey: string = '') {
this.privateKey = _privateKey || this.getPrivateKey();
// this.privateKey = this.getPrivateKey();
this.publicKey = this.getPublicKey();
this.account = this.getAccount();
this.balance = 0;
Wallet.createWallet(this);
}
static createWallet(myWallet: Wallet): void {
// 파일만들기
// writeFileSync() 인자값 : 1.파일명, 2.파일 안에 들어갈 내용들
// 파일명을 account / 내용을 privateKey
const filename = path.join(dir, myWallet.account);
// console.log(filename);
const filecontent = myWallet.privateKey;
fs.writeFileSync(filename, filecontent);
}
static getWalletList(): string[] {
// readdirSync() 인자값 : 디렉토리명
// 디렉토리 안에 있는 파일 목록을 가져온다.
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 filecontent.toString();
}
static createSign(_obj: any): elliptic.ec.Signature {
const {
sender: { publicKey, account },
received,
amount,
} = _obj;
// hashing
const hash: string = SHA256([publicKey, received, amount].join('')).toString();
// privateKey
const privateKey: string = Wallet.getWalletPrivateKey(account);
// 서명
const keyPair: elliptic.ec.KeyPair = ec.keyFromPrivate(privateKey); // string 형태의 privateKey를 타원곡선 알고리즘에 활용하기
return keyPair.sign(hash, 'hex');
}
public getPrivateKey(): string {
return randomBytes(32).toString('hex');
}
public getPublicKey(): string {
// 개인키 -> 공개키
// 현재 개인키의 type은 string -> elliptic이 해석할 수 있게 변환 작업 필요
const keyPair = ec.keyFromPrivate(this.privateKey);
return keyPair.getPublic().encode('hex', true);
}
public getAccount(): string {
return Buffer.from(this.publicKey).slice(26).toString();
}
}
👉 루트 디렉토리 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';
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;
const newBlock = ws.addBlock(data);
if (newBlock.isError) return res.status(500).send(newBlock.error);
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();
});
👉 src/core/wallet/ 디렉토리 wallet.ts 파일
// 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);
console.log(verify.isError);
// 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();
}
}
'BlockChain' 카테고리의 다른 글
BlockChain - 블록체인 지갑 서버 만들기 (2) 코인베이스 트랜잭션 (0) | 2022.06.22 |
---|---|
BlockChain - [블록체인 네트워크] 트랜잭션 (0) | 2022.06.21 |
BlockChain - 개인키, 공개키, 서명, 지갑/계정 (1) | 2022.06.17 |
BlockChain - 블록체인 P2P 네트워크 만들기 (3) (2) | 2022.06.16 |
BlockChain - 블록체인 P2P 네트워크 만들기 (2) (0) | 2022.06.15 |