1. 程式人生 > >Java搭建區塊鏈

Java搭建區塊鏈

前言

為了更好的理解區塊鏈的底層實現原理,決定自己動手模擬實現一條區塊鏈。

思路分析

通過之前的學習,從文字知識的角度,我們知道,創世區塊、記賬原理、挖礦原理、工作量證明、共識機制等等區塊鏈的相關知識。

建立一條區塊鏈,首先預設構造創世區塊。在此基礎上,我們可以釋出交易,並進行挖礦,計算出工作量證明,將交易記錄到區塊中,每成功的挖一次礦,塊高就+1。當然在此過程中,可能會出現“造假”的問題。也就是說,每一個新註冊的節點,都可以有自己的鏈。這些鏈長短不一,為了保證賬本的一致性,需要通過一種一致性共識演算法來找到最長的鏈,作為樣本,同步資料,保證每個節點上的賬本資訊都是一致的。

資料結構

  • 區塊鏈
    這裡寫圖片描述
    如圖所示,索引為1的區塊即為創始區塊。可想而知,可以用List<區塊>來表示區塊鏈。其中,區塊鏈的高度即為鏈上區塊的塊數,上圖區塊高度為4。
  • 區塊
    這裡寫圖片描述
    單個區塊的資料結構有索引、交易列表、時間戳、工作量證明、上一個區塊的hash組成。
  • 交易列表
    這裡寫圖片描述
    整個區塊鏈就是一個超級大的分散式賬本,當發生交易時,礦工們通過計算工作量證明的方法來進行挖礦(本文中挖到礦將得到1個幣的獎勵),將發生的交易記錄到賬本之中。

Web API

我們將通過Postman來模擬請求。請求API如下:

/nodes/register 註冊網路節點
/nodes/resolve 一致性共識演算法
/transactions/new 新建交易
/mine 挖礦
/chain 輸出整條鏈的資料

專案目錄結構

Gradle Web 專案
這裡寫圖片描述

dependencies {
    compile('javax:javaee-api:7.0')
    compile('org.json:json:20160810')

    testCompile('junit:junit:4.12')
}

實現程式碼

註釋寫的很詳細,如果遇到不懂的地方,歡迎大家一同討論。

  • BlockChain類 ,所有的核心程式碼都在其中。
    // 儲存區塊鏈
    private List<Map<String, Object>> chain;
    // 該例項變數用於當前的交易資訊列表
private List<Map<String, Object>> currentTransactions; // 網路中所有節點的集合 private Set<String> nodes; private static BlockChain blockChain = null; private BlockChain() { // 初始化區塊鏈以及當前的交易資訊列表 chain = new ArrayList<Map<String, Object>>(); currentTransactions = new ArrayList<Map<String, Object>>(); // 初始化儲存網路中其他節點的集合 nodes = new HashSet<String>(); // 建立創世區塊 newBlock(100, "0"); } /** * 在區塊鏈上新建一個區塊 * @param proof 新區塊的工作量證明 * @param previous_hash 上一個區塊的hash值 * @return 返回新建的區塊 */ public Map<String, Object> newBlock(long proof, String previous_hash) { Map<String, Object> block = new HashMap<String, Object>(); block.put("index", getChain().size() + 1); block.put("timestamp", System.currentTimeMillis()); block.put("transactions", getCurrentTransactions()); block.put("proof", proof); // 如果沒有傳遞上一個區塊的hash就計算出區塊鏈中最後一個區塊的hash block.put("previous_hash", previous_hash != null ? previous_hash : hash(getChain().get(getChain().size() - 1))); // 重置當前的交易資訊列表 setCurrentTransactions(new ArrayList<Map<String, Object>>()); getChain().add(block); return block; } // 建立單例物件 public static BlockChain getInstance() { if (blockChain == null) { synchronized (BlockChain.class) { if (blockChain == null) { blockChain = new BlockChain(); } } } return blockChain; } /** * @return 得到區塊鏈中的最後一個區塊 */ public Map<String, Object> lastBlock() { return getChain().get(getChain().size() - 1); } /** * 生成新交易資訊,資訊將加入到下一個待挖的區塊中 * @param sender 傳送方的地址 * @param recipient 接收方的地址 * @param amount 交易數量 * @return 返回該交易事務的塊的索引 */ public int newTransactions(String sender, String recipient, long amount) { Map<String, Object> transaction = new HashMap<String, Object>(); transaction.put("sender", sender); transaction.put("recipient", recipient); transaction.put("amount", amount); getCurrentTransactions().add(transaction); return (Integer) lastBlock().get("index") + 1; } /** * 生成區塊的 SHA-256格式的 hash值 * @param block 區塊 * @return 返回該區塊的hash */ public static Object hash(Map<String, Object> block) { return new Encrypt().Hash(new JSONObject(block).toString()); } /** * 註冊節點 * @param address 節點地址 * @throws MalformedURLException */ public void registerNode(String address) throws MalformedURLException { URL url = new URL(address); String node = url.getHost() + ":" + (url.getPort() == -1 ? url.getDefaultPort() : url.getPort()); nodes.add(node); } /** * 驗證是否為有效鏈,遍歷每個區塊驗證hash和proof,來確定一個給定的區塊鏈是否有效 * @param chain * @return */ public boolean vaildChain(List<Map<String,Object>> chain) { Map<String,Object> lastBlock = chain.get(0); int currentBlockIndex = 1; while (currentBlockIndex < lastBlock.size()) { Map<String,Object> currentBlock = chain.get(currentBlockIndex); //檢查區塊的hash是否正確 if (!currentBlock.get("previous_hash").equals(hash(lastBlock))) { return false; } lastBlock = currentBlock; currentBlockIndex ++; } return true; } /** * 使用網路中最長的鏈. 遍歷所有的鄰居節點,並用上一個方法檢查鏈的有效性, * 如果發現有效更長鏈,就替換掉自己的鏈 * @return 如果鏈被取代返回true, 否則返回false * @throws IOException */ public boolean resolveConflicts() throws IOException { //獲得當前網路上所有的鄰居節點 Set<String> neighbours = this.nodes; List<Map<String, Object>> newChain = null; // 尋找最長的區塊鏈0 long maxLength = this.chain.size(); // 獲取並驗證網路中的所有節點的區塊鏈 for (String node : neighbours) { URL url = new URL("http://" + node + "/chain"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); if (connection.getResponseCode() == 200) { BufferedReader bufferedReader = new BufferedReader( new InputStreamReader(connection.getInputStream(), "utf-8")); StringBuffer responseData = new StringBuffer(); String response = null; while ((response = bufferedReader.readLine()) != null) { responseData.append(response); } bufferedReader.close(); JSONObject jsonData = new JSONObject(responseData.toString()); long length = jsonData.getLong("blockLength"); List<Map<String, Object>> chain = (List) jsonData.getJSONArray("chain").toList(); // 檢查長度是否長,鏈是否有效 if (length > maxLength && vaildChain(chain)) { maxLength = length; newChain = chain; } } } // 如果發現一個新的有效鏈比我們的長,就替換當前的鏈 if (newChain != null) { this.chain = newChain; return true; } return false; }
  • Proof 類 ,計算工作量證明
/**
     * 計算當前區塊的工作量證明
     * @param last_proof 上一個區塊的工作量證明
     * @return
     */
    public long ProofOfWork(long last_proof){
        long proof = 0;
        while (!(vaildProof(last_proof,proof))) {
            proof ++;
        }
        return proof;
    }

    /**
     * 驗證證明,是否拼接後的Hash值以4個0開頭
     * @param last_proof 上一個區塊工作量證明
     * @param proof 當前區塊的工作量證明
     * @return
     */
    public boolean vaildProof(long last_proof, long proof) {
        String guess = last_proof + "" + proof;
        String guess_hash = new Encrypt().Hash(guess);
        boolean flag = guess_hash.startsWith("0000");
        return  flag;
    }
  • Encrypt 類 ,Hash計算工具類
public class Encrypt {
     /**
      * 傳入字串,返回 SHA-256 加密字串
      * @param strText
      * @return
      */
     public String Hash(final String strText) {
         // 返回值
         String strResult = null;
         // 是否是有效字串
         if (strText != null && strText.length() > 0) {
             try {
                 // 建立加密物件,傳入要加密型別
                 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
                 // 傳入要加密的字串
                 messageDigest.update(strText.getBytes());
                 // 執行雜湊計算,得到 byte 陣列
                 byte byteBuffer[] = messageDigest.digest();
                 // 將 byte 陣列轉換 string 型別
                 StringBuffer strHexString = new StringBuffer();
                 // 遍歷 byte 陣列
                 for (int i = 0; i < byteBuffer.length; i++) {
                     // 轉換成16進位制並存儲在字串中
                     String hex = Integer.toHexString(0xff & byteBuffer[i]);
                     if (hex.length() == 1) {
                         strHexString.append('0');
                     }
                     strHexString.append(hex);
                 }
                 // 得到返回結果
                 strResult = strHexString.toString();
             } catch (NoSuchAlgorithmException e) {
                 e.printStackTrace();
             }
         }
         return strResult;
     }
 }
  • FullChain 類,輸出整條鏈的資訊。
/**
 * @Author: cfx
 * @Description: 該Servlet用於輸出整個區塊鏈的資料(Json)
 * @Date: Created in 2018/5/9 17:24
 */
@WebServlet("/chain")
public class FullChain extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        BlockChain blockChain = BlockChain.getInstance();
        Map<String,Object> response = new HashMap<String, Object>();
        response.put("chain",blockChain.getChain());
        response.put("blockLength",blockChain.getChain().size());

        JSONObject jsonObject = new JSONObject(response);
        resp.setContentType("application/json");
        PrintWriter printWriter = resp.getWriter();
        printWriter.println(jsonObject);
        printWriter.close();
    }
}
  • InitialID 類 ,初始化時執行,隨機的uuid作為礦工的賬戶地址。
/**
 * @Author: cfx
 * @Description: 初始化時,使用UUID來作為節點ID
 * @Date: Created in 2018/5/9 17:17
 */
@WebListener
public class InitialID implements ServletContextListener {

    public void contextInitialized(ServletContextEvent sce) {
        ServletContext servletContext = sce.getServletContext();
        String uuid = UUID.randomUUID().toString().replace("-", "");
        servletContext.setAttribute("uuid", uuid);
        System.out.println("uuid is : "+servletContext.getAttribute("uuid"));
    }

    public void contextDestroyed(ServletContextEvent sce) {
    }
}
  • Register 類 ,節點註冊類,記錄網路上所有的節點,使用者共識演算法,保證所有的節點上的賬本都是一致的。
/**
 * @Author: cfx
 * @Description: 註冊網路節點
 * @Date: Created in 2018/5/10 11:26
 */
@WebServlet("/nodes/register")
public class Register extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("utf-8");
        // 讀取客戶端傳遞過來的資料並轉換成JSON格式
        BufferedReader reader = req.getReader();
        String input = null;
        StringBuffer requestBody = new StringBuffer();
        while ((input = reader.readLine()) != null) {
            requestBody.append(input);
        }
        JSONObject jsonValue = new JSONObject(requestBody.toString());
        BlockChain blockChain = BlockChain.getInstance();
        blockChain.registerNode(jsonValue.getString("nodes"));

        PrintWriter printWriter = resp.getWriter();
        printWriter.println(new JSONObject().append("message","The Nodes is : " + blockChain.getNodes()));
        printWriter.close();

    }
}
  • NewTransaction 類,新建交易類。
/**
 * @Author: cfx
 * @Description: 該Servlet用於接收並處理新的交易資訊
 * @Date: Created in 2018/5/9 17:22
 */
@WebServlet("/transactions/new")
public class NewTransaction extends HttpServlet {

    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        req.setCharacterEncoding("utf-8");
        // 讀取客戶端傳遞過來的資料並轉換成JSON格式
        BufferedReader reader = req.getReader();
        String input = null;
        StringBuffer requestBody = new StringBuffer();
        while ((input = reader.readLine()) != null) {
            requestBody.append(input);
        }
        JSONObject jsonValues = new JSONObject(requestBody.toString());

        // 檢查所需要的欄位是否位於POST的data中
        String[] required = { "sender", "recipient", "amount" };
        for (String string : required) {
            if (!jsonValues.has(string)) {
                // 如果沒有需要的欄位就返回錯誤資訊
                resp.sendError(400, "Missing values");
            }
        }

        // 新建交易資訊
        BlockChain blockChain = BlockChain.getInstance();
        int index = blockChain.newTransactions(jsonValues.getString("sender"), jsonValues.getString("recipient"),
                jsonValues.getLong("amount"));

        // 返回json格式的資料給客戶端
        resp.setContentType("application/json");
        PrintWriter printWriter = resp.getWriter();
        printWriter.println(new JSONObject().append("message", "Transaction will be added to Block " + index));
        printWriter.close();
    }
}
  • Mine , 挖礦類。
/**
 * @Author: cfx
 * @Description: 該Servlet用於執行工作演算法的證明來獲得下一個證明,也就是所謂的挖礦
 * @Date: Created in 2018/5/9 17:21
 */
@WebServlet("/mine")
public class Mine extends HttpServlet{
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        BlockChain blockChain = BlockChain.getInstance();

        //計算出工作量證明
        Map<String,Object> lastBlock = blockChain.lastBlock();
        Long last_proof = Long.parseLong(lastBlock.get("proof") + "");
        Long proof = new Proof().ProofOfWork(last_proof);

        //獎勵計算出工作量證明的礦工1個幣的獎勵,傳送者為"0"表明這是新挖出的礦。
        String uuid = (String) this.getServletContext().getAttribute("uuid");
        blockChain.newTransactions("0",uuid,1);

        //構建新的區塊
        Map<String,Object> newBlock = blockChain.newBlock(proof,null);
        Map<String, Object> response = new HashMap<String, Object>();
        response.put("message", "New Block Forged");
        response.put("index", newBlock.get("index"));
        response.put("transactions", newBlock.get("transactions"));
        response.put("proof", newBlock.get("proof"));
        response.put("previous_hash", newBlock.get("previous_hash"));

        // 返回新區塊的資料給客戶端
        resp.setContentType("application/json");
        PrintWriter printWriter = resp.getWriter();
        printWriter.println(new JSONObject(response));
        printWriter.close();
    }
}
  • Consensus 類 ,通過判斷不同節點上鍊的長度,來找出最長鏈,這就是一致性共識演算法。
/**
 * @Author: cfx
 * @Description: 一致性共識演算法,解決共識衝突,保證所有的節點都在同一條鏈上(最長鏈)
 * @Date: Created in 2018/5/10 11:38
 */
@WebServlet("/nodes/resolve")
public class Consensus extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        BlockChain blockChain = BlockChain.getInstance();
        boolean flag = blockChain.resolveConflicts();
        System.out.println("是否解決一致性共識衝突:" + flag);
    }
}

執行結果

以下是本人之前的測試記錄:

首次請求/chain:
    初始化Blockchain
    {
        "chain": [
            {
                "index": 1,
                "proof": 100,
                "transactions": [],
                "timestamp": 1526284543591,
                "previous_hash": "0"
            }
        ],
        "chainLenth": 1
    }

請求/nodes/register,進行網路節點的註冊。
request:
    {
      "nodes": "http://lcoalhost:8080"
    }
response:
    {"message":["All Nodes are:[lcoalhost:8080]"]}

請求/mine,進行挖礦。
{
    "index": 2,
    "proof": 35293,
    "message": "New Block Forged",
    "transactions": [
        {
            "amount": 1,
            "sender": "0",
            "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
        }
    ],
    "previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
}
請求/chain,檢視鏈上所有區塊的資料
{
    "chain": [
        {
            "index": 1,
            "proof": 100,
            "transactions": [],
            "timestamp": 1526284543591,
            "previous_hash": "0"
        },
        {
            "index": 2,
            "proof": 35293,
            "transactions": [
                {
                    "amount": 1,
                    "sender": "0",
                    "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
                }
            ],
            "timestamp": 1526284661678,
            "previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
        }
    ],
    "chainLenth": 2
}

請求/transactions/new,新建交易。
request: 
    {
     "sender": "d4ee26eee15148ee92c6cd394edd974e",
     "recipient": "someone-other-address",
     "amount": 6
    }
response:
    {
        "message": [
            "Transaction will be added to Block 3"
        ]
    }
請求/mine,計算出工作量證明。將上面的交易記錄到賬本之中。
{
    "index": 3,
    "proof": 35089,
    "message": "New Block Forged",
    "transactions": [
        {
            "amount": 6,
            "sender": "d4ee26eee15148ee92c6cd394edd974e",
            "recipient": "someone-other-address"
        },
        {
            "amount": 1,
            "sender": "0",
            "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
        }
    ],
    "previous_hash": "a12748a35d57a4a371cefc4a8c294236d69c762d28b889abb2ae34a31d2b7597"
}

請求/chain,檢視鏈上所有區塊的資料
{
    "chain": [
        {
            "index": 1,
            "proof": 100,
            "transactions": [],
            "timestamp": 1526284543591,
            "previous_hash": "0"
        },
        {
            "index": 2,
            "proof": 35293,
            "transactions": [
                {
                    "amount": 1,
                    "sender": "0",
                    "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
                }
            ],
            "timestamp": 1526284661678,
            "previous_hash": "c4b2bb2f6e042680aed249309791cac96da6c1f65b811c306088723ae3c73f66"
        },
        {
            "index": 3,
            "proof": 35089,
            "transactions": [
                {
                    "amount": 6,
                    "sender": "d4ee26eee15148ee92c6cd394edd974e",
                    "recipient": "someone-other-address"
                },
                {
                    "amount": 1,
                    "sender": "0",
                    "recipient": "e91467fe51bd43b8ad7892b3bc09bd4e"
                }
            ],
            "timestamp": 1526284774452,
            "previous_hash": "a12748a35d57a4a371cefc4a8c294236d69c762d28b889abb2ae34a31d2b7597"
        }
    ],
    "chainLenth": 3
}

存在的問題

有一個問題沒有解決,就是我們啟動多例項來模擬不同的網路節點時,並不能解決節點加入同一個Set的問題,也就是說根本無法通過節點本身來獲得其他網路節點,進而判斷最長鏈。所以/nodes/resolve請求暫時時無用的。期間也有想方法解決,比如通過所謂的“第三方”–資料庫,當一個節點註冊時,儲存到資料庫中;當第二個節點加入時,也加入到資料庫中…當需要請求解決一致性演算法時,去資料庫中讀取節點資訊遍歷即可。但是,自己沒有去實現。這是我的想法,畢竟是兩個不相干的例項。如果有朋友有其他的解決方案,請一定要告訴我!謝謝。

總結

通過簡單的Demo實現區塊鏈,當然其中簡化了大量的實現細節,所以說其實並沒有多少實際參考價值。但是意義在於,能幫助我們更容易的理解區塊鏈,為之後的學習打下夯實的基礎。

專案原始碼

參考文章