main.js
import { initHttpServer } from "./httpServer.js";
import { initP2PServer } from "./p2pServer.js";
import { initWallet } from "./wallet.js";
const httpPort = parseInt(process.env.HTTP_PORT) || 3001;
const p2pPort = parseInt(process.env.P2P_PORT) || 6001;
initWallet()
initHttpServer(httpPort);
initP2PServer(p2pPort);
main2.js
import { initHttpServer } from "./httpServer.js";
import { initP2PServer } from "./p2pServer.js";
const httpPort = parseInt(process.env.HTTP_PORT) || 3002;
const p2pPort = parseInt(process.env.P2P_PORT) || 6002;
initHttpServer(httpPort);
initP2PServer(p2pPort);
httpServer.js
// 웹에 명령어를 입력해서 내 노드를 제어하는 서버
// const epress = require('express') // 전부다 가져온다.
import express from 'express'; // 필요한 것만 가져와서 간결하게 쓸수 있다.
import bodyParser from 'body-parser';
import { getBlocks, createBlock, getUnspentTxOuts } from './block.js';
import { connectionToPeer, getPeers, mineBlock } from './p2pServer.js';
import { getPublicKeyFromWallet } from './wallet.js'
import { getTransactionPool, sendTransaction } from './transaction.js'
// import path from 'path';
// 초기화 함수
const initHttpServer = (myHttpPoryt) => {
const app = express();
// const __dirname = path.resolve();
app.use(bodyParser.json());
app.get('/', (req, res) => {
//res.sendFile(path.join(__dirname, './index.html'));;
res.send('Hello, World!');;
})
app.get('/blocks', (req, res) => {
res.send(getBlocks());
})
// app.post('/createblock', (req, res) => {
// const data = req.body.data
// res.send(createBlock(data));
// })
app.post('/createblock', (req, res) => {
res.send(createBlock(req.body.data));
})
app.post('/mineBlock', (req, res) => {
res.send(mineBlock(req.body.data));
})
app.post('/peers', (req, res) => {
res.send(getPeers())
})
app.post('/addPeer', (req, res) => {
console.log('/addrPeer : ', req.body.message);
res.send(connectionToPeer(req.body.data));
})
// app.post('/sendMessage', (req, res) => {
// res.send(sendMessage(req.body.data))
// })
// app.post('/allblocks', (req, res) => {
// res.send(responseAllMessage(req.body.data))
// })
// app.use('/latestblock', (req, res) => {
// res.send(responseLatestMessage(req.body.data))
// })
app.post('/allblocks', (req, res) => {
res.send(queryAllMessage(req.body.data))
})
app.use('/latestblock', (req, res) => {
res.send(queryLatestMessage(req.body.data))
})
app.get('/address', (req, res) => {
const address = getPublicKeyFromWallet();
res.send( {'address' : address } );
})
app.post('/sendTransaction', (req, res) => {
const address = req.body.address; // 트랙잭션 아웃 정보
const amount = req.body.amount; // 트랙잭션 아웃 정보
res.send(sendTransaction(address, amount));
})
app.get('/transactions', (req, res) =>{
res.send(getTransactionPool());
})
app.get('/unspentTxOuts', (req, res) => {
res.send(getUnspentTxOuts());
})
app.listen(myHttpPoryt, () => {
console.log('listening httpServer Port : ', myHttpPoryt);
})
}
export { initHttpServer }
block.js
// 블록체인 관련 함수
// 블록 구조 설계
/*
index : 블록체인의높이 height
data : 블록에 포함된 모든데이터 (트랜잭션 포함)
timestamp : 블록이 생성된 시간
hash : 블록 내부 데이터로 생성한 sha256 값 (블록의 유일성 보장)
previousHash : 이전 블록의 해쉬(이전 블록을 참조한다)
*/
import _ from 'lodash'
import random from 'random';
import CryptoJS from 'crypto-js' // 모듈이 없다고 나오면 npm install
import { getCoinbaseTransaction, getTransactionPool, updateTransactionPool, processTransaction, addToTransactionPool } from './transaction.js'
import { getPublicKeyFromWallet } from './wallet.js'
const BLOCK_GENERATION_INTERVAL = 10; // SECOND
const DIFFICULTY_ADJUSTMENT_INTERVAL = 10; // generate block count
class Block {
constructor(index, data, timestamp, hash, previousHash, difficulty, nonce)
{
this.index = index;
this.data = data;
this.timestamp = timestamp;
this.hash = hash;
this.previousHash = previousHash;
this.difficulty = difficulty;
this.nonce = nonce;
}
}
// function getBlocks() {
// return blocks;
// }
// let unspentTxOuts = []; // UnspentTxOut []
let unspentTxOuts = processTransaction(getTransactionPool() /* Transaction[] */, [] /* UnspentTxOut[] */, 0 /* blockIndex */);
const getUnspentTxOuts = () => {
return _.cloneDeep(unspentTxOuts);
}
const getBlocks = () => {
return blocks;
}
const getLatestBlock = () => {
return blocks[blocks.length - 1];
}
const calculateHash = (index, data, timestamp, previousHash, difficulty, nonce) => {
// return CryptoJS.SHA256(index + data + timestamp + previousHash).toString();
return CryptoJS.SHA256((index + data + timestamp + previousHash + difficulty + nonce).toString()).toString();
// 0 하나로 시작하는 hash값을 만드는 매개변수 (nonce)를 찾는다.
// 0 두개로 시작하는 hash값을 만드는 매개변수를 찾는다. 난이도 오른다.
// 16진수 64자리
// 16진수 1자리 -> 2진수 4자리로 / 256개의 0과 1로 표현
// return CryptoJS.SHA256((2).toString()).toString();
}
const createGenesisBlock = () => {
const genesisBlock = new Block(0, 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks', 0 /* new Date().getTime() / 1000 */, 0, 0, 1, 0);
genesisBlock.hash = calculateHash(genesisBlock.index, genesisBlock.data, genesisBlock.timestamp, genesisBlock.previousHash, genesisBlock.difficulty, genesisBlock.nonce);
genesisBlock.data = getCoinbaseTransaction(getPublicKeyFromWallet(), 1); // 아래에서 추가됨
// addToTransactionPool(genesisBlock.data);
unspentTxOuts = processTransaction([genesisBlock.data], getUnspentTxOuts(), 0);
return genesisBlock;
}
const genesisBlock = createGenesisBlock()
// genesisBlock.data = getCoinbaseTransaction(getPublicKeyFromWallet(), getLatestBlock().index + 1);
let blocks = [genesisBlock];
const createBlock = (blockdata) => {
const previousBlock = blocks[blocks.length - 1];
const nextIndex = previousBlock.index + 1;
const nextTimestamp = new Date().getTime() / 1000;
const nextDifficulty = getDifficulty();
const nextNonce = findNonce(nextIndex, blockdata, nextTimestamp, previousBlock.hash, nextDifficulty);
const nextHash = calculateHash(nextIndex, blockdata, nextTimestamp, previousBlock.hash, nextDifficulty, nextNonce);
const newBlock = new Block(nextIndex, blockdata, nextTimestamp, nextHash, previousBlock.hash, nextDifficulty, nextNonce);
return newBlock; // 간소화
}
const createNextBlock = () => {
// 1. 코인베이스 트랜잭션 생성
const coinbaseTx = getCoinbaseTransaction(getPublicKeyFromWallet(), getLatestBlock().index + 1);
// 2. 생성된 코인베이스 트랜잭션 뒤에 현재 보유 중인 트랜잭션 풀의 내용을 포함 (마이닝된 블록의 데이터)
const blockdata = [coinbaseTx].concat(getTransactionPool())
return createBlock(blockdata);
}
const addblock = (newBlock, previousBlock) => {
if (isValidNewBlock(newBlock, previousBlock)) {
blocks.push(newBlock);
// 사용되지 않는 txOuts 셋팅
processTransaction(newBlock.data, getUnspentTxOuts(), newBlock.index);
// 트랜잭션 풀 업데이트
updateTransactPool(unspentTxOuts);
return true;
}
return false;
}
// 블록의 무결성 검증
/*
블록의 인덱스가 이전 블록인덱스보다 1 크다.
블록의 previousHash가 이전 블록의 hash 이다.
블록의 구조가 일치해야 한다.
*/
const isValidBlockStructure = (newBlock) => {
if (typeof (newBlock.index) === 'number'
&& typeof (newBlock.data) === 'string'
&& typeof (newBlock.timestamp) === 'number'
&& typeof (newBlock.hash) === 'string'
&& typeof (newBlock.previousHash) === 'string'
&& typeof (newBlock.difficulty) === 'number'
&& typeof (newBlock.nonce) === 'number') {
return true;
}
return false;
}
/*
(typeof (newBlock.index) !== 'number'
|| typeof (newBlock.data) !== 'string'
|| typeof (newBlock.timestamp) !== 'number'
|| typeof (newBlock.hash) !== 'string'
|| typeof (newBlock.previousHash) !== 'string' )
*/
const isValidNewBlock = (newBlock, previousBlock) => {
if (newBlock.index !== previousBlock.index + 1) // !== 타입까지 비교해준다.
{
console.log('invalid index');
return false;
}
else if (newBlock.previousHash !== previousBlock.hash)
{
console.log('invalid previous hash');
return false;
}
else if(isValidBlockStructure(newBlock) == false) {
console.log('invalid previous structure')
return false;
}
return true;
}
// let testHash = calculateHash(10, 20, 30, 40);
// console.log(testHash);
// console.log(testHash.length);
// 문제 해결을 검사하는 함수
const hashMatchDifficulty = (hash, difficulty) => {
const binaryHash = hexToBinary(hash);
const requiredPrefix = '0'.repeat(difficulty);
return binaryHash.startsWith(requiredPrefix);
}
const hexToBinary = (hex) => {
const lookupTable = {
'0' : '0000', '1' : '0001', '2' : '0010', '3' : '0011',
'4' : '0100', '5' : '0101', '6' : '0110', '7' : '0111',
'8' : '1000', '9' : '1001', 'a' : '1010', 'b' : '1011',
'c' : '1100', 'd' : '1101', 'e' : '1110', 'f' : '1111'
}
// 03cf
// 0000001111001111
let binary = '';
for(let i =0; i < hex.length; i++)
{
if(lookupTable[hex[i]]) {
binary += lookupTable[hex[i]]
}
else {
console.log('invalid hex : ', hex);
return null;
}
}
return binary;
}
const isValidBlockchain = (receiveBlockchain) => {
// 같은 제네시스 블록인가
// JSON.stringify(receiveBlockchain[0]) === JSON.stringify(getBlocks()[0])
if (JSON.stringify(receiveBlockchain[0]) !== JSON.stringify(getBlocks()[0])) {
console.log('같은 제네시스 블록이 아님');
console.log(receiveBlockchain[0]);
console.log('-------------------------')
console.log(getBlocks()[0]);
return false;
}
// 체인내의 모든 블록을 확인
for(let i = 1; i < receiveBlockchain.length; i++)
{
if(isValidNewBlock(receiveBlockchain[i], receiveBlockchain[i - 1]) == false)
{
console.log(i - 1, '번 블록과 ', i, '번 블록이 문제');
console.log(receiveBlockchain[i - 1]);
console.log(receiveBlockchain[i]);
return false;
}
}
console.log('블록체인 확인 완료')
return true;
}
const findNonce = (index, data, timestamp, previousHash, difficulty) => {
let nonce = 0;
while(true)
{
let hash = calculateHash(index, data, timestamp, previousHash, difficulty, nonce);
if (hashMatchDifficulty(hash, difficulty)) {
return nonce;
}
nonce++;
}
}
// 통채로 교체가 필요가 있을때
const replaceBlockchain = (receiveBlockchain) => {
console.log(receiveBlockchain);
if (isValidBlockchain(receiveBlockchain))
{
//let blocks = getblock();
if ((receiveBlockchain.length > blocks.length) ||
receiveBlockchain.length == blocks.length && random.boolean())
{
console.log('받은 블록체인의 길이가 길거나 같아서 바꿈');
blocks = receiveBlockchain;
// for(let i = 0; i < newBlocks.length - 1; i++ ){
// blocks[i] = newBlocks[i];
// }
// 받은 블록체인이 현재 블록체인보다 더 길면 (바꿈)
// 사용되지 않은 txOuts 셋팅
const latestBlock = getLatestBlock();
processTransaction(latestBlock.data, getUnspentTxOuts(), latestBlock.index);
// 트랜잭션 풀 업데이트
updateTransactionPool(unspentTxOuts);
}
}
else{
console.log('받은 블록체인에 문제가 있음');
}
}
const getAdjustmentDifficulty = () => {
// 현재 (만들 블록의) 시간, 마지막으로 난이도 조정된 시간 인터벌로 정의한 시간보다 적으면 난이도 낮추고 정의한 시간보다 크면 난이도를 올린다
const prevAdjustedBlock = blocks[blocks.length - DIFFICULTY_ADJUSTMENT_INTERVAL - 1];
const latestBlock = getLatestBlock();
const elapsedTime = latestBlock.timestamp - prevAdjustedBlock.timestamp;
const expectedTime = DIFFICULTY_ADJUSTMENT_INTERVAL * BLOCK_GENERATION_INTERVAL;
if (elapsedTime > expectedTime * 2)
{
// 오래걸린다 -> 난이도를 낮춘다.
return prevAdjustedBlock.difficulty - 1;
}
else if (elapsedTime < expectedTime / 2)
{
// 적게걸린다 -> 난이도를 높인다.
return prevAdjustedBlock.difficulty + 1;
}
else
{
return prevAdjustedBlock.difficulty;
}
}
const getDifficulty = () => {
const latestBlock = getLatestBlock();
// 난이도 조정 주기 확인
if (latestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0 &&
latestBlock.index !==0 ) {
return getAdjustmentDifficulty()
}
return latestBlock.difficulty;
}
export { getBlocks, getLatestBlock, createBlock, addblock, isValidNewBlock, replaceBlockchain, getUnspentTxOuts };
p2pServer.js
// peer to peer = p2p, 노드 vs 노드, 개인과 개인, 서로가 필요한 정보를 복사해주며 공유해가며 통신
// 다른노드와 통신을 위한 서버
import WebSocket from 'ws'
import { WebSocketServer } from 'ws'; // 포트만 넣어주면 서버만들어주는 친구
import { getBlocks, getLatestBlock, createBlock, addblock, replaceBlockchain } from './block.js'
import { getTransactionPool, addToTransactionPool } from './transaction.js'
const MessageType = {
// RESPONSE_MESSAGE : 0,
// SENT_MESSAGE : 1
// 최신 블록 요청
QUERY_LATEST : 0,
// 모든 블록 요청
QUERY_ALL : 1,
// 블록 전달
RESPONSE_BLOCKCHAIN : 2,
QUERY_TRANSACTION_POOL : 3,
RESPONSE_TRANSACTION_POOL : 4
}
const sockets = []; //
const getPeers = () => {
return sockets;
}
const initP2PServer = (p2pPort) => {
const server = new WebSocketServer({port:p2pPort})
server.on('connection', (ws) => {
initConnection(ws);
//console.log("온건가?")
})
console.log('listening P2PServer Port : ', p2pPort);
}
const initConnection = (ws) => {
sockets.push(ws);
initMessageHandler(ws);
write(ws, queryAllMessage());
//console.log("ddd")
}
const connectionToPeer = (newPeer) => { // newPeer = ip:port
console.log(newPeer)
const ws = new WebSocket(newPeer) // ws = 상대방의 웹소켓 정보
ws.on('open', () => { initConnection(ws); console.log('Connect peer : ', newPeer ); })
ws.on('error', () => { console.log('Fail to Connection peer : ', newPeer); })
}
const initMessageHandler = (ws) => {
ws.on('message', (data) => {
const message = JSON.parse(data); // 메시지를 제이슨으로 변경
switch(message.type)
{
// case MessageType.SENT_MESSAGE: // 메시지 받았을 때
// console.log(ws._socket.remoteAddress, ' : ', message.message);
// break;
case MessageType.QUERY_LATEST:
break;
case MessageType.QUERY_ALL:
write(ws, responseAllMessage());
break;
case MessageType.RESPONSE_BLOCKCHAIN:
console.log(ws._socket.remoteAddress, ' : ', message.data);
//replaceBlockchain(message.data);
handleBlockchainResponse(message.data);
break;
case MessageType.QUERY_TRANSACTION_POOL:
write(ws, responseTransactionPoolMessage());
break;
case MessageType.RESPONSE_TRANSACTION_POOL:
handleTransactionPoolResponse(message.data)
break;
}
})
}
const handleBlockchainResponse = (receiveBlockchain) => {
const newBlocks = JSON.parse(receiveBlockchain)
// 받아온 블록의 마지막 인덱스가 내 마지막 블록의 인덱스보다 크다.
const latestNewBlock = newBlocks[newBlocks.length - 1];
console.log('받아온 마지막 블록 : ', latestNewBlock)
const latestMyBlock = getLatestBlock();
console.log('내 마지막 블록 : ', latestMyBlock)
// 받아온 마지막 블록의 previousHash와 내 마지막 블록의 hash를 확인한다.
if(latestNewBlock.index > latestMyBlock.index)
{
if (latestNewBlock.previousHash === latestMyBlock.hash)
{
if (addblock(latestNewBlock, latestMyBlock))
{ // 제한된 flooding 플러딩을 사용한다 = 최대한 많이 전파한다
broadcasting(responseLatestMessage());
}
}
// 받아온 블록의 전체 크기가 1인 경우 -> 재요청
else if(newBlocks.length === 1)
{
broadcasting(queryAllMessage());
}
else
{
replaceBlockchain(newBlocks);
// 그외
// // 받은 블록체인보다 현재 블록체인이 더 길다 (안바꿈)
// // 같으면. (바꾸거나 안바꿈) //
// // 받은 블록체인이 현재 블록체인보다 더 길면 (바꿈)
}
}
}
const handleTransactionPoolResponse = (recieveTransactionPool) => {
const recieveTransactions = JSON.parse(recieveTransactionPool)
console.log('recieveTransactionPool : ', recieveTransactions);
recieveTransactions.forEach((transaction) => {
addToTransactionPool(transaction);
// 중복검사, 트랜잭션 풀에 추가
// 다시 전파
})
// recieveTransactionPool.forEach((transaction) => {
// addToTransactionPool(transaction);
// // 중복검사, 트랜잭션 풀에 추가
// // 다시 전파
// })
// addToTransactionPool
}
const queryLatestMessage = () => {
return ({
"type" : MessageType.QUERY_LATEST,
"data" : null })
}
const queryAllMessage = () => {
return ({
"type" : MessageType.QUERY_ALL,
"data" : null })
}
const responseLatestMessage = () => {
return ({
"type" : MessageType.RESPONSE_BLOCKCHAIN,
"data" : JSON.stringify([getLatestBlock()]) }) /* 내가 가지고 있는 체인의 마지막 블록 */
}
const responseAllMessage = () => {
return ({
"type" : MessageType.RESPONSE_BLOCKCHAIN,
"data" : JSON.stringify(getBlocks()) }) /* 내가 가지고 있는 전체 블록 */
}
const responseTransactionPoolMessage = () => {
return ({
"type" : MessageType.RESPONSE_TRANSACTION_POOL,
"data" : JSON.stringify(getTransactionPool()) })
}
const write = (ws, message) => { // ws 보낼 상대방의 웹소켓정보
console.log('write()', ws._socket.remoteAddress, ' : ', message);
ws.send(JSON.stringify(message)) // 제이슨을 메시지로 변경
} // 내가 상대방의 메시지를
const broadcasting = (message) => {
sockets.forEach( (socket) => {
write(socket, message);
});
}
const mineBlock = (blockdata) => {
const newBlock = createBlock(blockdata);
if(addblock(newBlock, getLatestBlock()))
{
broadcasting(responseLatestMessage());
}
}
// JSON.parse 와 JSON.stringify 서로 변경할수 있다.
// const responseMessage = () => {
// console.log()
// }
// 내가 새로운 블록을 채굴했을 때 연결된 노드들에게 전파
const broadcastingTransactionPool = () => {
broadcasting(responseTransactionPoolMessage())
}
export { initP2PServer, connectionToPeer, getPeers, broadcasting, mineBlock, broadcastingTransactionPool}
wallet.js
/*
암호화
블록체인
탈중앙화
분산원장관리
무결성 : 정보는 일반적으로 수정이 가능한데, 이는 권한이 있는 사용자에게만 허가
기밀성 : 정보를 저장하고 전송하면서 부적절한 노출을 방지, 정보 보안의 주된 목적
가용성 : 활용되어야 할 정보에 접근할 수 없다면, 기밀성과 무결성이 훼손된 것만큼이나 무의미하다.
지갑
프라이빗키 으로 퍼블릭키를 만들수 있지만 퍼블릭키로 프라이빗키를 유추할수 없다
private Key
public Key
타원 곡선 디지털 서명 알고리즘 (ECDSA)
영지식증명 (Zero Knowledge Proof)
증명하는 사람(A), 증명을 원하는 사람(B)
A와 B는 증명된 내용에 합의
그 외의 사람들은 동의하지 않는다.
증명하는 과정에서 A는 B에게 아무런 정보도 주지 않는다.
*/
import ecdsa from 'elliptic'
import fs from 'fs';
const ec = new ecdsa.ec('secp256k1');
const privateKeyLocation = 'wallet/' + (process.env.PRIVATE_KEY || 'defalut' );
const privateKeyFile = privateKeyLocation + '/private_key';
const createPrivateKey = () => {
const keyPair = ec.genKeyPair();
const privateKey = keyPair.getPrivate();
// console.log(privateKey);
// console.log(privateKey.toString(16));
return privateKey.toString(16)
}
const initWallet = () => {
// 이미 만들어져 있을 때
if (fs.existsSync(privateKeyFile)) {
console.log('지갑에 비밀키가 만들어져 있음');
return;
}
if (!fs.existsSync('wallet/')) { fs.mkdirSync('wallet/'); } // 폴더가 없으면 만들어줘
if (!fs.existsSync(privateKeyLocation)) { fs.mkdirSync(privateKeyLocation); } // 폴더에 로케이션이 없으면 만들어줘
const privateKey = createPrivateKey();
fs.writeFileSync(privateKeyFile, privateKey);
}
const getPrivateKeyFromWallt = () => {
const buffer = fs.readFileSync(privateKeyFile, 'utf-8');
return buffer.toString();
}
const getPublicKeyFromWallet = () => {
const privateKey = getPrivateKeyFromWallt();
const publicKey = ec.keyFromPrivate(privateKey, 'hex');
// console.log(publicKey.getPublic().encode('hex'));
return publicKey.getPublic().encode('hex')
}
//initWallet();
// console.log(getPrivateKeyFromWallt());
// console.log(createPublickey());
const createPublickey = (privateKey) => {
const publicKey = ec.keyFromPrivate(privateKey, 'hex')
return publicKey.getPublic().encode('hex')
}
export { initWallet, getPublicKeyFromWallet, getPrivateKeyFromWallt , createPrivateKey, createPublickey}
// var string = '045f91db2dc67817c4af3fa7eb3f325ed77d0c759cba0d4adb7a05eca6fc9efc02d723a8ca40d78d70873a2e3497b36b1866ce3c0f24b22691ca79565ef2fbe068';
// console.log(string.length);
// TODO api 중 숫자를 단어로 바꿔주는거 쓸거임
transaction.js
import CryptoJS from 'crypto-js'
import _ from 'lodash' // 배열기능이 있고 깊은 복사의 유용한 기능들이 있는 라이브러리
import { getPublicKeyFromWallet, getPrivateKeyFromWallt } from './wallet.js'
import { broadcastingTransactionPool } from './p2pServer.js'
import { getUnspentTxOuts } from './block.js';
const COINBASE_AMOUNT = 50;
let transactionPool = [];
// let transactionPool = [getLatestBlock().data/* genesisblock.data */];
const getTransactionPool = () => {
return _.cloneDeep(transactionPool);
}
class UnspentTxOut {
constructor(txOutId, txOutIndex, address, amount) {
this.txOutId = txOutId;
this.txOutIndex = txOutIndex;
this.address = address;
this.amount = amount;
}
}
// let trans = [...transactionPool] 깊은 복사가 되지만 1단계까지만 됨
// 얕은 복사만 일어나게 된다 ( 구조체 안에 다른 클래스의 배열들안에 다른 데이터들 처럼 중첩되었기 때문 )
// 코인을 어디로 얼만큼 보냈는가
class TxOut {
constructor(address, amount ) {
this.address = address; // string
this.amount = amount; // number
}
}
// 보내진 코인이 실제로 소유했다에 대한 증거
class TxIn {
constructor(txOutId, txOutIndex, sign) {
this.txOutId = txOutId; // string
this.txOutIndex = txOutIndex; // number
this.sign = sign; // string
}
}
// 구조체 안에 스트링이 있고 다른 클래스 배열들(안에 다른데이터) 로 구성
class Transaction {
constructor(id, txIns, txOuts) {
this.id = id; // string
this.txIns = txIns; // TxIn []
this.txOuts = txOuts; // TxOut []
}
}
// transaction id
const getTransactionId = (transaction) => {
// txIns 에 있는 내용들을 하나의 문자열로 만든다.
// const txInsContent = transaction.txIns
// .map((txIn) => txIn.txOutId + txIn.txOutIndex)
// .reduce((a, b) => a + b, '');
/* 아래가 조금 복잡
const txInsContent = transaction.txIns.map((txIn) => {
(txIn.txOutId + txIn.txOutIndex).reduce((a, b) => {
a + b, ''
})
*/
// txOuts 에 있는 내용들을 하나의 문자열로 만든다.
const txOutsContent = transaction.txOuts
.map((txOut) => txOut.address + txOut.amount)
.reduce((a, b) => a + b, '');
// 위 두 내용을 다 합해서 hash 처리한다.
return CryptoJS.SHA256(/* txInsContent */ + txOutsContent).toString()
}
// 이 내용이 변조되지않았다
// transaction signature
const signTxIn = (transaction, txInIndex, privateKey) => {
// const txIn = transaction.txIns[txInIndex];
// TODO : sign 코드 검증
const signature = toHexString(privateKey, transaction.id).toDER();
return signature;
}
// 누가 보냈는지
// coinbase Transaction
const getCoinbaseTransaction = (address, blockIndex) => {
const tr = new Transaction();
const txIn = new TxIn();
txIn.sign = '';
txIn.txOutId = '';
txIn.txOutIndex = blockIndex;
const txOut = new TxOut();
txOut.address = address;
txOut.amount = COINBASE_AMOUNT;
tr.txIns = [txIn];
tr.txOuts = [txOut];
tr.id = getTransactionId(tr);
return tr;
}
const sendTransaction = (address, amount) => {
// 1. 트랜잭션 생성
const tx = createTransaction(address, amount);
// console.log('2 : ',tx);
// 2. 트랜잭션 풀에 추가
transactionPool.push(tx);
// 3. 주변 노드에 전파
broadcastingTransactionPool();
return tx;
}
const createTransaction = (address, amount) => {
// 미사용 TxOuts 에서 사용할 내용들을 추출
const unspentTxOuts = getUnspentTxOuts();
const {includeTxOuts, leftoverAmount} = findTxOutsForAmount(amount, unspentTxOuts);
// 서명되지않은 TxIns 구성
const unsignedTxIns = includeTxOuts.map(createUnsignedTxIn);
console.log('unsignedTxIns : ', unsignedTxIns);
const tx = new Transaction();
// 서명
tx.txIns = unsignedTxIns;
tx.txOuts = createTxOuts(address, amount, leftoverAmount)
tx.id = getTransactionId(tx);
// console.log('1 : ',tx.txOuts);
return tx;
}
const filterTxPoolTxs = (myUnspentTxOuts) => {
// 트랜잭션 풀에서 트랜잭션 인풋 내용만 추출 // 내가 올린것과 남이 올린것을 구분지어줄거야
const txIns = _(transactionPool)
.map((tx) => tx.txIns) // transactionPool 안에서 txIns들만 하나씩 가져와서 새로운 배열을 구성한다
.flatten() // 일차원 배열로 만들어준다
.value();
console.log('트랜잭션 풀 : ', transactionPool);
console.log('트랜잭션 풀안의 Inputs : ', txIns);
const removable = [];
for (const unspentTxOuts of myUnspentTxOuts) {
const findTxIn = _.find(txIns, (txIn) => {
return txIn.txOutIndex === unspentTxOuts.txOutIndex &&
txIn.txOutId === unspentTxOuts.txOutId;
})
if (findTxIn === undefined) {
}
else {
removable.push(unspentTxOuts);
}
}
_.without(myUnspentTxOuts, ...removable);
}
// 5 5 5 5 5 => 17 개 보낼때 => 5 5 5 5 보내면서 => 마지막 5개중 3개 나에게 보내기
const findTxOutsForAmount = (amount, filterUnspentTxOuts) => {
let currentAmount = 0;
const includeTxOuts = [];
for (const unspentTxOuts of filterUnspentTxOuts) {
includeTxOuts.push(unspentTxOuts);
currentAmount = currentAmount + unspentTxOuts.amount;
if (currentAmount >= amount) {
const leftoverAmount = currentAmount - amount;
return { includeTxOuts, leftoverAmount };
}
}
throw Error('보내려는 금액보다 보유 금액이 적다!!');
}
const createUnsignedTxIn = (unspentTxOut) => {
const txIn = new TxIn();
txIn.txOutId = unspentTxOut.txOutId;
txIn.txOutIndex = unspentTxOut.txOutIndex;
return txIn;
}
// const createUnsignedTxIn = (unspentTxOuts) => {
// const txIn = new TxIn();
// txIn.txOutId = unspentTxOuts.txOutId;
// txIn.txOutIndex = unspentTxOuts.txOutIndex;
// return txIn;
// }
const createTxOuts = (address, amount, leftoverAmount) => {
const txOut = new TxOut(address, amount);
if (leftoverAmount > 0) {
const leftoverTxOut = new TxOut(getPublicKeyFromWallet(), leftoverAmount);
return [leftoverTxOut, txOut];
}
else {
return [txOut];
}
}
const addToTransactionPool = (transaction) => {
// 올바른 트랜잭션인지
// if (!isValidateTransaction(transaction, unspentTxOuts)) {
// throw Error('추가하려는 트랜잭션이 올바르지 않습니다. : ', transaction);
// }
// 중복되는지
if (!isValidateTxForPool(transaction)) {
throw Error('추가하려는 트랜잭션이 트랜잭션 풀에 있습니다. : ', transaction);
}
transactionPool.push(transaction);
}
const isValidateTransaction = (transaction, unspentTxOuts) => {
// 트랜잭션 아이디가 올바르게 구성되어있는지
if (getTransactionId(transaction) !== transaction.id) {
console.log('invalid transaction id : ', transaction.id);
return false;
}
const totalTxInValues = transaction.txIns
.map((txIn) => getTxInAmount(txIn, unspentTxOuts))
.reduce((a, b) => (a + b), 0); // 데이터값에 따라 스트링은 이어붙여주고 숫자int 는 합해준다. 객체지향 프로그램에선 오버로딩!! 이름은 같은데 매개변수의 타입에 따라 다른함수 기능을 하는것을 오버로딩이라고 한다. 나온값들을 합해서 하나의 값으로 짧은 에로우 펑션은 {} 생략 가능
const totalTxOutValues = transaction.txOuts
.map((txOut) => txOut.amount)
.reduce((a, b) => (a + b), 0);
if ( totalTxInValues !== totalTxOutValues) {
console.log( 'totalTxInValues !== totalTxOutValues id : ', transaction.id );
return false;
}
return true;
}
const getTxInAmount = (txIn, unspentTxOuts) => {
const findUnspentTxOut = unspentTxOuts.find((uTxO) => uTxO.txOutId === txIn.txOutId &&
uTxO.txOutIndex === txIn.txOutIndex );
return findUnspentTxOut.amount;
}
const isValidateTxForPool = (transaction) => {
// 트랜잭션 풀에 있는 txIns 들과 transaction 에 txIns 들을 비교해서 같은 것이 있는지 확인
const txPoolIns = _(transactionPool)
.map((tx) => tx.txIns)
.flatten()
.value(); // 속에 있는걸 겉으로 꺼내왔다
const containTxIn = (txIn) => {
return _.find(txPoolIns, (txPoolIn) => {
return txIn.txOutIndex === txPoolIn.txOutIndex &&
txIn.txOutId === txPoolIn.txOutId;
})
}
for (const txIn of transaction.txIns) {
if (containTxIn(txIn)) {
console.log('이미 존재하는 트랜잭션입니다. : ', transaction.id);
return false;
}
}
return true;
}
const updateTransactionPool = () => {
const removable = [];
// 1. 현재 트랜잭션 풀에 있는 트랜잭션 중에
// 사용되지 않은 TxOuts내용과 일치하지 않는 트랜잭션들을 제거한다.
for (const tx of transactionPool) {
for (const txIn of tx.txIns) {
if (isInTx(txIn)) {
}
else {
removable.push(tx)
break;
}
}
}
transactionPool = _.without(transactionPool, ...removable);
}
const isInTx = (txIn) => {
const findTxOut = _(unspentTxOuts).find((uTxO) => { uTxO.txOutIndex === txIn.txOutIndex &&
uTxO.txOutId === txIn.txOutId });
return findTxOut !== undefined;
}
// processTransaction(transactions /* Transaction[] */, [] /* UnspentTxOut[] */, 0 /* blockIndex */);
const processTransaction = (transactions , unspentTxOuts , blockIndex) => {
console.log('processTransaction : ',transactions);
// 1. 예외처리 (트랜잭션 구조를 검증하는 과정)
// if (isValidateBlockTransaction(transactions , unspentTxOuts , blockIndex)) {
// console.log('invalid processTransaction!')
// return null;
// }
// 2. 미사용 txouts를 추출하는 과정
// 2_1. 블록에 있는 데이터 (처리해야 할 트랜잭션 정보) 중에서 txIns로 소모된 txOuts(UnspentTxOut)를 구성
const consumedTxOuts = transactions.map((tx) => tx.txIns) // txIns로 구성된 배열로 변경
.reduce((a, b) => a.concat(b), [])
.map((txIn) => new UnspentTxOut(txIn.txOutId, txIn.txOutIndex, '', 0));
console.log('consumedTxOuts : ',consumedTxOuts);
// 2_2. 새로 들어온 트랜잭션 정보에서 추출한 UnspentTxOut 생성
const newUnspentTxOuts = transactions.map((tx) => {
return tx.txOuts.map((txOut) => new UnspentTxOut(tx.id, blockIndex, txOut.address, txOut.amount));
})
.reduce((a, b) => a.concat(b), []);
console.log('newUnspentTxOuts : ',newUnspentTxOuts);
// 2_3. 기존 UnspentTxOut - 소모된 UnspentTxOut + newUnspentTxOuts 을 추가
// 두 1차원 배열의 (txOutId와 txOutIndex 를 비교해서 같은 요소)를 filter 하는 코드를 만들어보자.
const resultUnspentTxOuts = (unspentTxOuts.filter((uTxO) => !checkSameElement(consumedTxOuts, uTxO.txOutIndex, uTxO.txOutId))).concat(newUnspentTxOuts);
console.log('resultUnspentTxOuts : ',resultUnspentTxOuts);
unspentTxOuts = resultUnspentTxOuts;
return resultUnspentTxOuts;
}
const checkSameElement = (txOuts, txOutIndex, txOutId) => {
return txOuts.find((txOut) => txOut.txOutId === txOutId && txOut.txOutIndex === txOutIndex);
}
export { getTransactionPool, sendTransaction, addToTransactionPool, getCoinbaseTransaction, updateTransactionPool, processTransaction }