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

項目源碼

Java從零開始創建區塊鏈Demo

參考文章

https://learnblockchain.cn/2017/11/04/bitcoin-pow/
http://blog.51cto.com/zero01/2086195等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章