教你利用Java語言創(chuàng)建區(qū)塊鏈

對區(qū)塊鏈技術感到新奇的我們爵憎,都想知道區(qū)塊鏈在代碼上是怎么實現的鸡岗,所以本文是實戰(zhàn)向的,畢竟理論我們都看了不少,但是對于區(qū)塊鏈具體的實現還不是很清楚歼跟,本文就使用Java語言來實現一個簡單的區(qū)塊鏈。

但是要完全搞懂區(qū)塊鏈并非易事格遭,對于一門較為陌生的技術哈街,我們需要在理論+實踐中學習,通過寫代碼來學習技術會掌握得更牢固拒迅,構建一個區(qū)塊鏈可以加深對區(qū)塊鏈的理解骚秦。

準備工作

掌握基本的JavaSE以及JavaWeb開發(fā),能夠使用Java開發(fā)簡單的項目璧微,并且需要了解HTTP協議作箍。

我們知道區(qū)塊鏈是由區(qū)塊的記錄構成的不可變、有序的鏈結構前硫,記錄可以是交易胞得、文件或任何你想要的數據,重要的是它們是通過哈希值(hashes)鏈接起來的屹电。

如果你還不是很了解哈希是什么阶剑,可以查看這篇文章

環(huán)境描述

  • 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類

首先創(chuàng)建一個Blockchain類外莲,在構造器中創(chuàng)建了兩個主要的集合猪半,一個用于儲存區(qū)塊鏈,一個用于儲存交易列表偷线,本文中所有核心的主要代碼都寫在這個類里磨确,方便隨時查看,在實際開發(fā)則不宜這么做淋昭,應該把代碼拆分仔細降低耦合度俐填。

以下是Blockchain類的框架代碼:

package org.zero01.core;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class BlockChain {
    // 存儲區(qū)塊鏈
    private List<Object> chain;
    // 該實例變量用于當前的交易信息列表
    private List<Object> currentTransactions;
    public BlockChain() {
        // 初始化區(qū)塊鏈以及當前的交易信息列表
        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類用來管理區(qū)塊鏈,它能存儲交易翔忽,加入新塊等英融,下面我們來進一步完善這些方法。

區(qū)塊的結構

首先需要說明一下區(qū)塊的結構歇式,每個區(qū)塊包含屬性:索引(index)驶悟,時間戳(timestamp),交易列表(transactions)材失,工作量證明(稍后解釋)以及前一個區(qū)塊的Hash值痕鳍。

以下是一個區(qū)塊的結構:

block = {
    'index': 1,
    'timestamp': 1506057125.900785,
    'transactions': [
        {
            'sender': "8527147fe1f5426f9dd545de4b27ee00",
            'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
            'amount': 5,
        }
    ],
    'proof': 324984774000,
    'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}

到這里,區(qū)塊鏈的概念就清楚了,每個新的區(qū)塊都包含上一個區(qū)塊的Hash笼呆,這是關鍵的一點熊响,它保障了區(qū)塊鏈不可變性。如果***者破壞了前面的某個區(qū)塊诗赌,那么后面所有區(qū)塊的Hash都會變得不正確汗茄。不理解的話,慢慢消化铭若,可以參考區(qū)塊鏈記賬原理洪碳。

由于需要計算區(qū)塊的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 加密開始
                // 創(chuàng)建加密對象镜雨,傳入加密類型
                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以及l(fā)astBlock方法:

   /**
     * @return 得到區(qū)塊鏈中的最后一個區(qū)塊
     */
    public HashMap<String, Object> lastBlock() {
        return getChain().get(getChain().size() - 1);
    }
    /**
     * 生成新交易信息,信息將加入到下一個待挖的區(qū)塊中
     * 
     * @param sender
     *            發(fā)送方的地址
     * @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方法向列表中添加一個交易記錄冷离,并返回該記錄將被添加到的區(qū)塊 (下一個待挖掘的區(qū)塊)的索引吵冒,等下在用戶提交交易時會有用。

創(chuàng)建新塊

當Blockchain實例化后西剥,我們需要構造一個創(chuàng)世區(qū)塊(沒有前區(qū)塊的第一個區(qū)塊)痹栖,并且給它加上一個工作量證明。

每個區(qū)塊都需要經過工作量證明瞭空,俗稱挖礦揪阿,稍后會繼續(xù)講解。

為了構造創(chuàng)世塊咆畏,我們還需要完善剩下的幾個方法南捂,并且把該類設計為單例:

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 {
    // 存儲區(qū)塊鏈
    private List<Map<String, Object>> chain;
    // 該實例變量用于當前的交易信息列表
    private List<Map<String, Object>> currentTransactions;
    private static BlockChain blockChain = null;
    private BlockChain() {
        // 初始化區(qū)塊鏈以及當前的交易信息列表
        chain = new ArrayList<Map<String, Object>>();
        currentTransactions = new ArrayList<Map<String, Object>>();
        // 創(chuàng)建創(chuàng)世區(qū)塊
        newBlock(100, "0");
    }
    // 創(chuàng)建單例對象
    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 得到區(qū)塊鏈中的最后一個區(qū)塊
     */
    public Map<String, Object> lastBlock() {
        return getChain().get(getChain().size() - 1);
    }
    /**
     * 在區(qū)塊鏈上新建一個區(qū)塊
     * 
     * @param proof
     *            新區(qū)塊的工作量證明
     * @param previous_hash
     *            上一個區(qū)塊的hash值
     * @return 返回新建的區(qū)塊
     */
    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);
        // 如果沒有傳遞上一個區(qū)塊的hash就計算出區(qū)塊鏈中最后一個區(qū)塊的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;
    }
    /**
     * 生成新交易信息,信息將加入到下一個待挖的區(qū)塊中
     * 
     * @param sender
     *            發(fā)送方的地址
     * @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;
    }
    /**
     * 生成區(qū)塊的 SHA-256格式的 hash值
     * 
     * @param block
     *            區(qū)塊
     * @return 返回該區(qū)塊的hash
     */
    public static Object hash(Map<String, Object> block) {
        return new Encrypt().getSHA256(new JSONObject(block).toString());
    }
}

通過上面的代碼和注釋可以對區(qū)塊鏈有直觀的了解旧找,接下來我們來編寫一些簡單的測試代碼來測試一下這些代碼能否正常工作:

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();
        // 一個區(qū)塊中可以不包含任何交易記錄
        Map<String, Object> block = blockChain.newBlock(300, null);
        System.out.println(new JSONObject(block));
        // 一個區(qū)塊中可以包含一筆交易記錄
        blockChain.newTransactions("123", "222", 33);
        Map<String, Object> block1 = blockChain.newBlock(500, null);
        System.out.println(new JSONObject(block1));
        // 一個區(qū)塊中可以包含多筆交易記錄
        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));
        // 查看整個區(qū)塊鏈
        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));
    }
}

運行結果:

// 挖出來的新區(qū)塊
{
    "index": 2,
    "transactions": [],
    "proof": 300,
    "timestamp": 1519478559703,
    "previous_hash": "185b62ca1fc31285bce8878acfc970983cb561f19c63b65120d2c95148cf151f"
}
// 包含一筆交易的區(qū)塊
{
    "index": 3,
    "transactions": [
        {
            "amount": 33,
            "sender": "123",
            "recipient": "222"
        }
    ],
    "proof": 500,
    "timestamp": 1519478559728,
    "previous_hash": "bce15693c0a028b1fc6d7d1c1d30494f97ef37b8b3384865559ceed9b5ff798b"
}
// 包含多筆交易的區(qū)塊
{
    "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"
}
// 整個區(qū)塊鏈币狠,第一個是創(chuàng)始區(qū)塊
{
    "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
}

通過以上的測試阻肩,可以很直觀的看到區(qū)塊鏈的數據蝴蜓,但是現在只是完成了初步的代碼編寫受神,還有幾件事情還沒做,接下來我們看看區(qū)塊是怎么挖出來的魏颓。

理解工作量證明

新的區(qū)塊依賴工作量證明算法(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的工作量證明算法箩溃,它和上面的問題很類似。礦工們?yōu)榱藸帄Z創(chuàng)建區(qū)塊的權利而爭相計算結果碌嘀。通常涣旨,計算難度與目標字符串需要滿足的特定字符的數量成正比,礦工算出結果后股冗,會獲得比特幣獎勵霹陡。

當然,在網絡上非常容易驗證這個結果止状。

實現工作量證明

讓我們來實現一個相似PoW算法烹棉,規(guī)則是:尋找一個數 p,使得它與前一個區(qū)塊的 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個來用于演示,你會發(fā)現多一個零都會大大增加計算出結果所需的時間集峦。

現在Blockchain類基本已經完成了伏社,接下來使用Servlet接收HTTP請求來進行交互。

Blockchain作為API接口

我們將使用Java Web中的Servlet來接收用戶的HTTP請求塔淤,通過Servlet我們可以方便的將網絡請求的數據映射到相應的方法上進行處理摘昌,現在我們來讓Blockchain運行在基于Java Web上。

我們將創(chuàng)建三個接口:

  • /transactions/new 創(chuàng)建一個交易并添加到區(qū)塊
  • /mine 告訴服務器去挖掘新的區(qū)塊
  • /chain 返回整個區(qū)塊鏈

注冊節(jié)點ID

我們的“Tomcat服務器”將扮演區(qū)塊鏈網絡中的一個節(jié)點高蜂,而每個節(jié)點都需要有一個唯一的標識符聪黎,也就是id。在這里我們使用UUID來作為節(jié)點ID备恤,我們需要在服務器啟動時稿饰,將UUID設置到ServletContext屬性中,這樣我們的服務器就擁有了唯一標識烘跺,這一步我們可以配置監(jiān)聽類來完成湘纵,首先配置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) {
    }
}

創(chuàng)建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用于輸出整個區(qū)塊鏈的數據
@WebServlet("/chain")
public class FullChain extends HttpServlet{
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }
}

我們先來完成最簡單的FullChain的代碼,這個Servlet用于向客戶端輸出整個區(qū)塊鏈的數據(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用于輸出整個區(qū)塊鏈的數據
@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();
    }
}

發(fā)送交易

然后是記錄交易數據的功能,每一個區(qū)塊都可以記錄交易數據铺敌,發(fā)送到節(jié)點的交易數據結構如下:

{
 "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
  • 通過新增一個交易授予礦工(自己)一個幣
  • 構造新區(qū)塊并將其添加到鏈中

代碼實現如下:

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);
        // 給工作量證明的節(jié)點提供獎勵产弹,發(fā)送者為 "0" 表明是新挖出的幣
        String uuid = (String) this.getServletContext().getAttribute("uuid");
        blockChain.newTransactions("0", uuid, 1);
        // 構建新的區(qū)塊
        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"));
        // 返回新區(qū)塊的數據給客戶端
        resp.setContentType("application/json");
        PrintWriter printWriter = resp.getWriter();
        printWriter.println(new JSONObject(response));
        printWriter.close();
    }
}

注意交易的接收者是我們自己的服務器節(jié)點,我們做的大部分工作都只是圍繞Blockchain類的方法進行交互弯囊。到此痰哨,我們的區(qū)塊鏈就算完成了,我們來實際運行下匾嘱。

運行區(qū)塊鏈

由于我們這里也沒有寫前端的web頁面斤斧,只寫了后端的API,所以只能使用 Postman 之類的軟件去和API進行交互霎烙。首先啟動Tomcat服務器撬讽,然后通過post請求 http://localhost:8089/BlockChain_Java/transactions/new來添加新的交易信息(注意我這里沒有使用默認的8080端口,默認的情況下是8080端口):

image

但是這時候還沒有新的區(qū)塊可以寫入這個交易信息悬垃,所以我們還需要請求 http://localhost:8089/BlockChain_Java/mine 來進行挖礦游昼,挖出一個新的區(qū)塊來存儲這筆交易:

image

在挖了兩次礦之后,就有3個塊了尝蠕,通過請求 http://localhost:8089/BlockChain_Java/chain 可以得到所有的區(qū)塊塊的信息:

 {
    "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
}

一致性(共識)

我們已經有了一個基本的區(qū)塊鏈可以接受交易和挖礦烘豌。但是區(qū)塊鏈系統應該是分布式的。既然是分布式的趟佃,那么我們究竟拿什么保證所有節(jié)點有同樣的鏈呢扇谣?這就是一致性問題,我們要想在網絡上有多個節(jié)點闲昭,就必須實現一個一致性的算法罐寨。

注冊節(jié)點

在實現一致性算法之前,我們需要找到一種方式讓一個節(jié)點知道它相鄰的節(jié)點序矩。每個節(jié)點都需要保存一份包含網絡中其它節(jié)點的記錄鸯绿。因此讓我們新增幾個接口:

  • /nodes/register 接收URL形式的新節(jié)點列表
  • /nodes/resolve執(zhí)行一致性算法,解決任何沖突簸淀,確保節(jié)點擁有正確的鏈

我們需要修改下BlockChain的構造函數并提供一個注冊節(jié)點方法:

package org.zero01.core;
...
import java.net.URL;
...
    private Set<String> nodes;
    private BlockChain() {
        ...
        // 用于存儲網絡中其他節(jié)點的集合
        nodes = new HashSet<String>();
        ...
    }
    public Set<String> getNodes() {
        return nodes;
    }
    /**
     * 注冊節(jié)點
     * 
     * @param address
     *            節(jié)點地址
     * @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 集合來儲存節(jié)點瓶蝴,這是一種避免出現重復添加節(jié)點的簡單方法。

實現共識算法

前面提到租幕,沖突是指不同的節(jié)點擁有不同的鏈舷手,為了解決這個問題,規(guī)定最長的劲绪、有效的鏈才是最終的鏈男窟,換句話說盆赤,網絡中有效最長鏈才是實際的鏈。

我們使用以下算法歉眷,來達到網絡中的共識:

...
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
...
public class BlockChain {
    ...
    /**
     * 檢查是否是有效鏈牺六,遍歷每個區(qū)塊驗證hash和proof,來確定一個給定的區(qū)塊鏈是否有效
     * 
     * @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;
    }
    /**
     * 共識算法解決沖突汗捡,使用網絡中最長的鏈. 遍歷所有的鄰居節(jié)點淑际,并用上一個方法檢查鏈的有效性, 如果發(fā)現有效更長鏈扇住,就替換掉自己的鏈
     * 
     * @return 如果鏈被取代返回true, 否則返回false
     * @throws IOException
     */
    public boolean resolveConflicts() throws IOException {
        Set<String> neighbours = this.nodes;
        List<Map<String, Object>> newChain = null;
        // 尋找最長的區(qū)塊鏈
        long maxLength = this.chain.size();
        // 獲取并驗證網絡中的所有節(jié)點的區(qū)塊鏈
        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;
                }
            }
        }
        // 如果發(fā)現一個新的有效鏈比我們的長,就替換當前的鏈
        if (newChain != null) {
            this.chain = newChain;
            return true;
        }
        return false;
    }
    ...

第一個方法 validChain() 用來檢查是否是有效鏈艘蹋,遍歷每個塊驗證hash和proof.

第2個方法 resolveConflicts() 用來解決沖突淡溯,遍歷所有的鄰居節(jié)點,并用上一個方法檢查鏈的有效性簿训, 如果發(fā)現有效更長鏈,就替換掉自己的鏈

讓我們添加兩個Servlet米间,一個用來注冊節(jié)點强品,一個用來解決沖突:

注冊節(jié)點:

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;
// 用于注冊節(jié)點的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());
        // 獲得節(jié)點集合數據,并進行判空
        List<String> nodes = (List) jsonValues.getJSONArray("nodes").toList();
        if (nodes == null) {
            resp.sendError(400, "Error: Please supply a valid list of nodes");
        }
        // 注冊節(jié)點
        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();
    }
}

我們可以在不同的機器運行節(jié)點屈糊,或在一臺機機開啟不同的網絡端口來模擬多節(jié)點的網絡的榛,這里在同一臺機器開啟不同的端口演示,配置兩個不同端口的服務器即可逻锐,我這里啟動了兩個節(jié)點:http://localhost:8089http://localhost:8066夫晌。

兩個節(jié)點互相進行注冊:

image

然后在8066節(jié)點上挖兩個塊,確保是更長的鏈:

image

接著在8089節(jié)點上訪問接口/nodes/resolve 昧诱,這時8089節(jié)點的鏈會通過共識算法被8066節(jié)點的鏈取代:

image

通過共識算法保持一致性后晓淀,兩個節(jié)點的區(qū)塊鏈數據就都是一致的了:

image

到此為止我們就完成了一個區(qū)塊鏈的開發(fā),雖然這只是一個最基本的區(qū)塊鏈盏档,而且在開發(fā)的過程中也沒有考慮太多的程序設計方面的問題凶掰,而是以最基本、原始的方式進行開發(fā)的蜈亩。但是我們不妨以這個簡單的區(qū)塊鏈為基礎懦窘,發(fā)揮自己的能力動手去重構、擴展稚配、完善這個區(qū)塊鏈程序畅涂,直至成為自己的一個小項目。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末道川,一起剝皮案震驚了整個濱河市午衰,隨后出現的幾起案子立宜,更是在濱河造成了極大的恐慌,老刑警劉巖苇经,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件赘理,死亡現場離奇詭異,居然都是意外死亡扇单,警方通過查閱死者的電腦和手機商模,發(fā)現死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蜘澜,“玉大人施流,你說我怎么就攤上這事”尚牛” “怎么了瞪醋?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長装诡。 經常有香客問我银受,道長,這世上最難降的妖魔是什么鸦采? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任宾巍,我火速辦了婚禮,結果婚禮上渔伯,老公的妹妹穿的比我還像新娘顶霞。我一直安慰自己,他們只是感情好锣吼,可當我...
    茶點故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布选浑。 她就那樣靜靜地躺著,像睡著了一般玄叠。 火紅的嫁衣襯著肌膚如雪古徒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天诸典,我揣著相機與錄音描函,去河邊找鬼。 笑死狐粱,一個胖子當著我的面吹牛舀寓,可吹牛的內容都是我干的。 我是一名探鬼主播肌蜻,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼互墓,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蒋搜?” 一聲冷哼從身側響起篡撵,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤判莉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后育谬,有當地人在樹林里發(fā)現了一具尸體券盅,經...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年膛檀,在試婚紗的時候發(fā)現自己被綠了锰镀。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡咖刃,死狀恐怖泳炉,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情嚎杨,我是刑警寧澤花鹅,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布,位于F島的核電站枫浙,受9級特大地震影響刨肃,放射性物質發(fā)生泄漏。R本人自食惡果不足惜箩帚,卻給世界環(huán)境...
    茶點故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一之景、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧膏潮,春花似錦、人聲如沸满力。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽油额。三九已至叠纷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間潦嘶,已是汗流浹背涩嚣。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掂僵,地道東北人航厚。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像锰蓬,于是被迫代替她去往敵國和親幔睬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內容