내멋대로 블록체인 #6 - 간단한 채굴 로직

    저번 시간에는 제네시스 블록 JSON 파일을 이용하여 제네시스 블록을 생성하였고, 제네시스 블록에는 Header와 Transfer의 영역으로 나뉘어져 있으며 Header에는 채굴자의 정보와 블록을 생성하기 위한 정보등이 담겨져 있고, Transfer는 송신자와 수신자가 존재해서 전송하는 기록등을 담게 되었다.


    이번장은 본격적으로 간단한 채굴 로직에 들어가기에 앞서, genesis.json을 이용해서 블록을 생성하는 로직을 소스와 함께 설명해보며, 기본중에 기본인 채굴 로직을 설명하고 다음 장에는 노드를 관리하며 채굴을 하는 방법을 알려드리고자 한다.


    해당 포스팅은 실제 돌아다니고 있는 블록체인의 모습을 자바로 구현을 해본 것일 뿐이며 실제 모습은 이와 많이 다를 수 있다는 점을 염두에 두었으면 좋겠다.



    Genesis Block 생성 소스


    우선 이거 한가지는 확실히 알아둬야 하는 것이, Genesis Block의 설정 파일인 genesis.json 자체가 블록 모양을 가지고 있다는 점이다. 필자가 만드는 내멋대로 블록체인은 블록의 모양을 JSON 형태로 구현하고 있다. 원래는 데이터의 보안과 압축 로직을 써서 소스 코드가 아니면 이해할 수 없게 만들어 놓겠지만 여기서는 그런 부분에 대해서 고민하지 않았다.


    일단 내가 그런 분야에 대한 전문가도 아닐 뿐더러, 지금 만들고 있는 1차 버전은 오로지 자바 + 웹에서 구동이 되는 블록체인일 뿐이지 나머지는 심각한 고민도 하지 않는 것이다. 1차 버전(Prototype)이 모두 만들어지면, 위에 관련된 부분을 추가로 고민할 예정이다. 


    Controller

    1
    2
    3
    4
    boolean genesis = cryptoModule.setGenesisBlock();
    model.addAttribute("genesis", genesis);
     
    return "jsonView";
    cs


    setGenesisBlock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    BufferedReader inFiles;
    BufferedWriter bw;
     
    StringBuffer sb = new StringBuffer();
     
    try {
        inFiles = new BufferedReader(
                new InputStreamReader(
                new FileInputStream("c:/steelj/genesis.json"), "UTF8"));
     
        String line = "";
        while((line = inFiles.readLine()) != null) {
            if(line.trim().length() > 0) {
                sb.append(line.toString());
            }
        }
     
        inFiles.close();
    catch (Exception e) {
        LOGGER.error("get genesis : " + e.getMessage());
        return false;
    }
    cs


    우선 당연하겠지만, genesis.json 파일을 읽어 들인다. FileInputStream안에 있는 경로는 우선 개발을 원활하게 하기 위해서 "c:/steelj" 라는 폴더를 하드코딩하였는데 이런 부분은 properties에 맞춰서 변경하거나 genesis 블록을 못찾을 경우 폴더를 입력받게 해서 경로를 시스템 내부에 기록하는 방법도 있을 것이다.


    우선 JSON 파일을 읽어야 하기 때문에 InputStream을 써서 모든 라인을 StringBuffer에 기록한다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    try {            
        bw = new BufferedWriter(
            new OutputStreamWriter(
            new FileOutputStream(
            "c:/steelj/chain/00000"false),    // true to append 
            StandardCharsets.UTF_8));    // set encoding utf-8
     
        JSONParser parser = new JSONParser();
        JSONObject obj = (JSONObject) parser.parse(sb.toString());
        JSONObject block = new JSONObject();
     
        // 헤더 세팅
        block.put("header", genesisHeader((JSONObject)obj.get("header")));
     
        // 전송 세팅
        block.put("transfer", genesisTransfer((JSONArray)obj.get("transfer")));
     
        // 렛저 세팅
        block.put("ledger", genesisLedger((JSONArray)obj.get("transfer")));
     
        bw.write(block.toJSONString());
        bw.close();            
     
        bw = new BufferedWriter(
            new OutputStreamWriter(
            new FileOutputStream(
            "c:/steelj/chain/sequence"false),    // true to append 
            StandardCharsets.UTF_8));    // set encoding utf-8
     
        bw.write("00000");
        bw.close();
    catch(Exception e){
        e.printStackTrace();
        LOGGER.error("set genesis : " + e.getMessage());
        return false;
    }
     
    return true;
    cs


    StringBuffer 파일을 기반으로 이제 파일을 2개 생성하게 되는데 하나는 0번째 블록이고, 또 하나는 시퀀스(sequence) 파일이다. 시퀀스는 마지막 블록의 위치를 기록한 파일이며, 추후 시퀀스를 없애고 서버가 기동하면 모든 블록을 검증하면서 전 블록을 다시 읽는 방식으로 변경할 계획이다.


    위 소스를 실행(http://localhost:8080/steelj/block/genesis)하면, 아래와 같은 00000 파일의 블록이 생성되어진다.



    00000 파일 내용

    {"ledger":[{"address":"0000000000000000000000000000000000000000","balance":"5000000"}],

    "transfer":[{"stlj":"1000000","memo":"자바로 만든 내멋대로 블록체인",

    "from":"void","to":"0000000000000000000000000000000000000000"}],

    "header":{"reward":"1000000","hashed":"steelj",

    "miner":"0000000000000000000000000000000000000000","age":"20181217093443"}}


    아직 여러가지 설정들이 하드코딩이 되어진 상태이다. 블록의 난이도와 블록의 Limit 등은 추후 구현이 될 예정이고 현재는 Limit은 정해지지 않았고 난이도 역시 하드코딩된 상태라고 보면 된다.



    간단한 채굴 로직


    채굴 로직은 매우 간단하다. 이전 해시값(여기서는 hashed라는 값)을 읽어 들이고, 새로운 문장을 랜덤으로 생성하여 Java에서 기본으로 제공하는 sha256 알고리즘으로 채굴한다.


    Controller

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    miningFlag = true;
     
    String no = cryptoModule.getHashed();    // 시퀀스의 번호를 가져온다
    String msg = "";
     
    IDVO vo = walletService.getIDInfo();    // 지갑의 아이디 정보를 가져온 후
    HashMap<String, AddressVO> map = vo.getAddressMap();    // address를 세팅한다
    String address = "";
     
    for(String _address : map.keySet()) {
        address =  map.get(_address).getAddress();
        break;
    }
     
    // address가 있을 경우, MiningModule의 Thread를 가동시킨다
    if(address.trim().length() > 0) {
        module = new MiningModule(no, address);
        module.start();
    else {
        msg = "address not found.";
    }
     
    model.addAttribute("isMining", miningFlag);
    model.addAttribute("msg", msg);
     
    return "jsonView";
    cs



    MiningModule의 run

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public void run() {
        int blockSeq = Integer.parseInt(no);
     
        // 정답을 위한 해시작업
        while (executeFlag) {
            long startTime = System.currentTimeMillis();
            
            // 예전 블록의 정보를 가져온다            
            HashMap<String, Object> bfInfo = crypto.getBlockInfo(String.valueOf(blockSeq));            
            HashMap<StringString> map = mining((String)bfInfo.get("hashed"));
            
            long endTime = System.currentTimeMillis();
            blockSeq++;
            
            // 블록 생성
            crypto.setBlockJson(map, blockSeq, (endTime-startTime));
            
            // temp의 블록 제거
            crypto.deleteTemp();
            
            // 시퀀스값 기록
            crypto.setSequence(blockSeq);
        }
    }
    cs


    MiningModule은 Thread를 상속받아서 구현하기 때문에, 중요 구동 부분은 run() 메소드 안에 담겨져 있다.



    mining 메소드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    do {
        try {
            word = crypto.generateRandomWord(30);
            merge = crypto.sha256(hashed + word);
            flag = crypto.isCorrect(merge); 
            
            if(flag) {
                System.out.println(merge + " " + executeFlag);
            }
            
            Thread.sleep(10);
        } catch (Exception e) {
            e.printStackTrace();
        }
    while(!flag && executeFlag);
    cs

    do ~ while 문으로 반복 하되, 우선 이전 해시값(hashed)와 결합할 랜덤 문자열을 generate 하고, 둘을 결합하여 sha256으로 hashing한다. 그렇게해서 만들어진 hash 값을 isCorrect 메소드로 검증하여 값을 체크한 후 flag에 담게 된다.


    generateRandomWord

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public String generateRandomWord(int wordLength) {
        Random r = new Random();
        StringBuilder sb = new StringBuilder(wordLength);
        
        for(int i = 0; i < wordLength; i++) {
            char tmp = (char) ('a' + r.nextInt('z' - 'a'));
            sb.append(tmp);
        }
        
        return sb.toString();
    }
    cs


    ascii index 값을 이용하여 소문자 영문을 랜덤으로 생성한다. 예제에서는 30자의 문자열을 기반으로 생성하였다. (이 부분은 자유롭게 수정해도 될 부분, 예를 들어 실패한 문자열의 마지막 단어만 바꿔서 연산을 줄이는 방법도 존재한다)


    sha256

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    /**
     * SHA-256으로 해싱하는 메소드
     * 
     * @param bytes
     * @return
     * @throws NoSuchAlgorithmException 
     */
    public String sha256(String msg) throws Exception {
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        md.update(msg.getBytes());
        
        return bytesToHex(md.digest());
    }
     
     
    /**
     * 바이트를 헥스값으로 변환한다
     * 
     * @param bytes
     * @return
     */
    public String bytesToHex(byte[] bytes) {
        StringBuilder builder = new StringBuilder();
        for (byte b: bytes) {
          builder.append(String.format("%02x", b));
        }
        return builder.toString();
    }
    cs


    Java에서는 MessageDigest를 기본으로 제공하고 있다. 우선 SHA-256으로 연산 한 후, bytesToHex라는 메소드를 호출 하여 bytes 값을 String 값으로 변환한다.


    isCorrect

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
     /**
     * 결과 체크, 난이도
     *  
     * @param str
     * @return
     */
    public boolean isCorrect(String str) {
        // 난이도 조절 첫번째
        int len = 2;
        
        for(int i = 0; i < len; i++) {
            // 마지막 값, 난이도 조절 2번째 설정해야 될 부분
            if(i == len-1) {
                if(hex2Decimal(String.valueOf(str.charAt(i))) < 6) {
                    return true;
                }
            } 
            // 0 이 아니면 false
            else {
                if(hex2Decimal(String.valueOf(str.charAt(i))) > 0) {
                    return false;
                }
            }                        
        }
        
        return false;
    }
    cs


    난이도를 생성하는 메소드이다. 난이도의 조정은 앞자리부터 값을 체크하는 방식(ex: 앞자리 3자리가 5보다 작은지)으로 만들어지기 때문에 몇자리 수를 계산하는 가와 마지막 값이 몇보다 작은지만 알면 된다. 한글자씩 가져와서, hex2Decimal로 숫자값으로 전환한 후 값을 체크하는 로직이다.




    마치며...


    소스에 대한 설명은 유튜브에도 올릴 것이고 유튜브가 포스팅보다 좀 더 자세할 것이니 이해가 잘 안되는 부분이 있으면 유튜브를 보는 것이 더 나을 것이다.



    금일(2018.12.31) 오후에 업로드 할 예정이니, genesis 블록과 마이닝을 하는 알고리즘이 궁금하다면 채널을 구독하는 것을 권장하고 싶다. 다음은 마이닝으로 블록이 생성되는 부분을 설명해보도록 하겠다.



    댓글

    Designed by JB FACTORY