[blockchain] 2. Naivechanin 코드리뷰! - HTTPServer

in #blockchain6 years ago (edited)

본 포스팅은 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 객체인 apphttp_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.indexpreviousBlock.index 보다 1만큼 크지 않다면 나가리 되네요.
두 번째는 previousBlock.hashnewBlock.previousHash와 같지 않아도 나가리 되네요. 포인터 역할을 하는 해시값은 당연히 같아야겠죠?
세 번째는 calculateHashForBlock(newBlock) 함수를 호출하네요. 그럼 우리는 저 함수가 뭔지 한번 가봐야죠.

var calculateHashForBlock = (block) => {
    return calculateHash(block.index, block.previousHash, block.timestamp, block.data);
};

앞에서 많이 본 함수가 호출되네요. calculateHash() 함수는 뭐였죠? 네. 새로운 해시값을 만드는 함수였죠.
즉, newBlockindex, 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개의 필드가 있는데 typedata가 있군요. 뭔가 이 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에서 루프를 돌면서 각각의 socketmessage를 전달 하네요. 이부분은 다음 포스팅에 등장할 P2P Interface 부분을 보시면 아~ 이거구나~ 하실껀데 지금은 그냥 새로운 블록이 생성됬다는걸 블록체인의 모든 노드들에게 알려주는 기능이라고만 알고 넘어가시면 됩니다. 여기서 message는 위에서 만든 typedata를 갖고 있는 객체가 되겠죠.

/mineBlock 의 마지막 두 줄은 newBlock을 콘솔에 출력하고, res 객체를 통해 끝났다고 전달하면 마이닝의 한 사이클이 완성되게 됩니다. (휴, 왜이리 길죠?)

이제 코드의 맨~~~ 마지막 부분을 한번 보도록 하죠.

initHttpServer();

우리가 열심히 들여다 봤던 initHttpServer() 함수를 호출하는군요.
네 이로써 HTTP Interface가 완성 되었습니다. 단, P2P Interface 부분은 아직 없죠. 이건 다음 포스팅에 이어서 리뷰하도록 할께요.

두서없이 정신사납고 지저분한 리뷰를 보시느라 욕보셨습니다.
다음번에는 좀 더 깔끔하고 젠틀하게 하도록 노력해 볼께요.

그게 언제가 될지는 아무도 모르지만요 ^^


Naivechain 코드리뷰 시리즈

[blockchain] 1. Naivechanin 코드리뷰! - 소개

Sort:  

2018년에는 두루 평안하시길!

네 감사합니다 ^^