區塊鏈編程語言(使用Java語言從零開始創建區塊鏈)
目前網絡上關于區塊鏈入門、科普鏈文章不少,本文就不再贅述區塊鏈的基本概念了,如果對區塊鏈不是很了解的話,可以看一下我之前收集的一些入門學習資源:
https://blog.51cto.com/zero01/2066321
對區塊鏈技術感到新奇的我們,都想知道區塊鏈在代碼上是怎么實現的,所以本文是實戰向的,畢竟理論我們都看了不少,但是對于區塊鏈具體的實現還不是很清楚,本文就使用Java語言來實現一個簡單的區塊鏈。
但是要完全搞懂區塊鏈并非易事,對于一門較為陌生的技術,我們需要在理論+實踐中學習,通過寫代碼來學習技術會掌握得更牢固,構建一個區塊鏈可以加深對區塊鏈的理解。
準備工作
掌握基本的JavaSE以及JavaWeb開發,能夠使用Java開發簡單的項目,并且需要了解HTTP協議。
我們知道區塊鏈是由區塊的記錄構成的不可變、有序的鏈結構,記錄可以是交易、文件或任何你想要的數據,重要的是它們是通過哈希值(hashes)鏈接起來的。
如果你還不是很了解哈希是什么,可以查看這篇文章
環境描述
JDK1.8
Tomcat 9.0
Maven 3.5
JSON 20160810
javaee-api 7.0
pom.xml文件配置內容:
<dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20160810</version> </dependency> </dependencies>
然后還需要一個HTTP客戶端,比如Postman,Linux命令行下的curl或其它客戶端,我這里使用的是Postman。
Blockchain類首先創建一個Blockchain類,在構造器中創建了兩個主要的集合,一個用于儲存區塊鏈,一個用于儲存交易列表,本文中所有核心的主要代碼都寫在這個類里,方便隨時查看,在實際開發則不宜這么做,應該把代碼拆分仔細降低耦合度。
以下是Blockchain類的框架代碼:
package org.zero01.core;import java.util.ArrayList;import java.util.HashMap;import java.util.List;public class BlockChain { // 存儲區塊鏈 private List<Object> chain; // 該實例變量用于當前的交易信息列表 private List<Object> currentTransactions; public BlockChain() { // 初始化區塊鏈以及當前的交易信息列表 this.chain = new ArrayList<Object>(); this.currentTransactions= new ArrayList<Object>(); } public List<Object> getChain() { return chain; } public void setChain(List<Object> chain) { this.chain = chain; } public List<Object> getCurrentTransactions() { return currentTransactions; } public void setCurrentTransactions(List<Object> currentTransactions) { this.currentTransactions = currentTransactions; } public Object lastBlock() { return null; } public HashMap<String, Object> newBlock() { return null; } public int newTransactions() { return 0; } public static Object hash(HashMap<String, Object> block) { return null; }}
Blockchain類用來管理區塊鏈,它能存儲交易,加入新塊等,下面我們來進一步完善這些方法。
區塊的結構
首先需要說明一下區塊的結構,每個區塊包含屬性:索引(index),時間戳(timestamp),交易列表(transactions),工作量證明(稍后解釋)以及前一個區塊的Hash值。
以下是一個區塊的結構:
block = { 'index': 1, 'timestamp': 1506057125.900785, 'transactions': [ { 'sender': "8527147fe1f5426f9dd545de4b27ee00", 'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f", 'amount': 5, } ], 'proof': 324984774000, 'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"}
到這里,區塊鏈的概念就清楚了,每個新的區塊都包含上一個區塊的Hash,這是關鍵的一點,它保障了區塊鏈不可變性。如果***者破壞了前面的某個區塊,那么后面所有區塊的Hash都會變得不正確。不理解的話,慢慢消化,可以參考區塊鏈記賬原理。
由于需要計算區塊的hash,所以我們得先編寫一個用于計算hash值的工具類:
package org.zero01.util;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public class Encrypt { /** * 傳入字符串,返回 SHA-256 加密字符串 * * @param strText * @return */ public String getSHA256(final String strText) { return SHA(strText, "SHA-256"); } /** * 傳入字符串,返回 SHA-512 加密字符串 * * @param strText * @return */ public String getSHA512(final String strText) { return SHA(strText, "SHA-512"); } /** * 傳入字符串,返回 MD5 加密字符串 * * @param strText * @return */ public String getMD5(final String strText) { return SHA(strText, "SHA-512"); } /** * 字符串 SHA 加密 * * @param strSourceText * @return */ private String SHA(final String strText, final String strType) { // 返回值 String strResult = null; // 是否是有效字符串 if (strText != null && strText.length() > 0) { try { // SHA 加密開始 // 創建加密對象,傳入加密類型 MessageDigest messageDigest = MessageDigest.getInstance(strType); // 傳入要加密的字符串 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; }}
加入交易功能
接下來我們需要實現一個交易\記賬功能,所以來完善newTransactions以及lastBlock方法:
/** * @return 得到區塊鏈中的最后一個區塊 */ public HashMap<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; }
newTransactions方法向列表中添加一個交易記錄,并返回該記錄將被添加到的區塊 (下一個待挖掘的區塊)的索引,等下載用戶提交交易時會有用。
創建新塊
當Blockchain實例化后,我們需要構造一個創世區塊(沒有前區塊的第一個區塊),并且給它加上一個工作量證明。每個區塊都需要經過工作量證明,俗稱挖礦,稍后會繼續講解。
為了構造創世塊,我們還需要完善剩下的幾個方法,并且把該類設計為單例:
package org.zero01.dao;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import java.util.Map;import org.json.JSONObject;import org.zero01.util.Encrypt;public class BlockChain { // 存儲區塊鏈 private List<Map<String, Object>> chain; // 該實例變量用于當前的交易信息列表 private List<Map<String, Object>> currentTransactions; private static BlockChain blockChain = null; private BlockChain() { // 初始化區塊鏈以及當前的交易信息列表 chain = new ArrayList<Map<String, Object>>(); currentTransactions = new ArrayList<Map<String, Object>>(); // 創建創世區塊 newBlock(100, "0"); } // 創建單例對象 public static BlockChain getInstance() { if (blockChain == null) { synchronized (BlockChain.class) { if (blockChain == null) { blockChain = new BlockChain(); } } } return blockChain; } public List<Map<String, Object>> getChain() { return chain; } public void setChain(List<Map<String, Object>> chain) { this.chain = chain; } public List<Map<String, Object>> getCurrentTransactions() { return currentTransactions; } public void setCurrentTransactions(List<Map<String, Object>> currentTransactions) { this.currentTransactions = currentTransactions; } /** * @return 得到區塊鏈中的最后一個區塊 */ public Map<String, Object> lastBlock() { return getChain().get(getChain().size() - 1); } /** * 在區塊鏈上新建一個區塊 * * @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; } /** * 生成新交易信息,信息將加入到下一個待挖的區塊中 * * @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().getSHA256(new JSONObject(block).toString()); }}
通過上面的代碼和注釋可以對區塊鏈有直觀的了解,接下來我們來編寫一些簡單的測試代碼來測試一下這些代碼能否正常工作:
package org.zero01.test;import java.util.HashMap;import java.util.Map;import org.json.JSONObject;import org.zero01.dao.BlockChain;public class Test { public static void main(String[] args) throws Exception { BlockChain blockChain = BlockChain.getInstance(); // 一個區塊中可以不包含任何交易記錄 Map<String, Object> block = blockChain.newBlock(300, null); System.out.println(new JSONObject(block)); // 一個區塊中可以包含一筆交易記錄 blockChain.newTransactions("123", "222", 33); Map<String, Object> block1 = blockChain.newBlock(500, null); System.out.println(new JSONObject(block1)); // 一個區塊中可以包含多筆交易記錄 blockChain.newTransactions("321", "555", 133); blockChain.newTransactions("000", "111", 10); blockChain.newTransactions("789", "369", 65); Map<String, Object> block2 = blockChain.newBlock(600, null); System.out.println(new JSONObject(block2)); // 查看整個區塊鏈 Map<String, Object> chain = new HashMap<String, Object>(); chain.put("chain", blockChain.getChain()); chain.put("length", blockChain.getChain().size()); System.out.println(new JSONObject(chain)); }}
運行結果:
// 挖出來的新區塊{ "index": 2, "transactions": [], "proof": 300, "timestamp": 1519478559703, "previous_hash": "185b62ca1fc31285bce8878acfc970983cb561f19c63b65120d2c95148cf151f"}// 包含一筆交易的區塊{ "index": 3, "transactions": [ { "amount": 33, "sender": "123", "recipient": "222" } ], "proof": 500, "timestamp": 1519478559728, "previous_hash": "bce15693c0a028b1fc6d7d1c1d30494f97ef37b8b3384865559ceed9b5ff798b"}// 包含多筆交易的區塊{ "index": 4, "transactions": [ { "amount": 133, "sender": "321", "recipient": "555" }, { "amount": 10, "sender": "000", "recipient": "111" }, { "amount": 65, "sender": "789", "recipient": "369" } ], "proof": 600, "timestamp": 1519478656178, "previous_hash": "b0edde645f76fc3a6cb45b7c91b07b686e8e214cfc1dea4823bf38bda37c909c"}// 整個區塊鏈,第一個是創始區塊{ "chain": [ { "index": 1, "transactions": [], "proof": 100, "timestamp": 1519478656153, "previous_hash": "0" }, { "index": 2, "transactions": [], "proof": 300, "timestamp": 1519478656154, "previous_hash": "7925a01fa8cb67b51ea89b9cfcfa16c5febee008bb559f94c5758418e7acc670" }, { "index": 3, "transactions": [ { "amount": 33, "sender": "123", "recipient": "222" } ], "proof": 500, "timestamp": 1519478656178, "previous_hash": "40ccc2f4ad97f75cb611ed69a4ecc7438eefd31afca17ca00c2ed7b5163d0831" }, { "index": 4, "transactions": [ { "amount": 133, "sender": "321", "recipient": "555" }, { "amount": 10, "sender": "000", "recipient": "111" }, { "amount": 65, "sender": "789", "recipient": "369" } ], "proof": 600, "timestamp": 1519478656178, "previous_hash": "b0edde645f76fc3a6cb45b7c91b07b686e8e214cfc1dea4823bf38bda37c909c" } ], "length": 4}
通過以上的測試,可以很直觀的看到區塊鏈的數據,但是現在只是完成了初步的代碼編寫,還有幾件事情還沒做,接下來我們看看區塊是怎么挖出來的。
理解工作量證明新的區塊依賴工作量證明算法(PoW)來構造。PoW的目標是找出一個符合特定條件的數字,這個數字很難計算出來,但容易驗證。這就是工作量證明的核心思想。
為了方便理解,舉個例子:
假設一個整數 x 乘以另一個整數 y 的積的 Hash 值必須以 0 結尾,即 hash(x * y) = ac23dc…0。設變量 x = 5,求 y 的值?
用Java實現如下:
package org.zero01.test;import org.zero01.util.Encrypt;public class TestProof { public static void main(String[] args) { int x = 5; int y = 0; while (!new Encrypt().getSHA256((x * y) + "").endsWith("0")) { y++; } System.out.println("y=" + y); }}
結果是 y=21 ,因為:
hash(5 * 21) = 1253e9373e...5e3600155e860
在比特幣中,使用稱為Hashcash的工作量證明算法,它和上面的問題很類似。礦工們為了爭奪創建區塊的權利而爭相計算結果。通常,計算難度與目標字符串需要滿足的特定字符的數量成正比,礦工算出結果后,會獲得比特幣獎勵。當然,在網絡上非常容易驗證這個結果。
實現工作量證明讓我們來實現一個相似PoW算法,規則是:尋找一個數 p,使得它與前一個區塊的 proof 拼接成的字符串的 Hash 值以 4 個零開頭:
... /** * 簡單的工作量證明: * - 查找一個 p' 使得 hash(pp') 以4個0開頭 * - p 是上一個塊的證明, p' 是當前的證明 * * @param last_proof * 上一個塊的證明 * @return */ public long proofOfWork(long last_proof) { long proof = 0; while (!validProof(last_proof, proof)) { proof += 1; } return proof; } /** * 驗證證明: 是否hash(last_proof, proof)以4個0開頭? * * @param last_proof * 上一個塊的證明 * @param proof * 當前的證明 * @return 以4個0開頭返回true,否則返回false */ public boolean validProof(long last_proof, long proof) { String guess = last_proof + "" + proof; String guess_hash = new Encrypt().getSHA256(guess); return guess_hash.startsWith("0000"); }
衡量算法復雜度的辦法是修改零開頭的個數。使用4個來用于演示,你會發現多一個零都會大大增加計算出結果所需的時間。
現在Blockchain類基本已經完成了,接下來使用Servlet接收HTTP請求來進行交互。
Blockchain作為API接口我們將使用Java Web中的Servlet來接收用戶的HTTP請求,通過Servlet我們可以方便的將網絡請求的數據映射到相應的方法上進行處理,現在我們來讓Blockchain運行在基于Java Web上。
我們將創建三個接口:
/transactions/new 創建一個交易并添加到區塊
/mine 告訴服務器去挖掘新的區塊
/chain 返回整個區塊鏈
注冊節點ID我們的“Tomcat服務器”將扮演區塊鏈網絡中的一個節點,而每個節點都需要有一個唯一的標識符,也就是id。在這里我們使用UUID來作為節點ID,我們需要在服務器啟動時,將UUID設置到ServletContext屬性中,這樣我們的服務器就擁有了唯一標識,這一步我們可以配置監聽類來完成,首先配置web.xml文件內容如下:
<?xml version="1.0" encoding="UTF-8"?><web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"> <listener> <listener-class>org.zero01.servlet.InitialID</listener-class> </listener></web-app>
然后編寫一個類實現ServletContextListener接口,在初始化方法中把uuid設置到ServletContext的屬性中:
package org.zero01.servlet;import java.util.UUID;import javax.servlet.ServletContext;import javax.servlet.ServletContextEvent;import javax.servlet.ServletContextListener;public class InitialID implements ServletContextListener { public void contextInitialized(ServletContextEvent sce) { ServletContext servletContext = sce.getServletContext(); String uuid = UUID.randomUUID().toString().replace("-", ""); servletContext.setAttribute("uuid", uuid); } public void contextDestroyed(ServletContextEvent sce) { }}
創建Servlet類我們這里沒有使用任何框架,所以我們需要通過最基本的Servlet來接收并處理用戶的HTTP請求:
package org.zero01.servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;// 該Servlet用于運行工作算法的證明來獲得下一個證明,也就是所謂的挖礦@WebServlet("/mine")public class Mine extends HttpServlet{ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { }}package org.zero01.servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;// 該Servlet用于接收并處理新的交易信息@WebServlet("/transactions/new")public class NewTransaction extends HttpServlet{ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { }}package org.zero01.servlet;import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;// 該Servlet用于輸出整個區塊鏈的數據@WebServlet("/chain")public class FullChain extends HttpServlet{ protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { }}
我們先來完成最簡單的FullChain的代碼,這個Servlet用于向客戶端輸出整個區塊鏈的數據(JSON格式):
package org.zero01.servlet;import java.io.IOException;import java.io.PrintWriter;import java.util.HashMap;import java.util.Map;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.json.JSONObject;import org.zero01.core.BlockChain;// 該Servlet用于輸出整個區塊鏈的數據@WebServlet("/chain")public class FullChain extends HttpServlet { 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("length", blockChain.getChain().size()); JSONObject jsonResponse = new JSONObject(response); resp.setContentType("application/json"); PrintWriter printWriter = resp.getWriter(); printWriter.println(jsonResponse); printWriter.close(); }}
發送交易然后是記錄交易數據的功能,每一個區塊都可以記錄交易數據,發送到節點的交易數據結構如下:
{ "sender": "my address", "recipient": "someone else's address", "amount": 5}
實現代碼如下:
package org.zero01.servlet;import java.io.BufferedReader;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.json.JSONObject;import org.zero01.core.BlockChain;// 該Servlet用于接收并處理新的交易信息@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(); }}
挖礦挖礦正是神奇所在,它很簡單,只做了以下三件事:
計算工作量證明PoW
通過新增一個交易授予礦工(自己)一個幣
構造新區塊并將其添加到鏈中
代碼實現如下:
package org.zero01.servlet;import java.io.IOException;import java.io.PrintWriter;import java.util.HashMap;import java.util.Map;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.json.JSONObject;import org.zero01.core.BlockChain;//該Servlet用于運行工作算法的證明來獲得下一個證明,也就是所謂的挖礦@WebServlet("/mine")public class Mine extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { BlockChain blockChain = BlockChain.getInstance(); Map<String, Object> lastBlock = blockChain.lastBlock(); long lastProof = Long.parseLong(lastBlock.get("proof") + ""); long proof = blockChain.proofOfWork(lastProof); // 給工作量證明的節點提供獎勵,發送者為 "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(); }}
注意交易的接收者是我們自己的服務器節點,我們做的大部分工作都只是圍繞Blockchain類的方法進行交互。到此,我們的區塊鏈就算完成了,我們來實際運行下。
運行區塊鏈由于我們這里也沒有寫前端的web頁面,只寫了后端的API,所以只能使用 Postman 之類的軟件去和API進行交互。首先啟動Tomcat服務器,然后通過post請求 http://localhost:8089/BlockChain_Java/transactions/new 來添加新的交易信息(注意我這里沒有使用默認的8080端口,默認的情況下是8080端口):
但是這時候還沒有新的區塊可以寫入這個交易信息,所以我們還需要請求 http://localhost:8089/BlockChain_Java/mine 來進行挖礦,挖出一個新的區塊來存儲這筆交易:
在挖了兩次礦之后,就有3個塊了,通過請求 http://localhost:8089/BlockChain_Java/chain 可以得到所有的區塊塊的信息:
{ "chain": [ { "index": 1, "proof": 100, "transactions": [], "timestamp": 1520928588165, "previous_hash": "0" }, { "index": 2, "proof": 35293, "transactions": [ { "amount": 6, "sender": "d4ee26eee15148ee92c6cd394edd974e", "recipient": "someone-other-address" }, { "amount": 1, "sender": "0", "recipient": "050bbfe4ad644d008545ff490387a889" } ], "timestamp": 1520928734580, "previous_hash": "e5cf7ba38f7f0c3a93fcca5d57b624c8fd255093af4abe3c6999be61bdb81040" }, { "index": 3, "proof": 35089, "transactions": [ { "amount": 1, "sender": "0", "recipient": "050bbfe4ad644d008545ff490387a889" } ], "timestamp": 1520928870963, "previous_hash": "aa64ab003d15d50a43bd59deb88c939ea43349d00d0b653abd83b42e8fa4417c" } ], "length": 3}
一致性(共識)我們已經有了一個基本的區塊鏈可以接受交易和挖礦。但是區塊鏈系統應該是分布式的。既然是分布式的,那么我們究竟拿什么保證所有節點有同樣的鏈呢?這就是一致性問題,我們要想在網絡上有多個節點,就必須實現一個一致性的算法。
注冊節點在實現一致性算法之前,我們需要找到一種方式讓一個節點知道它相鄰的節點。每個節點都需要保存一份包含網絡中其它節點的記錄。因此讓我們新增幾個接口:
/nodes/register 接收URL形式的新節點列表
/nodes/resolve執行一致性算法,解決任何沖突,確保節點擁有正確的鏈
我們需要修改下BlockChain的構造函數并提供一個注冊節點方法:
package org.zero01.core;...import java.net.URL;... private Set<String> nodes; private BlockChain() { ... // 用于存儲網絡中其他節點的集合 nodes = new HashSet<String>(); ... } public Set<String> getNodes() { return nodes; } /** * 注冊節點 * * @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); } ...
我們用 HashSet 集合來儲存節點,這是一種避免出現重復添加節點的簡單方法。
實現共識算法前面提到,沖突是指不同的節點擁有不同的鏈,為了解決這個問題,規定最長的、有效的鏈才是最終的鏈,換句話說,網絡中有效最長鏈才是實際的鏈。
我們使用以下算法,來達到網絡中的共識:
...import java.net.HttpURLConnection;import java.net.MalformedURLException;import java.net.URL;...public class BlockChain { ... /** * 檢查是否是有效鏈,遍歷每個區塊驗證hash和proof,來確定一個給定的區塊鏈是否有效 * * @param chain * @return */ public boolean validChain(List<Map<String, Object>> chain) { Map<String, Object> lastBlock = chain.get(0); int currentIndex = 1; while (currentIndex < chain.size()) { Map<String, Object> block = chain.get(currentIndex); System.out.println(lastBlock.toString()); System.out.println(block.toString()); System.out.println("\n-------------------------\n"); // 檢查block的hash是否正確 if (!block.get("previous_hash").equals(hash(lastBlock))) { return false; } lastBlock = block; currentIndex++; } return true; } /** * 共識算法解決沖突,使用網絡中最長的鏈. 遍歷所有的鄰居節點,并用上一個方法檢查鏈的有效性, 如果發現有效更長鏈,就替換掉自己的鏈 * * @return 如果鏈被取代返回true, 否則返回false * @throws IOException */ public boolean resolveConflicts() throws IOException { Set<String> neighbours = this.nodes; List<Map<String, Object>> newChain = null; // 尋找最長的區塊鏈 long maxLength = this.chain.size(); // 獲取并驗證網絡中的所有節點的區塊鏈 for (String node : neighbours) { URL url = new URL("http://" + node + "/BlockChain_Java/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(bufferedReader.toString()); long length = jsonData.getLong("length"); List<Map<String, Object>> chain = (List) jsonData.getJSONArray("chain").toList(); // 檢查長度是否長,鏈是否有效 if (length > maxLength && validChain(chain)) { maxLength = length; newChain = chain; } } } // 如果發現一個新的有效鏈比我們的長,就替換當前的鏈 if (newChain != null) { this.chain = newChain; return true; } return false; } ...
第一個方法 validChain() 用來檢查是否是有效鏈,遍歷每個塊驗證hash和proof.
第2個方法 resolveConflicts() 用來解決沖突,遍歷所有的鄰居節點,并用上一個方法檢查鏈的有效性, 如果發現有效更長鏈,就替換掉自己的鏈
讓我們添加兩個Servlet,一個用來注冊節點,一個用來解決沖突:
注冊節點:
package org.zero01.servlet;import java.io.BufferedReader;import java.io.IOException;import java.io.PrintWriter;import java.util.HashMap;import java.util.List;import java.util.Map;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.json.JSONObject;import org.zero01.core.BlockChain;// 用于注冊節點的Servlet@WebServlet("/nodes/register")public class NodesRegister 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()); // 獲得節點集合數據,并進行判空 List<String> nodes = (List) jsonValues.getJSONArray("nodes").toList(); if (nodes == null) { resp.sendError(400, "Error: Please supply a valid list of nodes"); } // 注冊節點 BlockChain blockChain = BlockChain.getInstance(); for (String address : nodes) { blockChain.registerNode(address); } // 向客戶端返回處理結果 Map<String, Object> response = new HashMap<String, Object>(); response.put("message", "New nodes have been added"); response.put("total_nodes", blockChain.getNodes().toArray()); resp.setContentType("application/json"); PrintWriter printWriter = resp.getWriter(); printWriter.println(new JSONObject(response)); printWriter.close(); }}
解決沖突:
package org.zero01.servlet;import java.io.IOException;import java.io.PrintWriter;import java.util.HashMap;import java.util.Map;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.json.JSONObject;import org.zero01.core.BlockChain;// 用于解決沖突@WebServlet("/nodes/resolve")public class NodesResolve extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { BlockChain blockChain = BlockChain.getInstance(); boolean replaced = blockChain.resolveConflicts(); Map<String, Object> response = new HashMap<String, Object>(); if (replaced) { response.put("message", "Our chain was replaced"); response.put("new_chain", blockChain.getChain()); } else { response.put("message", "Our chain is authoritative"); response.put("chain", blockChain.getChain()); } resp.setContentType("application/json"); PrintWriter printWriter = resp.getWriter(); printWriter.println(new JSONObject(response)); printWriter.close(); }}
我們可以在不同的機器運行節點,或在一臺機器開啟不同的網絡端口來模擬多節點的網絡,這里在同一臺機器開啟不同的端口演示,配置兩個不同端口的服務器即可,我這里啟動了兩個節點:http://localhost:8089 和 http://localhost:8066。
兩個節點互相進行注冊:
然后在8066節點上挖兩個塊,確保是更長的鏈:
接著在8089節點上訪問接口/nodes/resolve ,這時8089節點的鏈會通過共識算法被8066節點的鏈取代:
通過共識算法保持一致性后,兩個節點的區塊鏈數據就都是一致的了:
到此為止我們就完成了一個區塊鏈的開發,雖然這只是一個最基本的區塊鏈,而且在開發的過程中也沒有考慮太多的程序設計方面的問題,而是以最基本、原始的方式進行開發的。但是我們不妨以這個簡單的區塊鏈為基礎,發揮自己的能力動手去重構、擴展、完善這個區塊鏈程序,直至成為自己的一個小項目。
原文鏈接:https://blog.51cto.com/zero01/2086195