블록, 체인 만들기와 이어서 지갑까지 만든다.
0. 블록체인
(1) 머클트리
(2) 블록체인 네트워크
SHA256 알고리은 256bit로 구성된 64자리 문자열로 암호화 한다.
머클루트의 값은 64자리이다.
머클트리 자체가 해시로 이루어져 있고 하나의 트랙잭션이나 블록의 값이 변조되면 머클루트 해시값이 변조되고 잘못된 해시값이 나오게 되면 해당 블록이 잘못된것으로 인식하여 블록을 거부한다.
이를 통해 블록체인 네트워크를 안정적으로 유지할 수 있다.
(3) 블록체인 동작
브로드 캐스팅(내가 새로운 거래를 만들었다는것을 알려준다)
- 지갑 프로그램에 연결된 peerlist 확인
- 네트워크 브로드 캐스팅
- 채굴 시작(다른사람이 만든 거래를 승인(블록 생성)해주고 수수료 받음)
- 채굴 성공(내가 찾은 nonce를 모두에게 알린다)
- 채굴에 성공한 블록을 전달
- 네트워크 브로드 캐스팅
- 블록 검증 후 블록체인에 연결
(4) 채굴
비트코인 화폐를 발행하는 방식은 채굴이고 비트코인 화폐는 일정시간에 한번씩 일정량이 생성되며
채굴에 참여한 사용자중 한명에게 지급된다.
참여자들은 hashcash라는 문제를 풀고 hashcash는 특정한 조건을 가지는 해시값을 찾는 것이다.
1. 암호화폐의 지갑
(1) 지갑의 필요성
지갑은 자신의 암호화폐를 다른 누군가에게 양도하기 위해서 사용하는 소프트웨어의 한 종류라고 보면 된다.
개인키가 있어야 본인의 지갑에 접근이 가능하다.
지갑은 암호화폐 자산을 관리하고 Dapps(탈중앙화 애플리케이션)과 상호작용을 하기 위해서 사용한다.
(2) 공개키&개인키
공개키는 우리가 사용하는 은행 계좌번호라고 생각하면 된다.
개인키는 비밀 핀번호 or 계좌 관리를 위한 수표의 서명과 비슷하다.
공개키로는 네트워크 참여자의 거래내역의 사람들이 거래가 정상인지 아닌지를 확인할 수 있다.
개인키를 이용해서 직접 거래할때 서명을 하기 때문에 잃어버린다면 본인이 소유하고 있는 암호화폐를 전송할 수 없다.
공개키, 개인키를 암호화 할때 단방향으로 암호화를 한다.
개인키로 공개키를 만들어주고 만든 공개키로 주소를 만든다.
단방향 암호화이기 때문에 공개키는 개인키를 알아내는건 불가능하고 또한 주소로 공개키를 알아내는건 불가능하다.
공개키는 타원 곡선 알고리즘이라는 수학적인 알고리즘으로 연산과정을 거쳐 개인키로 만들수 있다.
공개키는 모든사람들에게 공개되어도 상관이 없다.
공개키의 역할은 암호화폐 전송 받을때 사용하고 거래내역이 유효한지를 검사해준다.
암호화폐를 전송하는 사람이 전송하기 전에 암호화폐를 보유하고 있었는지 확인 가능하기 때문에 이중 지불을 방지한다.
2. /view/index.html
지갑을 만들고 전송시키기 위해 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" />
<title>Document</title>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<h1>지갑이다</h1>
<button id="walletBtn">지갑 생성</button>
<ul id="walletList">
<li>코인 : 비트코인</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="transactionForm">
<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="walletListBtn">지갑 목록 버튼</button>
<div>
<ul id="walletList2">
목록 버튼 눌러주세요
</ul>
</div>
</body>
<script>
const account = document.querySelector(".account");
const publicKey = document.querySelector(".publicKey");
const privateKey = document.querySelector(".privateKey");
const balance = document.querySelector(".balance");
const view = (wallet) => {
account.innerHTML = wallet.account;
publicKey.innerHTML = wallet.publicKey;
privateKey.innerHTML = wallet.privateKey;
balance.innerHTML = wallet.balance;
};
const createWallet = async () => {
const response = await axios.post("/newWallet", null);
console.log(response.data);
view(response.data);
};
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);
};
const getView = async (account) => {
// 계정 정보
const response = await axios.get(`/wallet/${account}`);
view(response.data);
};
const getWalletList = async () => {
const walletList = walletList2;
const response = await axios.post("/walletList", null);
const list = response.data
.map((account) => {
return `<li onClick="getView('${account}')">${account}</li>`;
})
.join("");
walletList.innerHTML = list;
};
walletBtn.addEventListener("click", createWallet);
walletListBtn.addEventListener("click", getWalletList);
transactionForm.addEventListener("submit", submitHandler);
</script>
</html>
3. /wallet/wallet.ts
// 지갑 클래스
import { randomBytes } from "crypto";
import elliptic from "elliptic";
import { SHA256 } from "crypto-js";
import fs from "fs";
import path from "path";
const dir = path.join(__dirname, "../data");
// elliptic 인스턴스 생성
const ec = new elliptic.ec("secp256k1");
export class Wallet {
// 지갑의 주소
public account: string;
// 지갑의 개인키
public privateKey: string;
// 지갑의 공개키
public publicKey: string;
// 암호 화폐
public balance: number;
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모듈을 사용해서 프로그램을 통해 지갑을 만들때 개인키를 안전하게 저장하는게 중요함
// fs 모듈을 이용해서 개인키를 저장할 파일 만들기
// writeFileSync 함수의 매개변수(파일이름, 내용)
// 지갑의 주소를 파일 이름으로 data폴더 경로까지 내용
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 {
// 타원 곡선 알고리즘을 사용해서 개인키를 이용하여 공개키를 만들어 준다.
const keyPair = ec.keyFromPrivate(this.privateKey);
return keyPair.getPublic().encode("hex", true);
}
// 지갑의 목록 가져오기
static getWalletList(): string[] {
const files: string[] = fs.readdirSync(dir);
// 파일 이름이 담긴 string 배열을 반환
return files;
}
// 정보를 받고 개인키를 구해주는 함수
static getWalletPrivateKey(account: string): string {
const filepath = path.join(dir, account);
// data 폴더에 만들어진 파일을 가져오고 해당 파일을 읽어옴
const fileContent = fs.readFileSync(filepath);
// 내용으로 적혀있는 개인키를 문자열로 반환
return fileContent.toString();
}
// 전자 서명 만드는 함수
static createSign(obj: any): elliptic.ec.Signature {
const {
sender: { publicKey, account },
received,
amount,
} = obj;
// obj는 server.ts에서 전달 받는 값
// 합쳐서 해싱하고 문자열로 저장
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 getAccount(): string {
return Buffer.from(this.publicKey).slice(26).toString();
}
}
4. /wallet/server.ts
지갑의 서버를 따로 만들어준다.
// 지갑 서버
import express from "express";
import { Wallet } from "./wallet";
import axios from "axios";
import nunjucks from "nunjucks";
const app = express();
nunjucks.configure("view", {
express: app, // express 속성에 우리가 만든 express 연결해준것
watch: true, // watch 옵션은 true면 html파일이 변경되면 템플릿 엔진이 리로드 시켜줌
});
app.set("view engine", "html");
// axios 사용할때 디폴드값 세팅
const baseURL = "http://localhost:3000";
const baseAuth = Buffer.from("seok" + ":" + "1234").toString("base64");
const request = axios.create({
baseURL,
headers: {
// api서버에서 데이터를 요청 응답할때 http Authorization 헤더에 유저의 아이디와
// 비밀번호를 base64형태로 인코딩한 문자열을 추가해서 인증하는 방식 base64로
// 인코딩 되어 전송되기 때문에 중간에 공격에 취약하기는 하다.
Authorization: "Basic " + baseAuth,
"Content-type": "application/json",
},
});
app.use(express.json());
app.get("/", (req, res) => {
res.render("index");
});
app.post("/newWallet", (req, res) => {
res.json(new Wallet());
});
app.post("/walletList", (req, res) => {
const list = Wallet.getWalletList();
res.json(list);
});
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) => {
const {
sender: { publicKey, account },
received,
amount,
} = req.body;
// 서명 만들기
// 필요한 값은 SHA256(보낸사람 공개키 + 받는 사람 : 계정 + 보낼 금액)
const signature = Wallet.createSign(req.body);
// 보낼사람 : 공개키
// 받는사람 : 계정, 서명
const txObject = {
sender: publicKey,
received,
amount,
signature,
};
const response = await request.post("/sendTransaction", txObject);
console.log(response.data);
res.json({});
});
app.listen(4000, () => {
console.log("서버 4000번에 열렸다.");
});
5. /src/core/wallet/wallet.ts
import elliptic from "elliptic";
import { SHA256 } from "crypto-js";
// elliptic 인스턴스 생성
const ec = new elliptic.ec("secp256k1");
// 지갑에 전송할 수신 형태
export interface ReceivedTx {
sender: string;
received: string;
amount: number;
signature: elliptic.ec.Signature;
}
export class Wallet {
// 지갑의 주소
public account: string;
// 지갑의 공개키
public publicKey: string;
// 암호화폐
public balance: number;
// 서명
public signature: elliptic.ec.Signature;
constructor(sender: string = "", signature: elliptic.ec.Signature) {
// 공개키
this.publicKey = sender;
// 지갑의 주소
this.account = this.getAccount();
// 서명
this.signature = signature;
// 화폐
this.balance = 0;
}
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();
// 공개키로 서명 검증
const keyPair = ec.keyFromPublic(sender, "hex");
const isVerify = keyPair.verify(hash, signature);
if (!isVerify) return { isError: true, value: "서명 검증 실패" };
return { isError: false, value: undefined };
}
static sendTransaction(receivedTx: ReceivedTx) {
// 서명 검증
// 공개키, 보내는사람 : 공개키, 받는사람 : 계정, 보낼 금액
const verify = Wallet.getVerify(receivedTx);
if (verify.isError) throw new Error(verify.value);
// 보내는 사람의 지갑 정보를 최신화
// 현재 가지고 있는 정보 공개키, 실제 트랜젝션에 넣을 정보는 account 정보
const myWallet = new Wallet(receivedTx.sender, receivedTx.signature);
console.log(myWallet);
}
// 지갑의 주소
public getAccount(): string {
// Buffer에 있는 동안 바이너리 데이터를 조작할수 있기 때문
return Buffer.from(this.publicKey).slice(26).toString();
}
}
6. concurrently
설치후 start
한번에 2개의 서버를 열어줘야 한다.
터미널을 2개 켜고 명령어를 따로 설정하여 2개 킬수 있지만 명령어 한번에 동시에 키고싶어서 concurrenlty 사용
npm i concurrently
설치후 package.json 의 scripts 수정
"scripts": {
"start": "concurrently \"ts-node ./wallet/server.ts\" \"ts-node ./index.ts\""
}
수정 후 npm start를 해보면 동시에 2개의 서버가 열리는 것을 확인 할수 있다.
'개발 > BlockChain' 카테고리의 다른 글
[BlockChain] ubuntu 환경 설정 (0) | 2022.11.21 |
---|---|
[BlockChain] TypeScript로 transaction 만들기 (0) | 2022.11.15 |
[BlockChain] TypeScript로 P2P 구현 (0) | 2022.11.07 |
[BlockChain] TypeScript로 체인 만들기 (0) | 2022.11.03 |
[BlockChain] TypeScript로 블록 만들기 (1) | 2022.11.02 |