본 포스팅은 Apache License Version 2.0 라이센스를 따르는 https://github.com/lhartikk/naivechain 의 내용과 소스코드를 공유하는 포스팅입니다. 참고해주시기 바랍니다.
잡담
얼마전 Bitfinex에서 EOSfinex를 발표했습니다. (Link)
EOS를 통한 탈 중앙화 거래소를 만든다고 하네요.
이거 보고 EOS 더 매수하고 싶었지만 돈이 없어서 못샀네요... (물려있는 코인들이 많아서... ㅠㅠ)
본격적으로 들어가기 앞서
코드가 약 200라인정도 됩니다. 적다면 적고, 많다면 많을 수 있겠지만 어떻게 리뷰를 하는것이 좋을까 고민을 해봤는데 전 포스팅에서 나왔던 컴포넌트를 기준으로 코드를 나눠서 리뷰를 하면 어떨까 생각을 해봤습니다. 컴포넌트는 아래와 같습니다.
- Blockchain
- HTTP Interface
- P2P Interface
이 중 Blockchain
의 경우 코드는 아래서 보시면 아시겠지만 그냥 Array 입니다. 설마 $8.5K나 하는 비트코인을 배열로 만들 수 있다고? 당연히 아니죠. 그냥 간단하게 구현하기 위해 Array로 표현했을 뿐 입니다. 그래서 이번 포스팅에서 Blockchain
컴포넌트도 같이 리뷰하는게 어떨까 싶네요.
즉, 이번 포스팅에서는
- Blockchain
- HTTP Interface
두 가지를 다루고, 다음 포스팅에서는
- P2P Interface
하나만 다룹니다.
자 그럼 시작하죠!
코드 리뷰
먼저 선언부가 나오는군요. 당연하죠.
'use strict';
var CryptoJS = require("crypto-js");
var express = require("express");
var bodyParser = require('body-parser');
var WebSocket = require("ws");
var http_port = process.env.HTTP_PORT || 3001;
var p2p_port = process.env.P2P_PORT || 6001;
var initialPeers = process.env.PEERS ? process.env.PEERS.split(',') : [];
4개의 모듈을 require()
하고 있는데 내용은 아래와 같습니다.
- crypto-js: JavaScript용 암호화 모듈이다. SHA-256 알고리즘을 사용하기 위해 추가했다.
- express: 너무도 유명한 node.js의 웹서버 프레임워크다. 모르면 간첩.
- body-parser: express에서 post 방식으로 전송된 파라미터를 파싱하기 위한 미들웨어로 사용된다.
- ws: 웹 소켓. (더이상의 자세한 설명은 생략한다)
그리고는 이어서 포트를 저장할 변수들이 나오는군요. http_port
, p2p_port
는 그냥 봐도 알테고... initialPeers
는 뭘 하는 앨까요? 초기 피어를 저장할 변수 같아 보이는군요. 다음으로 넘어가보죠.
다음!
class Block {
constructor(index, previousHash, timestamp, data, hash) {
this.index = index;
this.previousHash = previousHash.toString();
this.timestamp = timestamp;
this.data = data;
this.hash = hash.toString();
}
}
벌써 Block
이라는 엄청난 친구가 등장했습니다. class
는 ES6 문법인데 이거 없었으면 함수 선언하고 프로토타입 설정하고 막 안에 변수들 연결하고 후.. 그래요 ES6는 사랑입니다.
아무튼! 이곳에서 블록의 필드들을 정의하고 있네요. 전 포스팅에서도 언급되었던 Naivechain의 블록 구조가 바로 이것입니다. 각 필드는 아래와 같습니다.
- index: 블록의 순번
- previousHash: 이전 블록의 해시값
- timestamp: 블록이 생성된 시점의 timestamp값
- data: 블록 채굴자 (마이너)가 블록에 기록하고 싶은 데이터
- hash: index + previousHash + timestamp + data 를 SHA-256 알고리즘으로 암호화 한 해시값
다음 라인들은 P2P Interface와 연관되어 보이니깐 일단 넘어가고...
그 다음은 블록을 초기화 하는 코드 입니다.
var getGenesisBlock = () => {
return new Block(0, "0", 1465154705, "my genesis block!!", "816534932c2b7154836da6afc367695e6337db8a921823784c14378abed4f7d7");
};
var blockchain = [getGenesisBlock()];
getGenesisBlock()
함수는 위에서 봤던 Block
클래스의 객체를 생성해내죠. 생성자로 전달되는 값들은 보기 좋게 하드코딩 되어 있습니다. 나의 제네시스 블록!! 이라는 데이터와 함께 말이죠. (현*자동차의 한 모델과 전혀 상관 없음을 미리 밝힙니다)
보통 블록체인의 첫 번째 블록은 제네시스 블록이라고 지칭합니다.
그리고 중요한 부분! Blockchain
컴포넌트가 배열로 생성되었습니다! 벌써 하나의 컴포넌트가 끝났군요. 하하하
다음도 굉장히 중요한 부분이 나옵니다. 눈 크게 뜨시고 잘 봐보세요.
var initHttpServer = () => {
var app = express();
app.use(bodyParser.json());
app.get('/blocks', (req, res) => res.send(JSON.stringify(blockchain)));
app.post('/mineBlock', (req, res) => {
var newBlock = generateNextBlock(req.body.data);
addBlock(newBlock);
broadcast(responseLatestMsg());
console.log('block added: ' + JSON.stringify(newBlock));
res.send();
});
app.get('/peers', (req, res) => {
res.send(sockets.map(s => s._socket.remoteAddress + ':' + s._socket.remotePort));
});
app.post('/addPeer', (req, res) => {
connectToPeers([req.body.peer]);
res.send();
});
app.listen(http_port, () => console.log('Listening http on port: ' + http_port));
};
initHttpServer()
함수는 HTTP Interface를 구현한 하나의 웹서버 입니다. 당연히 express
로 구현 됬구요.
app.use(bodyParser.json())
부분은 JSON 파서를 미들웨어에 등록하는 부분입니다.
그 다음부터는 4개의 라우터가 구현되어 있네요. 내용은 다음과 같습니다.
- /blocks: 현재 블록체인의 모든 블록들 정보를 요청하는 부분입니다.
- /mineBlock: 새로운 블록을 블록체인에 추가하는 부분입니다. 마인 이라고 되어 있죠?
- /peers: 현재 블록체인에 접속되어있는 모든 노드들의 정보를 요청하는 부분입니다.
- /addPeer: 새로운 노드가 현재 블록체인에 추가될 때 호출되는 부분이죠.
그리고 위에서 생성된 express
객체인 app
에 http_port
를 이용해 리스닝 하도록 되어 있네요. 기본 포트는 맨 위에서 본것과 같이 3001
번 입니다.
위의 4개 라우터 중에서 이번 포스팅에서는 /blocks
, /mineBlock
만 보도록 하겠습니다. 왜냐구요? 나머지 두개는 노드의 통신과 관련된 부분이라 다음 포스팅에서 다루도록 할께요.
각각의 라우터를 하나씩 살펴보죠.
app.get('/blocks', (req, res) => res.send(JSON.stringify(blockchain)));
웹서버로 /blocks
라는 URL이 들어오면 묻지도 따지지도 않고 blockchain
을 문자열 형태의 JSON으로 변환해서 응답하고 끝납니다. 이걸 통해서 전체 블록들의 정보를 학인할 수 있죠. 참 쉽죠?
app.post('/mineBlock', (req, res) => {
var newBlock = generateNextBlock(req.body.data);
addBlock(newBlock);
broadcast(responseLatestMsg());
console.log('block added: ' + JSON.stringify(newBlock));
res.send();
});
이번에는 /mineBlock
입니다. 이름만 봐도 뭐 하는지 알겠죠? mine! 즉 채굴을 합니다! 즉 블록을 만들어 냅니다!
이곳에서 추가적으로 4개의 함수 generateNextBlock()
, addBlock()
, broadcast()
, responseLastestMsg()` 가 각각 쓰였는데요. 하나씩 알아보도록 하죠.
현재 함수 내부에서 사용되는 함수는 바로 이어서 작성하는게 좀 더 읽기 쉬운 코드를 만드는 방법입니다 라고 Clean code 에서 봤네요 :D
먼저 generateNextBlock()
을 봅시다.
var generateNextBlock = (blockData) => {
var previousBlock = getLatestBlock();
var nextIndex = previousBlock.index + 1;
var nextTimestamp = new Date().getTime() / 1000;
var nextHash = calculateHash(nextIndex, previousBlock.hash, nextTimestamp, blockData);
return new Block(nextIndex, previousBlock.hash, nextTimestamp, blockData, nextHash);
};
이름만 봐서는 다음 블록을 생성해 낼 것 같은데요. 그 전에 안에 보니깐 또 2개의 함수가 보이네요. 그럼 봐야죠!
먼저 getLatestBlock()
!
var getLatestBlock = () => blockchain[blockchain.length - 1];
너무 심플하네요. blockchain
에서 가장 마지막 블록을 가져오는 겁니다. 그게 다에요. 즉, 마지막 블록을 previousBlock
변수에 할당하는게 다죠.
그 다음 nextIndex
를 만들어 내야 합니다. 어떻게? previousBlock.index
에다가 1
을 더하면 됩니다.
그 다음 nextTimestamp
를 만들어야 하는데 이건 더 쉽죠. 그냥 현재 시간의 timestamp 값을 할당합니다.
그 다음 가장 중요한 nextHash
를 만듭니다. 보니깐 calculateHash()
함수가 사용되네요. 자 그럼 찾아보죠.
var calculateHash = (index, previousHash, timestamp, data) => {
return CryptoJS.SHA256(index + previousHash + timestamp + data).toString();
};
index
, previusHash
, timestamp
, data
를 인자로 받아서 우리가 맨 처음 가져왔던 CryptoJS
모듈의 SHA256()
함수를 사용해서 해시값을 만들어 내는군요. 오... 마이닝이 끝나가는게 느껴집니다.
자 이제 nextHash
값도 만들어 냈으니 블록을 만들어 봅니다.
위의 generateNextBlock()
함수의 마지막을 보시면 new Block(...)
을 통해 새로운 블록을 생성하면서 리턴하네요.
자. 이제 블록 하나가 만들어 졌습니다.
이제 다시 우리가 위에서 보고있던 /mineBlock
라우터로 돌아가 봅시다.
그동안 너무 방대한 양의 리뷰로 까먹었을 수 있으니 다시 코드를 첨부해 볼께요.
app.post('/mineBlock', (req, res) => {
var newBlock = generateNextBlock(req.body.data);
addBlock(newBlock);
broadcast(responseLatestMsg());
console.log('block added: ' + JSON.stringify(newBlock));
res.send();
});
generateNextBlock()
을 봤으니 그 다음 라인인 addBlock(newBlock)
을 볼 차례군요. newBlock
은 바로 위 라인에서 만들었죠. 그럼 이 함수는 이름 그대로 블록을 추가하는 기능이겠군요. 어떻게 추가할까요? 봅시다.
var addBlock = (newBlock) => {
if (isValidNewBlock(newBlock, getLatestBlock())) {
blockchain.push(newBlock);
}
};
if
구문에서 파라미터로 전달된 newBlock
이 올바른지 아닌지 확인을 하네요. 그럼 우리는 isValidNewBlock()
함수가 무슨 근거로 확인하는지 알아봐야겠습니다. 참고로 isValidNewBlock()
함수의 두 번째 인자로 getLatestBlock()
을 호출하죠? 위에서 봤었죠? 뇌스택에서 pop()
해서 뭐였는지 상기해 보면서 다음을 보도록 하죠.
var isValidNewBlock = (newBlock, previousBlock) => {
if (previousBlock.index + 1 !== newBlock.index) {
console.log('invalid index');
return false;
} else if (previousBlock.hash !== newBlock.previousHash) {
console.log('invalid previoushash');
return false;
} else if (calculateHashForBlock(newBlock) !== newBlock.hash) {
console.log(typeof (newBlock.hash) + ' ' + typeof calculateHashForBlock(newBlock));
console.log('invalid hash: ' + calculateHashForBlock(newBlock) + ' ' + newBlock.hash);
return false;
}
return true;
};
함수 내부에 보니깐 총 3번의 validation을 진행하네요. (유식해 보이게 영어좀 써봤어요 후후)
첫 번째 if
구문에서는 newBlock.index
가 previousBlock.index
보다 1
만큼 크지 않다면 나가리 되네요.
두 번째는 previousBlock.hash
와 newBlock.previousHash
와 같지 않아도 나가리 되네요. 포인터 역할을 하는 해시값은 당연히 같아야겠죠?
세 번째는 calculateHashForBlock(newBlock)
함수를 호출하네요. 그럼 우리는 저 함수가 뭔지 한번 가봐야죠.
var calculateHashForBlock = (block) => {
return calculateHash(block.index, block.previousHash, block.timestamp, block.data);
};
앞에서 많이 본 함수가 호출되네요. calculateHash()
함수는 뭐였죠? 네. 새로운 해시값을 만드는 함수였죠.
즉, newBlock
의 index
, previousHash
, timestamp
, data
를 다시 SHA256 알고리즘으로 해시값을 만든 후 newBlock.hash
와 비교하는 겁니다. 당연히 같아야겠죠?
이제 addBlock()
함수의 마지막이네요. validation이 끝났으니 무사히 blockchain
배열에 newBlock
을 추가해주면 됩니다.
마이닝 그까지꺼 아무것도 아니죠? 라고 생각하면 오산입니다. 실제는 엄청 복잡합니다.
다시 /mineBlock
라우터의 소스를 보죠. 자꾸 왔다갔다 하느라 정신 없겠지만 어쩌겠어요. 코딩이란 그런건데요 뭐 ^^
app.post('/mineBlock', (req, res) => {
var newBlock = generateNextBlock(req.body.data);
addBlock(newBlock);
broadcast(responseLatestMsg());
console.log('block added: ' + JSON.stringify(newBlock));
res.send();
});
세 번째 줄에서 broadcast(responseLatestMsg())
를 호출하네요. 먼저 resonseLatestMsg()
를 보도록 하죠.
var responseLatestMsg = () => ({
'type': MessageType.RESPONSE_BLOCKCHAIN,
'data': JSON.stringify([getLatestBlock()])
});
음 이건 뭘까요? 새로운 객체를 생성해서 리턴하는군요. 객체에는 2개의 필드가 있는데 type
과 data
가 있군요. 뭔가 이 type
별로 따르게 동작하는 뭔가가 코드 어딘가에 있을것 같은 기분이 드네요. MessageType
이 뭔지 한번 보죠.
var MessageType = {
QUERY_LATEST: 0,
QUERY_ALL: 1,
RESPONSE_BLOCKCHAIN: 2
};
이것도 그냥 객체군요. 우리는 RESPONSE_BLOCKCHAIN
이 중요한데 이건 그냥 2
가 할당되어 있네요. 일단 이런 타입이 정의되어 있다고 노트에 살포시 적어두고 넘어가도록 하죠.
data
에는 getLatestBlock()
을 스트링 형태의 JSON으로 파싱하네요. getLatestBlock()
은 위에서 많이 봤죠? 그냥 마지막 블록 가져오는 겁니다.
이제 broadcast()
함수를 보도록 하죠.
var broadcast = (message) => sockets.forEach(socket => write(socket, message));
sockets
에서 루프를 돌면서 각각의 socket
에 message
를 전달 하네요. 이부분은 다음 포스팅에 등장할 P2P Interface 부분을 보시면 아~ 이거구나~ 하실껀데 지금은 그냥 새로운 블록이 생성됬다는걸 블록체인의 모든 노드들에게 알려주는 기능이라고만 알고 넘어가시면 됩니다. 여기서 message
는 위에서 만든 type
과 data
를 갖고 있는 객체가 되겠죠.
/mineBlock
의 마지막 두 줄은 newBlock
을 콘솔에 출력하고, res
객체를 통해 끝났다고 전달하면 마이닝의 한 사이클이 완성되게 됩니다. (휴, 왜이리 길죠?)
이제 코드의 맨~~~ 마지막 부분을 한번 보도록 하죠.
initHttpServer();
우리가 열심히 들여다 봤던 initHttpServer()
함수를 호출하는군요.
네 이로써 HTTP Interface가 완성 되었습니다. 단, P2P Interface 부분은 아직 없죠. 이건 다음 포스팅에 이어서 리뷰하도록 할께요.
두서없이 정신사납고 지저분한 리뷰를 보시느라 욕보셨습니다.
다음번에는 좀 더 깔끔하고 젠틀하게 하도록 노력해 볼께요.
그게 언제가 될지는 아무도 모르지만요 ^^
2018년에는 두루 평안하시길!
네 감사합니다 ^^