使用Java語言從零開始創(chuàng)建區(qū)塊鏈

區(qū)塊鏈

目前網(wǎng)絡上關于區(qū)塊鏈入門、科普的文章不少妄辩,本文就不再贅述區(qū)塊鏈的基本概念了春瞬,如果對區(qū)塊鏈不是很了解的話,可以看一下我之前收集的一些入門學習資源:

http://blog.51cto.com/zero01/2066321 

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

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

準備工作

掌握基本的JavaSE以及JavaWeb開發(fā),能夠使用Java開發(fā)簡單的項目杆融,并且需要了解HTTP協(xié)議楞卡。

我們知道區(qū)塊鏈是由區(qū)塊的記錄構成的不可變、有序的鏈結構脾歇,記錄可以是交易蒋腮、文件或任何你想要的數(shù)據(jù),重要的是它們是通過哈希值(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.dao;

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 數(shù)組
                byte byteBuffer[] = messageDigest.digest();

                // 將 byte 數(shù)組轉換 string 類型
                StringBuffer strHexString = new StringBuffer();
                // 遍歷 byte 數(shù)組
                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;
    }
}
加入交易功能

接下來我們需要實現(xiàn)一個交易\記賬功能估灿,所以來完善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
     *            交易數(shù)量
     * @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
     *            交易數(shù)量
     * @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ū)塊鏈的數(shù)據(jù)圃阳,但是現(xiàn)在只是完成了初步的代碼編寫厌衔,還有幾件事情還沒做,接下來我們看看區(qū)塊是怎么挖出來的捍岳。

理解工作量證明

新的區(qū)塊依賴工作量證明算法(PoW)來構造富寿。PoW的目標是找出一個符合特定條件的數(shù)字,這個數(shù)字很難計算出來锣夹,但容易驗證页徐。這就是工作量證明的核心思想。

為了方便理解银萍,舉個例子:

假設一個整數(shù) x 乘以另一個整數(shù) y 的積的 Hash 值必須以 0 結尾变勇,即 hash(x * y) = ac23dc…0。設變量 x = 5贴唇,求 y 的值搀绣?

用Java實現(xiàn)如下:

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ū)塊的權利而爭相計算結果物咳。通常锣险,計算難度與目標字符串需要滿足的特定字符的數(shù)量成正比,礦工算出結果后览闰,會獲得比特幣獎勵芯肤。
當然,在網(wǎng)絡上非常容易驗證這個結果压鉴。

實現(xiàn)工作量證明

讓我們來實現(xiàn)一個相似PoW算法崖咨,規(guī)則是:尋找一個數(shù) 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");
    }

衡量算法復雜度的辦法是修改零開頭的個數(shù)击蹲。使用4個來用于演示,你會發(fā)現(xiàn)多一個零都會大大增加計算出結果所需的時間婉宰。

現(xiàn)在Blockchain類基本已經完成了歌豺,接下來使用Servlet接收HTTP請求來進行交互。

Blockchain作為API接口

我們將使用Java Web中的Servlet來接收用戶的HTTP請求心包,通過Servlet我們可以方便的將網(wǎng)絡請求的數(shù)據(jù)映射到相應的方法上進行處理类咧,現(xiàn)在我們來讓Blockchain運行在基于Java Web上。

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

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

我們的“Tomcat服務器”將扮演區(qū)塊鏈網(wǎng)絡中的一個節(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> 

然后編寫一個類實現(xiàn)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ū)塊鏈的數(shù)據(jù)
@WebServlet("/chain")
public class FullChain extends HttpServlet{

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

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

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

{
 "sender": "my address",
 "recipient": "someone else's address",
 "amount": 5
}

實現(xiàn)代碼如下:

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");
        // 讀取客戶端傳遞過來的數(shù)據(jù)并轉換成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格式的數(shù)據(jù)給客戶端
        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ū)塊并將其添加到鏈中

代碼實現(xiàn)如下:

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ū)塊的數(shù)據(jù)給客戶端
        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端口):

postman

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

在挖了兩次礦之后账蓉,就有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ū)塊鏈系統(tǒng)應該是分布式的。既然是分布式的九孩,那么我們究竟拿什么保證所有節(jié)點有同樣的鏈呢宪拥?這就是一致性問題犁河,我們要想在網(wǎng)絡上有多個節(jié)點灭翔,就必須實現(xiàn)一個一致性的算法。

注冊節(jié)點

在實現(xiàn)一致性算法之前链嘀,我們需要找到一種方式讓一個節(jié)點知道它相鄰的節(jié)點霹琼。每個節(jié)點都需要保存一份包含網(wǎng)絡中其它節(jié)點的記錄天通。因此讓我們新增幾個接口:

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

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

package org.zero01.core;
...
import java.net.URL;
...

    private Set<String> nodes;
    private BlockChain() {
        ...
        // 用于存儲網(wǎng)絡中其他節(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é)點胃夏,這是一種避免出現(xiàn)重復添加節(jié)點的簡單方法。

實現(xiàn)共識算法

前面提到昌跌,沖突是指不同的節(jié)點擁有不同的鏈仰禀,為了解決這個問題,規(guī)定最長的蚕愤、有效的鏈才是最終的鏈答恶,換句話說饺蚊,網(wǎng)絡中有效最長鏈才是實際的鏈。

我們使用以下算法悬嗓,來達到網(wǎng)絡中的共識:

...
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;
    }

    /**
     * 共識算法解決沖突烫扼,使用網(wǎng)絡中最長的鏈. 遍歷所有的鄰居節(jié)點曙求,并用上一個方法檢查鏈的有效性碍庵, 如果發(fā)現(xiàn)有效更長鏈映企,就替換掉自己的鏈
     * 
     * @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();

        // 獲取并驗證網(wǎng)絡中的所有節(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ā)現(xiàn)一個新的有效鏈比我們的長静浴,就替換當前的鏈
        if (newChain != null) {
            this.chain = newChain;
            return true;
        }
        return false;
    }
    ...

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

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

讓我們添加兩個Servlet得问,一個用來注冊節(jié)點囤攀,一個用來解決沖突:

我們可以在不同的機器運行節(jié)點,或在一臺機機開啟不同的網(wǎng)絡端口來模擬多節(jié)點的網(wǎng)絡宫纬,這里在同一臺機器開啟不同的端口演示焚挠,配置兩個不同端口的服務器即可,我這里啟動了兩個節(jié)點:http://localhost:8089http://localhost:8066漓骚。

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


postman

postman

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


postman

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

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

postman

到此為止我們就完成了一個區(qū)塊鏈的開發(fā)噩斟,雖然這只是一個最基本的區(qū)塊鏈,而且在開發(fā)的過程中也沒有考慮太多的程序設計方面的問題孤个,而是以最基本剃允、原始的方式進行開發(fā)的。但是我們不妨以這個簡單的區(qū)塊鏈為基礎齐鲤,發(fā)揮自己的能力動手去重構斥废、擴展、完善這個區(qū)塊鏈程序佳遂,直至成為自己的一個小項目营袜。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市丑罪,隨后出現(xiàn)的幾起案子荚板,更是在濱河造成了極大的恐慌凤壁,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跪另,死亡現(xiàn)場離奇詭異拧抖,居然都是意外死亡,警方通過查閱死者的電腦和手機免绿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門唧席,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人嘲驾,你說我怎么就攤上這事淌哟。” “怎么了辽故?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵徒仓,是天一觀的道長。 經常有香客問我誊垢,道長掉弛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任喂走,我火速辦了婚禮殃饿,結果婚禮上,老公的妹妹穿的比我還像新娘芋肠。我一直安慰自己乎芳,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布业栅。 她就那樣靜靜地躺著秒咐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碘裕。 梳的紋絲不亂的頭發(fā)上携取,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音帮孔,去河邊找鬼雷滋。 笑死,一個胖子當著我的面吹牛文兢,可吹牛的內容都是我干的晤斩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼姆坚,長吁一口氣:“原來是場噩夢啊……” “哼澳泵!你這毒婦竟也來了?” 一聲冷哼從身側響起兼呵,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤兔辅,失蹤者是張志新(化名)和其女友劉穎腊敲,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體维苔,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡碰辅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了介时。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片没宾。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖沸柔,靈堂內的尸體忽然破棺而出循衰,到底是詐尸還是另有隱情,我是刑警寧澤勉失,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布羹蚣,位于F島的核電站,受9級特大地震影響乱凿,放射性物質發(fā)生泄漏。R本人自食惡果不足惜咽弦,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一徒蟆、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧型型,春花似錦段审、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至绷落,卻和暖如春姥闪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背砌烁。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工筐喳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人函喉。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓避归,卻偏偏與公主長得像,于是被迫代替她去往敵國和親管呵。 傳聞我的和親對象是個殘疾皇子梳毙,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容